Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 930509d6be | |||
| c513c723ed | |||
| b34a264aec | |||
| a83c2f702f | |||
| 8644bf24fb | |||
| 69a979cb98 | |||
| 6ba43e70ea | |||
| 6f19009000 | |||
| 64677ad6ce | |||
| 3894309706 | |||
| e44f16258f | |||
| 1e3cf35b7b | |||
| 4bfb3f1774 | |||
| e056336955 | |||
| 64d2959a27 | |||
| eb1675d4fd | |||
| ca7e48cbe7 | |||
| 653f2817bc | |||
| edff806647 | |||
| c47d623118 | |||
| b270cf86ab | |||
| 5c2fd7dc97 | |||
| d21ef34392 | |||
| e6d54b0c33 | |||
| 7a1e5bca74 | |||
| 1996812c90 | |||
| 6e1f8f12d4 | |||
| 625e1ab169 | |||
| 52c33518ad | |||
| 3f6fc327ff | |||
| e3e4e4dd46 | |||
| b60e567a83 | |||
| 17e0ee5dd8 | |||
| 97909bacce | |||
| 30b0189f61 | |||
| 5bde849bf0 | |||
| b3b519ab4c | |||
| f3b62cc756 | |||
| a56d3314ad | |||
| f75d743cd9 | |||
| f8876d821e | |||
| bca8fde2de | |||
| efbaa465b2 | |||
| 20f1d72058 | |||
| 6c3b1efe97 | |||
| a98f090b6a | |||
| 542f65f044 | |||
| f4a1141f65 | |||
| a034c7dc6f | |||
| d9c6d18921 | |||
| 7eb3d601fa | |||
| 9b375b99dc | |||
| ee80976e92 | |||
| 10aea39cae | |||
| 431fdc6428 | |||
| eda60a3c3d | |||
| d7b90f1f58 | |||
| 08b30307aa | |||
| 751ca765d5 | |||
| 68ea5a9468 | |||
| 46ab845c8e | |||
| b597704115 | |||
| 889e2257ab | |||
| e962008b4c | |||
| d6c1d13d7d | |||
| 922a794d09 | |||
| 96092a208c | |||
| b71729035d | |||
| 2abba6f0cc | |||
| f081b7549d | |||
| 55fbae7b4a | |||
| 2acbde2efa | |||
| acbdf05d4f | |||
| c325caa914 | |||
| dd54d81573 | |||
| ebc282d5c2 | |||
| 80ce253f92 | |||
| fe2395066f | |||
| 3e66b4b4d4 | |||
| 58bbc97a1b | |||
| 8d9e9d92f4 | |||
| 03392c21e0 | |||
| 924170d159 | |||
| b355eb4e09 | |||
| 1373a816a6 | |||
| 196b04a703 | |||
| ac971e65a6 | |||
| 2c50502b28 | |||
| c222b5a745 | |||
| 2e86a6503c | |||
| 9ebd7fa7fe | |||
| 5197a16023 | |||
| 159627c63e | |||
| 07230cce1a | |||
| a33aee80d1 | |||
| e1d3da1ddb | |||
| 8c2524d990 | |||
| be1b7aa05f | |||
| 4b57fee75c | |||
| fc9c43a23c | |||
| fd4e4e3b77 | |||
| f1200252be | |||
| 046e64b2b8 | |||
| ad9e40ad1c | |||
| f19eaf7ec9 | |||
| 012291c1c9 | |||
| 70ba1c757e | |||
| 3ab47b568d | |||
| 6fd985d2ad | |||
| cf028e0e36 | |||
| f9e6dafc2c | |||
| e562a259fb | |||
| 9594b560d0 | |||
| bf2dae9569 | |||
| 90ac3a4336 | |||
| 395364b4eb | |||
| 6a2bb94037 | |||
| 7d81953b83 | |||
| feb69174ae | |||
| 00eee68aab | |||
| 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 | |||
| af1ab75c87 | |||
| e563ba2f99 | |||
| ea7796459a | |||
| 1dfce31580 | |||
| 5eb08dcca3 | |||
| 8565485253 | |||
| cc744a20c7 | |||
| c43b10d38d | |||
| ed61a41267 | |||
| 58d1695cba | |||
| 6bdfc917e3 | |||
| af739b0efb | |||
| cbaf1cf630 | |||
| 6f203c94b0 | |||
| 81852fb94c | |||
| d0028620e6 | |||
| 0e021f27fe | |||
| bd0996d39b | |||
| 0c383da201 | |||
| 8a7b9d6d7b | |||
| d3b9a68443 | |||
| 765383157d | |||
| 1f8ee401fa | |||
| 2200c4169d | |||
| cd485a0464 |
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2018] [Syed Haris Ali]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -9,7 +9,7 @@ EXTERNAL SOURCES:
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
SwiftAudioPlayer: 6d47a258720feecb431509eb71215559621fa12d
|
||||
SwiftAudioPlayer: abfeb4ac2467cdd7b5b8a5cb442780184ea172bc
|
||||
|
||||
PODFILE CHECKSUM: 84ea27746bf895da86125356a8d0df7a323c4c08
|
||||
|
||||
|
||||
+6
-4
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "SwiftAudioPlayer",
|
||||
"version": "0.1.0",
|
||||
"summary": "A short description of SwiftAudioPlayer.",
|
||||
"description": "TODO: Add long description of the pod here.",
|
||||
"summary": "SwiftAudioPlayer is a Swift based audio player that can handle streaming from a remote location and audio manipulation.",
|
||||
"description": "SwiftAudioPlayer is a Swift based audio player that can handle streaming from a remote location and audio manipulation. It can perform actions like playing audio up to 32x playback rate on streamed audio.",
|
||||
"homepage": "https://github.com/tanhakabir/SwiftAudioPlayer",
|
||||
"license": {
|
||||
"type": "MIT",
|
||||
"file": "LICENSE"
|
||||
},
|
||||
"authors": {
|
||||
"tanhakabir": "tanhakabir.ca@gmail.com"
|
||||
"tanhakabir": "tanhakabir.ca@gmail.com",
|
||||
"JonMercer": "mercer.jon@gmail.com"
|
||||
},
|
||||
"source": {
|
||||
"git": "https://github.com/tanhakabir/SwiftAudioPlayer.git",
|
||||
@@ -18,5 +19,6 @@
|
||||
"platforms": {
|
||||
"ios": "8.0"
|
||||
},
|
||||
"source_files": "SwiftAudioPlayer/Classes/**/*"
|
||||
"source_files": "SwiftAudioPlayer/Classes/**/*",
|
||||
"swift_version": "4.0"
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -9,7 +9,7 @@ EXTERNAL SOURCES:
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
SwiftAudioPlayer: 6d47a258720feecb431509eb71215559621fa12d
|
||||
SwiftAudioPlayer: abfeb4ac2467cdd7b5b8a5cb442780184ea172bc
|
||||
|
||||
PODFILE CHECKSUM: 84ea27746bf895da86125356a8d0df7a323c4c08
|
||||
|
||||
|
||||
+261
-21
@@ -7,7 +7,6 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1606D9AEB876D4A642C3CE2985C600C9 /* ReplaceMe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3EB275E4B4A4F3D26D5B3835EA81466 /* ReplaceMe.swift */; };
|
||||
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 */; };
|
||||
@@ -15,6 +14,45 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
A4681FC9220113920018AB51 /* LockScreenViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */; };
|
||||
A4681FCB220113980018AB51 /* AudioEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F822200D9150018AB51 /* AudioEngine.swift */; };
|
||||
A4681FCC2201139B0018AB51 /* AudioDiskEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F942200E2220018AB51 /* AudioDiskEngine.swift */; };
|
||||
A4681FCD2201139E0018AB51 /* AudioStreamEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FBC220100AB0018AB51 /* AudioStreamEngine.swift */; };
|
||||
A4681FCE220113A20018AB51 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FB62200FE090018AB51 /* AudioConverter.swift */; };
|
||||
A4681FCF220113A40018AB51 /* AudioConverterListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FBA2201002F0018AB51 /* AudioConverterListener.swift */; };
|
||||
A4681FD0220113A70018AB51 /* AudioConverterErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FB82200FE7F0018AB51 /* AudioConverterErrors.swift */; };
|
||||
A4681FD1220113AF0018AB51 /* AudioParsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FAB2200F8490018AB51 /* AudioParsable.swift */; };
|
||||
A4681FD2220113B20018AB51 /* AudioParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FAD2200F8E90018AB51 /* AudioParser.swift */; };
|
||||
A4681FD3220113B60018AB51 /* AudioParserPropertyListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FB12200FB020018AB51 /* AudioParserPropertyListener.swift */; };
|
||||
A4681FD4220113BA0018AB51 /* AudioParserPacketListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FB32200FC520018AB51 /* AudioParserPacketListener.swift */; };
|
||||
A4681FD5220113BD0018AB51 /* AudioParserErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FAF2200FA6C0018AB51 /* AudioParserErrors.swift */; };
|
||||
A4681FD6220113BF0018AB51 /* AudioThrottler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FA82200F5A20018AB51 /* AudioThrottler.swift */; };
|
||||
A4681FD7220113C30018AB51 /* StreamProgressPTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FA62200F0130018AB51 /* StreamProgressPTO.swift */; };
|
||||
A4681FD8220113C60018AB51 /* AudioDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F992200E3D90018AB51 /* AudioDataManager.swift */; };
|
||||
A4681FD9220113CD0018AB51 /* AudioStreamWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F9C2200E4B40018AB51 /* AudioStreamWorker.swift */; };
|
||||
A4681FDA220113D00018AB51 /* StreamProgressDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F9E2200E5DE0018AB51 /* StreamProgressDTO.swift */; };
|
||||
A4681FDB220113D40018AB51 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FA42200E7920018AB51 /* FileStorage.swift */; };
|
||||
A4681FDC220113D70018AB51 /* AudioDownloadWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FA22200E6710018AB51 /* AudioDownloadWorker.swift */; };
|
||||
A4681FDD220113DC0018AB51 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F962200E2E20018AB51 /* URL.swift */; };
|
||||
A4681FDE220113DE0018AB51 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F892200DB3C0018AB51 /* Date.swift */; };
|
||||
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 */; };
|
||||
A470FE0825F9ADF800F135FF /* AudioClockDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */; };
|
||||
A470FE0925F9ADF800F135FF /* DownloadProgressDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */; };
|
||||
A470FE1C25F9AEB900F135FF /* AudioQueueDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */; };
|
||||
A470FE2125F9AF1400F135FF /* AudioQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE2025F9AF1400F135FF /* AudioQueue.swift */; };
|
||||
A4827771262A216C00B6918A /* StreamingDownloadDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */; };
|
||||
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */; };
|
||||
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 */
|
||||
@@ -41,7 +79,7 @@
|
||||
030E0D4C0BE29E2606B0BCB65B9BBC42 /* Pods-SwiftAudioPlayer_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SwiftAudioPlayer_Tests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
0B3AF0F1A1DF1101E93137959D2E5F24 /* Pods-SwiftAudioPlayer_Example-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-SwiftAudioPlayer_Example-acknowledgements.markdown"; sourceTree = "<group>"; };
|
||||
0E268C8D5FBBF7E0E790D3AA6A70FEC2 /* SwiftAudioPlayer-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftAudioPlayer-umbrella.h"; sourceTree = "<group>"; };
|
||||
15DF3E7F1B5E10B1BBE49D3E9A67C938 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE; sourceTree = "<group>"; };
|
||||
15DF3E7F1B5E10B1BBE49D3E9A67C938 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
16FCEC9685DAD30C0013E9ECD938611E /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
305774DC5582C6E1BA1511DED1ECB225 /* Pods-SwiftAudioPlayer_Tests-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Tests-frameworks.sh"; sourceTree = "<group>"; };
|
||||
314D056517A7B04FFAFF279157B7ADBB /* Pods-SwiftAudioPlayer_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SwiftAudioPlayer_Tests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -49,28 +87,67 @@
|
||||
3B0B76CB1439F4D361322144E5A65C3A /* Pods-SwiftAudioPlayer_Example-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-SwiftAudioPlayer_Example-umbrella.h"; sourceTree = "<group>"; };
|
||||
3F45E3A0690F048214FCE84887950057 /* SwiftAudioPlayer-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftAudioPlayer-prefix.pch"; sourceTree = "<group>"; };
|
||||
41C6A056512760933DE244855EF94DF0 /* Pods-SwiftAudioPlayer_Tests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-SwiftAudioPlayer_Tests.modulemap"; sourceTree = "<group>"; };
|
||||
509D93CD81F074F6E7C4B9DE13210ACF /* Pods_SwiftAudioPlayer_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_SwiftAudioPlayer_Example.framework; path = "Pods-SwiftAudioPlayer_Example.framework"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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; lastKnownFileType = text; path = SwiftAudioPlayer.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
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>"; };
|
||||
782193D2A4B5EA65A5A468B871418969 /* SwiftAudioPlayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SwiftAudioPlayer.framework; path = SwiftAudioPlayer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
782193D2A4B5EA65A5A468B871418969 /* SwiftAudioPlayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftAudioPlayer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8F12318E3F0F591F1C2ACAE6F204F753 /* Pods-SwiftAudioPlayer_Tests-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Tests-resources.sh"; sourceTree = "<group>"; };
|
||||
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
9622E16E03B20FC0C41123BA8A50C1F0 /* Pods-SwiftAudioPlayer_Tests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-SwiftAudioPlayer_Tests-acknowledgements.plist"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
A3EB275E4B4A4F3D26D5B3835EA81466 /* ReplaceMe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ReplaceMe.swift; path = SwiftAudioPlayer/Classes/ReplaceMe.swift; sourceTree = "<group>"; };
|
||||
A40DBE282391D9C900F86146 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
||||
A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerFeatures.swift; 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>"; };
|
||||
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectorThreadSafeClosures.swift; sourceTree = "<group>"; };
|
||||
A4681F892200DB3C0018AB51 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
|
||||
A4681F8B2200DDD50018AB51 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
A4681F8D2200E00E0018AB51 /* SAPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayer.swift; sourceTree = "<group>"; };
|
||||
A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerPresenter.swift; sourceTree = "<group>"; };
|
||||
A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerDelegate.swift; sourceTree = "<group>"; };
|
||||
A4681F942200E2220018AB51 /* AudioDiskEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDiskEngine.swift; sourceTree = "<group>"; };
|
||||
A4681F962200E2E20018AB51 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
|
||||
A4681F992200E3D90018AB51 /* AudioDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AudioDataManager.swift; path = ../../Source/Model/AudioDataManager.swift; sourceTree = "<group>"; };
|
||||
A4681F9C2200E4B40018AB51 /* AudioStreamWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AudioStreamWorker.swift; path = ../../Model/Streaming/AudioStreamWorker.swift; sourceTree = "<group>"; };
|
||||
A4681F9E2200E5DE0018AB51 /* StreamProgressDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StreamProgressDTO.swift; path = ../../Model/Streaming/StreamProgressDTO.swift; sourceTree = "<group>"; };
|
||||
A4681FA22200E6710018AB51 /* AudioDownloadWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AudioDownloadWorker.swift; path = ../../Model/Downloading/AudioDownloadWorker.swift; sourceTree = "<group>"; };
|
||||
A4681FA42200E7920018AB51 /* FileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FileStorage.swift; path = ../../Model/Downloading/FileStorage.swift; sourceTree = "<group>"; };
|
||||
A4681FA62200F0130018AB51 /* StreamProgressPTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StreamProgressPTO.swift; path = ../Model/StreamProgressPTO.swift; sourceTree = "<group>"; };
|
||||
A4681FA82200F5A20018AB51 /* AudioThrottler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioThrottler.swift; sourceTree = "<group>"; };
|
||||
A4681FAB2200F8490018AB51 /* AudioParsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioParsable.swift; sourceTree = "<group>"; };
|
||||
A4681FAD2200F8E90018AB51 /* AudioParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioParser.swift; sourceTree = "<group>"; };
|
||||
A4681FAF2200FA6C0018AB51 /* AudioParserErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioParserErrors.swift; sourceTree = "<group>"; };
|
||||
A4681FB12200FB020018AB51 /* AudioParserPropertyListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioParserPropertyListener.swift; sourceTree = "<group>"; };
|
||||
A4681FB32200FC520018AB51 /* AudioParserPacketListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioParserPacketListener.swift; sourceTree = "<group>"; };
|
||||
A4681FB62200FE090018AB51 /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; };
|
||||
A4681FB82200FE7F0018AB51 /* AudioConverterErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterErrors.swift; sourceTree = "<group>"; };
|
||||
A4681FBA2201002F0018AB51 /* AudioConverterListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterListener.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioClockDirector.swift; sourceTree = "<group>"; };
|
||||
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadProgressDirector.swift; sourceTree = "<group>"; };
|
||||
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioQueueDirector.swift; sourceTree = "<group>"; };
|
||||
A470FE2025F9AF1400F135FF /* AudioQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioQueue.swift; sourceTree = "<group>"; };
|
||||
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingDownloadDirector.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>"; };
|
||||
AB41D88A2C694FBDF26EA56381EED25F /* Pods-SwiftAudioPlayer_Example-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-SwiftAudioPlayer_Example-dummy.m"; sourceTree = "<group>"; };
|
||||
B8C829A46249957CD3056074B5CC0BBB /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = "<group>"; };
|
||||
B8C829A46249957CD3056074B5CC0BBB /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
B9A6DFC8AB64B139080060EA639B3A7D /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BCAD67E3D7744FEFA5B221BDA7B25B20 /* SwiftAudioPlayer.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftAudioPlayer.xcconfig; sourceTree = "<group>"; };
|
||||
BF5B667B9103284C373811A04411C7C1 /* Pods-SwiftAudioPlayer_Example-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-SwiftAudioPlayer_Example-acknowledgements.plist"; sourceTree = "<group>"; };
|
||||
E1C110BCB4A9F826B59DC6905BAB3C6E /* Pods-SwiftAudioPlayer_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SwiftAudioPlayer_Example.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
F3E4AB96148429D903B6E5DAEB19C4C1 /* Pods_SwiftAudioPlayer_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_SwiftAudioPlayer_Tests.framework; path = "Pods-SwiftAudioPlayer_Tests.framework"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F3E4AB96148429D903B6E5DAEB19C4C1 /* Pods_SwiftAudioPlayer_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FB83B3B4253D41C37C5563D34D450BF8 /* SwiftAudioPlayer-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SwiftAudioPlayer-dummy.m"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -115,7 +192,7 @@
|
||||
41C7F403DA52FBC5C40644BB0E824CAA /* SwiftAudioPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A3EB275E4B4A4F3D26D5B3835EA81466 /* ReplaceMe.swift */,
|
||||
A4681FE2220117B50018AB51 /* Source */,
|
||||
840F8E752B4437107D761C28D4EE8D0B /* Pod */,
|
||||
EAE1BCB45D8F275CE4428674B5151284 /* Support Files */,
|
||||
);
|
||||
@@ -193,10 +270,121 @@
|
||||
15DF3E7F1B5E10B1BBE49D3E9A67C938 /* LICENSE */,
|
||||
B8C829A46249957CD3056074B5CC0BBB /* README.md */,
|
||||
6EC04ECC8F7CB2AF2E4E042A6A8ECFA1 /* SwiftAudioPlayer.podspec */,
|
||||
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */,
|
||||
);
|
||||
name = Pod;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4681F842200D91D0018AB51 /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4681F8B2200DDD50018AB51 /* Constants.swift */,
|
||||
A4681F802200D0500018AB51 /* Log.swift */,
|
||||
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */,
|
||||
A4681F892200DB3C0018AB51 /* Date.swift */,
|
||||
A4681F962200E2E20018AB51 /* URL.swift */,
|
||||
A40DBE282391D9C900F86146 /* Data.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4681F932200E2020018AB51 /* Engine */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */,
|
||||
A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */,
|
||||
A4681F822200D9150018AB51 /* AudioEngine.swift */,
|
||||
A4681F942200E2220018AB51 /* AudioDiskEngine.swift */,
|
||||
A4681FBC220100AB0018AB51 /* AudioStreamEngine.swift */,
|
||||
A4681FB52200FDF30018AB51 /* Converter */,
|
||||
A4681FAA2200F8280018AB51 /* Parser */,
|
||||
A4681FA82200F5A20018AB51 /* AudioThrottler.swift */,
|
||||
);
|
||||
path = Engine;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4681F9B2200E4850018AB51 /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A470FE2025F9AF1400F135FF /* AudioQueue.swift */,
|
||||
A4681F992200E3D90018AB51 /* AudioDataManager.swift */,
|
||||
A4681FA62200F0130018AB51 /* StreamProgressPTO.swift */,
|
||||
A4681FA02200E5F50018AB51 /* Streaming */,
|
||||
A4681FA12200E6540018AB51 /* Downloading */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4681FA02200E5F50018AB51 /* Streaming */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4681F9E2200E5DE0018AB51 /* StreamProgressDTO.swift */,
|
||||
A4681F9C2200E4B40018AB51 /* AudioStreamWorker.swift */,
|
||||
);
|
||||
path = Streaming;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4681FA12200E6540018AB51 /* Downloading */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4681FA22200E6710018AB51 /* AudioDownloadWorker.swift */,
|
||||
A4681FA42200E7920018AB51 /* FileStorage.swift */,
|
||||
);
|
||||
path = Downloading;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4681FAA2200F8280018AB51 /* Parser */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4681FAB2200F8490018AB51 /* AudioParsable.swift */,
|
||||
A4681FAD2200F8E90018AB51 /* AudioParser.swift */,
|
||||
A4681FB12200FB020018AB51 /* AudioParserPropertyListener.swift */,
|
||||
A4681FB32200FC520018AB51 /* AudioParserPacketListener.swift */,
|
||||
A4681FAF2200FA6C0018AB51 /* AudioParserErrors.swift */,
|
||||
);
|
||||
path = Parser;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4681FB52200FDF30018AB51 /* Converter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4681FB62200FE090018AB51 /* AudioConverter.swift */,
|
||||
A4681FBA2201002F0018AB51 /* AudioConverterListener.swift */,
|
||||
A4681FB82200FE7F0018AB51 /* AudioConverterErrors.swift */,
|
||||
);
|
||||
path = Converter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A4681FE2220117B50018AB51 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */,
|
||||
A4681F8D2200E00E0018AB51 /* SAPlayer.swift */,
|
||||
A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */,
|
||||
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */,
|
||||
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */,
|
||||
A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */,
|
||||
A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */,
|
||||
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */,
|
||||
A4681F932200E2020018AB51 /* Engine */,
|
||||
A470FE0D25F9AE1800F135FF /* Directors */,
|
||||
A4681F9B2200E4850018AB51 /* Model */,
|
||||
A4681F842200D91D0018AB51 /* Util */,
|
||||
);
|
||||
path = Source;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A470FE0D25F9AE1800F135FF /* Directors */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */,
|
||||
A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */,
|
||||
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */,
|
||||
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */,
|
||||
);
|
||||
path = Directors;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -317,14 +505,23 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0930;
|
||||
LastUpgradeCheck = 0930;
|
||||
LastUpgradeCheck = 1010;
|
||||
TargetAttributes = {
|
||||
042ACE071BA515F4DE0E0C8007C3F0EE = {
|
||||
LastSwiftMigration = 1010;
|
||||
};
|
||||
E50DAD13FFD3FC8036073A58BF8423D4 = {
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 7DB346D0F39D3F0E887471402A8071AB;
|
||||
productRefGroup = 21D946895A4F57F51246F3EBCF330719 /* Products */;
|
||||
@@ -343,8 +540,46 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1606D9AEB876D4A642C3CE2985C600C9 /* ReplaceMe.swift in Sources */,
|
||||
A470FE0925F9ADF800F135FF /* DownloadProgressDirector.swift in Sources */,
|
||||
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */,
|
||||
A470FE1C25F9AEB900F135FF /* AudioQueueDirector.swift in Sources */,
|
||||
A4681FDC220113D70018AB51 /* AudioDownloadWorker.swift in Sources */,
|
||||
A4681FD8220113C60018AB51 /* AudioDataManager.swift in Sources */,
|
||||
A4681FD1220113AF0018AB51 /* AudioParsable.swift in Sources */,
|
||||
A4681FD2220113B20018AB51 /* AudioParser.swift in Sources */,
|
||||
A4681FCF220113A40018AB51 /* AudioConverterListener.swift in Sources */,
|
||||
A4681FE1220113E70018AB51 /* Constants.swift in Sources */,
|
||||
A40DBE292391D9CA00F86146 /* Data.swift in Sources */,
|
||||
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */,
|
||||
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */,
|
||||
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */,
|
||||
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */,
|
||||
A4681FD5220113BD0018AB51 /* AudioParserErrors.swift in Sources */,
|
||||
A4681FC9220113920018AB51 /* LockScreenViewProtocol.swift in Sources */,
|
||||
A4827771262A216C00B6918A /* StreamingDownloadDirector.swift in Sources */,
|
||||
A4681FD6220113BF0018AB51 /* AudioThrottler.swift in Sources */,
|
||||
A4681FCC2201139B0018AB51 /* AudioDiskEngine.swift in Sources */,
|
||||
A4681FDE220113DE0018AB51 /* Date.swift in Sources */,
|
||||
A4681FDA220113D00018AB51 /* StreamProgressDTO.swift in Sources */,
|
||||
A4681FDB220113D40018AB51 /* FileStorage.swift in Sources */,
|
||||
A4681FD4220113BA0018AB51 /* AudioParserPacketListener.swift in Sources */,
|
||||
E08AD6157EF688FE832F866CBCDA3532 /* SwiftAudioPlayer-dummy.m in Sources */,
|
||||
A4681FDD220113DC0018AB51 /* URL.swift in Sources */,
|
||||
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */,
|
||||
A470FE2125F9AF1400F135FF /* AudioQueue.swift in Sources */,
|
||||
A4681FD3220113B60018AB51 /* AudioParserPropertyListener.swift in Sources */,
|
||||
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */,
|
||||
A4681FD0220113A70018AB51 /* AudioConverterErrors.swift in Sources */,
|
||||
A4681FD7220113C30018AB51 /* StreamProgressPTO.swift in Sources */,
|
||||
A4681FE0220113E40018AB51 /* Log.swift in Sources */,
|
||||
A4681FCE220113A20018AB51 /* AudioConverter.swift in Sources */,
|
||||
A470FE0825F9ADF800F135FF /* AudioClockDirector.swift in Sources */,
|
||||
A4FBA6B9221BAF8700D5A353 /* SAAudioAvailabilityRange.swift in Sources */,
|
||||
A4681FCD2201139E0018AB51 /* AudioStreamEngine.swift in Sources */,
|
||||
A411CE4625F9609D0039E1CD /* SAPlayerFeatures.swift in Sources */,
|
||||
A4681FD9220113CD0018AB51 /* AudioStreamWorker.swift in Sources */,
|
||||
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */,
|
||||
A4681FCB220113980018AB51 /* AudioEngine.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -435,7 +670,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -461,7 +696,7 @@
|
||||
GCC_PREFIX_HEADER = "Target Support Files/SwiftAudioPlayer/SwiftAudioPlayer-prefix.pch";
|
||||
INFOPLIST_FILE = "Target Support Files/SwiftAudioPlayer/Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MODULEMAP_FILE = "Target Support Files/SwiftAudioPlayer/SwiftAudioPlayer.modulemap";
|
||||
PRODUCT_MODULE_NAME = SwiftAudioPlayer;
|
||||
@@ -470,7 +705,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -493,7 +728,7 @@
|
||||
GCC_PREFIX_HEADER = "Target Support Files/SwiftAudioPlayer/SwiftAudioPlayer-prefix.pch";
|
||||
INFOPLIST_FILE = "Target Support Files/SwiftAudioPlayer/Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MODULEMAP_FILE = "Target Support Files/SwiftAudioPlayer/SwiftAudioPlayer.modulemap";
|
||||
PRODUCT_MODULE_NAME = SwiftAudioPlayer;
|
||||
@@ -502,7 +737,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -515,6 +750,7 @@
|
||||
baseConfigurationReference = E1C110BCB4A9F826B59DC6905BAB3C6E /* Pods-SwiftAudioPlayer_Example.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
@@ -526,7 +762,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = "Target Support Files/Pods-SwiftAudioPlayer_Example/Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MACH_O_TYPE = staticlib;
|
||||
MODULEMAP_FILE = "Target Support Files/Pods-SwiftAudioPlayer_Example/Pods-SwiftAudioPlayer_Example.modulemap";
|
||||
@@ -539,6 +775,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -595,10 +832,11 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRIP_INSTALLED_PRODUCT = NO;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SYMROOT = "${SRCROOT}/../build";
|
||||
};
|
||||
name = Release;
|
||||
@@ -608,6 +846,7 @@
|
||||
baseConfigurationReference = 69AF5444212FEC2674325627F26305AD /* Pods-SwiftAudioPlayer_Example.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
@@ -619,7 +858,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = "Target Support Files/Pods-SwiftAudioPlayer_Example/Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MACH_O_TYPE = staticlib;
|
||||
MODULEMAP_FILE = "Target Support Files/Pods-SwiftAudioPlayer_Example/Pods-SwiftAudioPlayer_Example.modulemap";
|
||||
@@ -631,6 +870,7 @@
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -14,6 +14,7 @@
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
|
||||
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 */
|
||||
|
||||
@@ -43,6 +44,7 @@
|
||||
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = "<group>"; };
|
||||
65A66AB4C3016E8BB53FF3E0 /* Pods-SwiftAudioPlayer_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Example/Pods-SwiftAudioPlayer_Example.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
A470FEE1260303DA00F135FF /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = "<group>"; };
|
||||
AF6A2C6BF79C291056D27D5D /* SwiftAudioPlayer.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = SwiftAudioPlayer.podspec; path = ../SwiftAudioPlayer.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
BBD877782CC67FBCC7BF7532 /* Pods-SwiftAudioPlayer_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Tests/Pods-SwiftAudioPlayer_Tests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
DA80DEA33D13EC91EB531881 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||
@@ -115,6 +117,7 @@
|
||||
children = (
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
|
||||
607FACD71AFB9204008FA782 /* ViewController.swift */,
|
||||
A470FEE1260303DA00F135FF /* Model.swift */,
|
||||
607FACD91AFB9204008FA782 /* Main.storyboard */,
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */,
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
|
||||
@@ -207,23 +210,25 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0830;
|
||||
LastUpgradeCheck = 0830;
|
||||
LastUpgradeCheck = 1010;
|
||||
ORGANIZATIONNAME = CocoaPods;
|
||||
TargetAttributes = {
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
LastSwiftMigration = 0900;
|
||||
DevelopmentTeam = H9Y26B6GZB;
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
607FACE41AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
LastSwiftMigration = 0900;
|
||||
DevelopmentTeam = H9Y26B6GZB;
|
||||
LastSwiftMigration = 1120;
|
||||
TestTargetID = 607FACCF1AFB9204008FA782;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudioPlayer" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
@@ -322,6 +327,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A470FEE2260303DA00F135FF /* Model.swift in Sources */,
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
@@ -377,12 +383,14 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -410,7 +418,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -430,12 +438,14 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -456,7 +466,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
@@ -469,13 +479,13 @@
|
||||
baseConfigurationReference = 65A66AB4C3016E8BB53FF3E0 /* Pods-SwiftAudioPlayer_Example.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = H9Y26B6GZB;
|
||||
INFOPLIST_FILE = SwiftAudioPlayer/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo-test.SwiftAudioPlayer-Example";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -484,13 +494,13 @@
|
||||
baseConfigurationReference = 4B5DD2AE0B23A759D18926DC /* Pods-SwiftAudioPlayer_Example.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = H9Y26B6GZB;
|
||||
INFOPLIST_FILE = SwiftAudioPlayer/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo-test.SwiftAudioPlayer-Example";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -498,6 +508,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BBD877782CC67FBCC7BF7532 /* Pods-SwiftAudioPlayer_Tests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = H9Y26B6GZB;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
@@ -510,8 +521,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 = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -520,6 +530,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 0B7D1E6C00E83B4AF8AA1781 /* Pods-SwiftAudioPlayer_Tests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = H9Y26B6GZB;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
@@ -528,8 +539,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 = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
+1
-3
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0900"
|
||||
LastUpgradeVersion = "1010"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -40,7 +40,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
@@ -70,7 +69,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftAudioPlayer
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
@@ -14,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
|
||||
}
|
||||
@@ -40,6 +41,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
|
||||
SAPlayer.Downloader.setBackgroundCompletionHandler(completionHandler)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<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">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -20,11 +18,216 @@
|
||||
<view key="view" contentMode="scaleToFill" id="kh9-bI-dsS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lTK-Hd-Tl2">
|
||||
<rect key="frame" x="16" y="303" width="343" height="4"/>
|
||||
<color key="tintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="progressTintColor" red="0.46202266219999999" green="0.83828371759999998" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="trackTintColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</progressView>
|
||||
<slider opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="w2a-RA-zmI">
|
||||
<rect key="frame" x="14" y="289" width="347" height="31"/>
|
||||
<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="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="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jUc-tP-CC5">
|
||||
<rect key="frame" x="172.5" y="233" width="30" height="30"/>
|
||||
<state key="normal" title="play"/>
|
||||
<connections>
|
||||
<action selector="playPauseTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Avk-K3-EZ7"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tFH-sY-Xu9">
|
||||
<rect key="frame" x="62.5" y="233" width="30" height="30"/>
|
||||
<state key="normal" title="-15"/>
|
||||
<connections>
|
||||
<action selector="skipBackwardTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="PCT-BE-udf"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0QE-3F-a4G">
|
||||
<rect key="frame" x="282.5" y="233" width="30" height="30"/>
|
||||
<state key="normal" title="+30"/>
|
||||
<connections>
|
||||
<action selector="skipForwardTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="uXv-bz-tnt"/>
|
||||
</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="448" width="347" height="31"/>
|
||||
<connections>
|
||||
<action selector="rateChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="FDJ-jA-bm8"/>
|
||||
</connections>
|
||||
</slider>
|
||||
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="300" minValue="0.10000000149011612" maxValue="1000" translatesAutoresizingMaskIntoConstraints="NO" id="nsl-df-P21">
|
||||
<rect key="frame" x="14" y="381" width="347" height="31"/>
|
||||
<connections>
|
||||
<action selector="reverbChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="J8Q-be-35q"/>
|
||||
</connections>
|
||||
</slider>
|
||||
<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="60" width="343" height="32"/>
|
||||
<segments>
|
||||
<segment title="Soundbite"/>
|
||||
<segment title="Podcast"/>
|
||||
<segment title="Radio"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="audioSelected:" destination="vXZ-lx-hvc" eventType="valueChanged" id="oYE-yq-348"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KDu-ea-kF8">
|
||||
<rect key="frame" x="43" y="123" width="69" height="30"/>
|
||||
<state key="normal" title="Download"/>
|
||||
<connections>
|
||||
<action selector="downloadTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="8Jg-1C-0Ms"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="rate: 1.0x" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yUQ-mI-ozK">
|
||||
<rect key="frame" x="153" y="419" width="69" height="21"/>
|
||||
<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="0:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j3w-gr-HzF">
|
||||
<rect key="frame" x="16" y="280" width="27" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Urj-Dv-41y">
|
||||
<rect key="frame" x="319" y="280" width="40" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="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="190" width="343" height="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="reverb: 300.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="y5i-MZ-Qat">
|
||||
<rect key="frame" x="136.5" y="352" width="102" height="21"/>
|
||||
<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" fixedFrame="YES" 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"/>
|
||||
<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"/>
|
||||
<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">
|
||||
<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>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pVf-cJ-9ca">
|
||||
<rect key="frame" x="164.5" y="123" width="46" height="30"/>
|
||||
<state key="normal" title="Queue"/>
|
||||
<connections>
|
||||
<action selector="queueTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="qRj-oT-AV1"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6d9-Bc-hIz">
|
||||
<rect key="frame" x="282" y="123" width="49" height="30"/>
|
||||
<state key="normal" title="Stream"/>
|
||||
<connections>
|
||||
<action selector="streamTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="AXY-N7-87Y"/>
|
||||
</connections>
|
||||
</button>
|
||||
</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="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="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"/>
|
||||
<constraint firstItem="joK-xi-MCo" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" constant="60" id="Ba7-nd-oCD"/>
|
||||
<constraint firstItem="pVf-cJ-9ca" firstAttribute="centerY" secondItem="KDu-ea-kF8" secondAttribute="centerY" id="Cma-VU-v2t"/>
|
||||
<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="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="43" id="SRU-sX-z5b"/>
|
||||
<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="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="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="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="pVf-cJ-9ca" firstAttribute="centerX" secondItem="joK-xi-MCo" secondAttribute="centerX" id="lOM-Fa-KdR"/>
|
||||
<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 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"/>
|
||||
<constraint firstItem="w2a-RA-zmI" firstAttribute="centerY" secondItem="lTK-Hd-Tl2" secondAttribute="centerY" constant="-1" id="zHt-h3-4ig"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<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"/>
|
||||
<outlet property="rateLabel" destination="yUQ-mI-ozK" id="Dx4-lO-A1B"/>
|
||||
<outlet property="rateSlider" destination="vfk-OJ-S3T" id="mNc-ET-aNM"/>
|
||||
<outlet property="reverbLabel" destination="y5i-MZ-Qat" id="8YR-mc-GFA"/>
|
||||
<outlet property="reverbSlider" destination="nsl-df-P21" id="BKt-Hb-akj"/>
|
||||
<outlet property="scrubberSlider" destination="w2a-RA-zmI" id="VbI-tT-lbc"/>
|
||||
<outlet property="skipBackwardButton" destination="tFH-sY-Xu9" id="LwM-2S-m6F"/>
|
||||
<outlet property="skipForwardButton" destination="0QE-3F-a4G" id="cQ7-b7-pW7"/>
|
||||
<outlet property="skipSilencesSwitch" destination="2cn-E5-TeQ" id="TRI-IT-YJT"/>
|
||||
<outlet property="sleepSwitch" destination="IGe-aU-Y6D" id="BZn-9C-hOk"/>
|
||||
<outlet property="streamButton" destination="6d9-Bc-hIz" id="DZe-ga-3RV"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="132" y="103.89805097451276"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Model.swift
|
||||
// SwiftAudioPlayer_Example
|
||||
//
|
||||
// Created by Tanha Kabir on 3/17/21.
|
||||
// Copyright © 2021 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudioPlayer
|
||||
|
||||
struct AudioInfo: Hashable {
|
||||
var index: Int = 0
|
||||
|
||||
var urls: [URL] = [URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!,
|
||||
URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!,
|
||||
URL(string: "https://ice6.somafm.com/groovesalad-256-mp3")!]
|
||||
|
||||
var url: URL {
|
||||
switch index {
|
||||
case 0:
|
||||
return urls[0]
|
||||
case 1:
|
||||
return urls[1]
|
||||
case 2:
|
||||
return urls[2]
|
||||
default:
|
||||
return urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch index {
|
||||
case 0:
|
||||
return "Soundbite"
|
||||
case 1:
|
||||
return "Podcast"
|
||||
case 2:
|
||||
return "Radio"
|
||||
default:
|
||||
return "Soundbite"
|
||||
}
|
||||
}
|
||||
|
||||
let artist: String = "SwiftAudioPlayer Sample App"
|
||||
let releaseDate: Int = 1550790640
|
||||
|
||||
var lockscreenInfo: SALockScreenInfo {
|
||||
get {
|
||||
return SALockScreenInfo(title: self.title, artist: self.artist, artwork: nil, releaseDate: self.releaseDate)
|
||||
}
|
||||
}
|
||||
|
||||
var savedUrl: URL? {
|
||||
get {
|
||||
return savedUrls[index]
|
||||
}
|
||||
}
|
||||
|
||||
var savedUrls: [URL?] = [nil, nil, nil]
|
||||
|
||||
mutating func addSavedUrl(_ url: URL) {
|
||||
savedUrls[index] = url
|
||||
}
|
||||
|
||||
mutating func deleteSavedUrl() {
|
||||
savedUrls[index] = nil
|
||||
}
|
||||
|
||||
mutating func addSavedUrl(_ url: URL, atIndex i: Int) {
|
||||
savedUrls[i] = url
|
||||
}
|
||||
|
||||
mutating func deleteSavedUrl(atIndex i: Int) {
|
||||
savedUrls[i] = nil
|
||||
}
|
||||
|
||||
func getUrl(atIndex i: Int) -> URL {
|
||||
return urls[i]
|
||||
}
|
||||
|
||||
mutating func setIndex(_ i: Int) {
|
||||
index = i
|
||||
}
|
||||
|
||||
func getIndex(forURL url: URL) -> Int? {
|
||||
return urls.firstIndex(of: url) ?? savedUrls.firstIndex(of: url)
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,385 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftAudioPlayer
|
||||
import AVFoundation
|
||||
|
||||
class ViewController: UIViewController {
|
||||
var selectedAudio: AudioInfo = AudioInfo(index: 0)
|
||||
|
||||
var freq:[Int] = [0,0,0,0,0,0,0,0,0,0]
|
||||
@IBOutlet weak var currentUrlLocationLabel: UILabel!
|
||||
@IBOutlet weak var bufferProgress: UIProgressView!
|
||||
@IBOutlet weak var scrubberSlider: UISlider!
|
||||
|
||||
@IBOutlet weak var playPauseButton: UIButton!
|
||||
@IBOutlet weak var skipBackwardButton: UIButton!
|
||||
@IBOutlet weak var skipForwardButton: UIButton!
|
||||
|
||||
@IBOutlet weak var audioSelector: UISegmentedControl!
|
||||
@IBOutlet weak var streamButton: UIButton!
|
||||
@IBOutlet weak var downloadButton: UIButton!
|
||||
@IBOutlet weak var rateSlider: UISlider!
|
||||
|
||||
@IBOutlet weak var rateLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var reverbLabel: UILabel!
|
||||
@IBOutlet weak var reverbSlider: UISlider!
|
||||
@IBOutlet weak var durationLabel: UILabel!
|
||||
@IBOutlet weak var currentTimestampLabel: UILabel!
|
||||
|
||||
var isDownloading: Bool = false
|
||||
var isStreaming: Bool = false
|
||||
var beingSeeked: Bool = false
|
||||
|
||||
|
||||
var downloadId: UInt?
|
||||
var durationId: UInt?
|
||||
var bufferId: UInt?
|
||||
var playingStatusId: UInt?
|
||||
var queueId: UInt?
|
||||
var elapsedId: UInt?
|
||||
|
||||
var duration: Double = 0.0
|
||||
var playbackStatus: SAPlayingStatus = .paused
|
||||
|
||||
var lastPlayedAudioIndex: Int?
|
||||
|
||||
var isPlayable: Bool = false {
|
||||
didSet {
|
||||
if isPlayable {
|
||||
playPauseButton.isEnabled = true
|
||||
skipBackwardButton.isEnabled = true
|
||||
skipForwardButton.isEnabled = true
|
||||
} else {
|
||||
playPauseButton.isEnabled = false
|
||||
skipBackwardButton.isEnabled = false
|
||||
skipForwardButton.isEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
|
||||
SAPlayer.Downloader.allowUsingCellularData = true
|
||||
|
||||
// SAPlayer.shared.DEBUG_MODE = true
|
||||
|
||||
isPlayable = false
|
||||
checkIfAudioDownloaded()
|
||||
selectAudio(atIndex: 0)
|
||||
|
||||
// addRandomModifiers()
|
||||
|
||||
subscribeToChanges()
|
||||
}
|
||||
|
||||
func addRandomModifiers() {
|
||||
let node = AVAudioUnitReverb()
|
||||
SAPlayer.shared.audioModifiers.append(node)
|
||||
node.wetDryMix = 300
|
||||
let frequency:[Int] = [60,170,310,600,1000,3000,6000,12000,14000,16000]
|
||||
let node2 = AVAudioUnitEQ(numberOfBands:frequency.count)
|
||||
node2.globalGain = 1
|
||||
for i in 0...(node2.bands.count-1) {
|
||||
node2.bands[i].frequency = Float(frequency[i])
|
||||
node2.bands[i].gain = 0
|
||||
node2.bands[i].bypass = false
|
||||
node2.bands[i].filterType = .parametric
|
||||
}
|
||||
SAPlayer.shared.audioModifiers.append(node2)
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
@IBAction func audioSelected(_ sender: Any) {
|
||||
let selected = audioSelector.selectedSegmentIndex
|
||||
|
||||
selectAudio(atIndex: selected)
|
||||
}
|
||||
|
||||
func selectAudio(atIndex i: Int) {
|
||||
selectedAudio.setIndex(i)
|
||||
|
||||
if selectedAudio.savedUrl != nil {
|
||||
downloadButton.setTitle("Delete downloaded", for: .normal)
|
||||
streamButton.isEnabled = false
|
||||
} else {
|
||||
downloadButton.setTitle("Download", for: .normal)
|
||||
streamButton.isEnabled = true
|
||||
}
|
||||
|
||||
if let savedUrl = selectedAudio.savedUrl {
|
||||
self.currentUrlLocationLabel.text = "saved url: \(savedUrl.absoluteString)"
|
||||
} else {
|
||||
self.currentUrlLocationLabel.text = "remote url: \(selectedAudio.url.absoluteString)"
|
||||
}
|
||||
|
||||
// if let savedUrl = savedUrls[selectedAudio] {}
|
||||
scrubberSlider.value = 0
|
||||
bufferProgress.progress = 0
|
||||
|
||||
// unsubscribeFromChanges()
|
||||
// subscribeToChanges()
|
||||
|
||||
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
|
||||
}
|
||||
|
||||
func checkIfAudioDownloaded() {
|
||||
for i in 0...2 {
|
||||
if let savedUrl = SAPlayer.Downloader.getSavedUrl(forRemoteUrl: selectedAudio.getUrl(atIndex: i)) {
|
||||
selectedAudio.addSavedUrl(savedUrl, atIndex: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func subscribeToChanges() {
|
||||
durationId = SAPlayer.Updates.Duration.subscribe { [weak self] (url, duration) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
|
||||
self.durationLabel.text = SAPlayer.prettifyTimestamp(duration)
|
||||
self.duration = duration
|
||||
}
|
||||
|
||||
elapsedId = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
|
||||
|
||||
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
|
||||
|
||||
guard self.duration != 0 else { return }
|
||||
|
||||
self.scrubberSlider.value = Float(position/self.duration)
|
||||
}
|
||||
|
||||
downloadId = SAPlayer.Updates.AudioDownloading.subscribe { [weak self] (url, progress) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.url else { return }
|
||||
|
||||
if self.isDownloading {
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.downloadButton.setTitle("Cancel \(String(format: "%.2f", (progress * 100)))%", for: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bufferId = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
|
||||
|
||||
if self.duration == 0.0 { return }
|
||||
|
||||
self.bufferProgress.progress = Float(buffer.bufferingProgress)
|
||||
|
||||
if buffer.bufferingProgress >= 0.99 {
|
||||
self.streamButton.isEnabled = false
|
||||
} else {
|
||||
self.streamButton.isEnabled = true
|
||||
}
|
||||
|
||||
self.isPlayable = buffer.isReadyForPlaying
|
||||
}
|
||||
|
||||
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (url, playing) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
|
||||
|
||||
self.playbackStatus = playing
|
||||
|
||||
switch playing {
|
||||
case .playing:
|
||||
self.isPlayable = true
|
||||
self.playPauseButton.setTitle("Pause", for: .normal)
|
||||
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
|
||||
case .ended:
|
||||
self.isPlayable = false
|
||||
self.playPauseButton.setTitle("Done", for: .normal)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
queueId = SAPlayer.Updates.AudioQueue.subscribe { [weak self] key, forthcomingPlaybackUrl in
|
||||
guard let self = self else { return }
|
||||
/// we update the selected audio. this is a little contrived, but allows us to update outlets
|
||||
if let indexFound = self.selectedAudio.getIndex(forURL: forthcomingPlaybackUrl) {
|
||||
self.selectAudio(atIndex: indexFound)
|
||||
}
|
||||
print("💥 Received queue update 💥")
|
||||
}
|
||||
}
|
||||
|
||||
func unsubscribeFromChanges() {
|
||||
guard let durationId = self.durationId,
|
||||
let elapsedId = self.elapsedId,
|
||||
let downloadId = self.downloadId,
|
||||
let queueId = self.queueId,
|
||||
let bufferId = self.bufferId,
|
||||
let playingStatusId = self.playingStatusId else { return }
|
||||
|
||||
SAPlayer.Updates.Duration.unsubscribe(durationId)
|
||||
SAPlayer.Updates.ElapsedTime.unsubscribe(elapsedId)
|
||||
SAPlayer.Updates.AudioDownloading.unsubscribe(downloadId)
|
||||
SAPlayer.Updates.AudioQueue.unsubscribe(queueId)
|
||||
SAPlayer.Updates.StreamingBuffer.unsubscribe(bufferId)
|
||||
SAPlayer.Updates.PlayingStatus.unsubscribe(playingStatusId)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func scrubberStartedSeeking(_ sender: UISlider) {
|
||||
beingSeeked = true
|
||||
}
|
||||
|
||||
@IBAction func scrubberSeeked(_ sender: Any) {
|
||||
let value = Double(scrubberSlider.value) * duration
|
||||
SAPlayer.shared.seekTo(seconds: value)
|
||||
beingSeeked = false
|
||||
}
|
||||
|
||||
|
||||
@IBAction func rateChanged(_ sender: Any) {
|
||||
let speed = rateSlider.value
|
||||
rateLabel.text = "rate: \(speed)x"
|
||||
|
||||
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
|
||||
reverbLabel.text = "reverb: \(reverb)"
|
||||
if let node = SAPlayer.shared.audioModifiers[1] as? AVAudioUnitReverb {
|
||||
node.wetDryMix = reverb
|
||||
}
|
||||
}
|
||||
@IBAction func queueTouched(_ sender: Any) {
|
||||
if let savedUrl = selectedAudio.savedUrl {
|
||||
SAPlayer.shared.queueSavedAudio(withSavedUrl: savedUrl)
|
||||
} else {
|
||||
SAPlayer.shared.queueRemoteAudio(withRemoteUrl: selectedAudio.url)
|
||||
}
|
||||
|
||||
print("queue: \(SAPlayer.shared.audioQueued)")
|
||||
}
|
||||
|
||||
@IBAction func downloadTouched(_ sender: Any) {
|
||||
if !isDownloading {
|
||||
if let savedUrl = SAPlayer.Downloader.getSavedUrl(forRemoteUrl: selectedAudio.url) {
|
||||
SAPlayer.Downloader.deleteDownloaded(withSavedUrl: savedUrl)
|
||||
selectedAudio.deleteSavedUrl()
|
||||
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, completion: { [weak self] url in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
|
||||
self.selectedAudio.addSavedUrl(url)
|
||||
|
||||
SAPlayer.shared.startSavedAudio(withSavedUrl: url, mediaInfo: self.selectedAudio.lockscreenInfo)
|
||||
self.lastPlayedAudioIndex = self.selectedAudio.index
|
||||
}
|
||||
})
|
||||
streamButton.isEnabled = false
|
||||
}
|
||||
} else {
|
||||
SAPlayer.Downloader.cancelDownload(withRemoteUrl: selectedAudio.url)
|
||||
downloadButton.setTitle("Download", for: .normal)
|
||||
streamButton.isEnabled = true
|
||||
isDownloading = false
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func streamTouched(_ sender: Any) {
|
||||
if !isStreaming {
|
||||
if selectedAudio.index == 2 { // radio
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, bitrate: .low, mediaInfo: selectedAudio.lockscreenInfo)
|
||||
} else {
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, mediaInfo: selectedAudio.lockscreenInfo)
|
||||
}
|
||||
|
||||
lastPlayedAudioIndex = selectedAudio.index
|
||||
streamButton.setTitle("Cancel streaming", for: .normal)
|
||||
downloadButton.isEnabled = false
|
||||
isStreaming = true
|
||||
} else {
|
||||
SAPlayer.shared.stopStreamingRemoteAudio()
|
||||
streamButton.setTitle("Stream", for: .normal)
|
||||
downloadButton.isEnabled = true
|
||||
isStreaming = false
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
}
|
||||
|
||||
@IBAction func skipBackwardTouched(_ sender: Any) {
|
||||
SAPlayer.shared.skipBackwards()
|
||||
}
|
||||
|
||||
@IBAction func skipForwardTouched(_ sender: Any) {
|
||||
SAPlayer.shared.skipForward()
|
||||
}
|
||||
@IBAction func setEqualizerValue(_ sender: Any) {
|
||||
if let slider = sender as? UISlider{
|
||||
print("slider of index:", slider.tag, "is changed to", slider.value)
|
||||
freq[slider.tag] = Int(slider.value)
|
||||
print("current frequency : ",freq)
|
||||
if let node = SAPlayer.shared.audioModifiers[2] as? AVAudioUnitEQ{
|
||||
for i in 0...(node.bands.count - 1){
|
||||
node.bands[i].gain = Float(freq[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@IBOutlet weak var skipSilencesSwitch: UISwitch!
|
||||
|
||||
@IBAction func skipSilencesSwitched(_ sender: Any) {
|
||||
if skipSilencesSwitch.isOn {
|
||||
_ = SAPlayer.Features.SkipSilences.enable()
|
||||
} else {
|
||||
_ = SAPlayer.Features.SkipSilences.disable()
|
||||
}
|
||||
}
|
||||
@IBOutlet weak var sleepSwitch: UISwitch!
|
||||
|
||||
@IBAction func sleepSwitched(_ sender: Any) {
|
||||
if sleepSwitch.isOn {
|
||||
_ = SAPlayer.Features.SleepTimer.enable(afterDelay: 5.0)
|
||||
} else {
|
||||
_ = SAPlayer.Features.SleepTimer.disable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2019 tanhakabir <tanhakabir.ca@gmail.com>
|
||||
Copyright (c) 2019 Tanha Kabir <tanhakabir.ca@gmail.com>, Jon Mercer <mercer.jon@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
# SwiftAudioPlayer
|
||||
|
||||
[](https://travis-ci.org/tanhakabir/SwiftAudioPlayer)
|
||||
[](https://cocoapods.org/pods/SwiftAudioPlayer)
|
||||
[](https://cocoapods.org/pods/SwiftAudioPlayer)
|
||||
[](https://cocoapods.org/pods/SwiftAudioPlayer)
|
||||
|
||||
## Example
|
||||
Swift-based audio player with AVAudioEngine as its base. Allows for: streaming online audio, playing local file, changing audio speed (3.5X, 4X, 32X), pitch, and real-time audio manipulation using custom [audio enhancements](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
|
||||
|
||||
To run the example project, clone the repo, and run `pod install` from the Example directory first.
|
||||
This player was built for [podcasting](https://chameleonpodcast.com/). We originally used AVPlayer for playing audio but we wanted to manipulate audio that was being streamed. We set up AVAudioEngine at first just to play a file saved on the phone and it worked great, but AVAudioEngine on its own doesn't support streaming audio as easily as AVPlayer.
|
||||
|
||||
## Requirements
|
||||
Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbox), we are able to stream audio and convert the downloaded data into usable data for the AVAudioEngine to play. For an overview of our solution check out our [blog post](https://medium.com/chameleon-podcast/creating-an-advanced-streaming-audio-engine-for-ios-9fbc7aef4115).
|
||||
|
||||
## Installation
|
||||
### Basic Features
|
||||
|
||||
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
|
||||
1. Uses only 1-2% CPU for optimal performance for the rest of your app
|
||||
1. You're able to install taps and any other AVAudioEngine features to do cool things like skipping silences
|
||||
|
||||
### Special Features
|
||||
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
|
||||
|
||||
### Requirements
|
||||
|
||||
iOS 10.0 and higher.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Running the Example Project
|
||||
|
||||
1. Clone repo
|
||||
2. CD to the `Example` folder where the Example app lives
|
||||
3. Run `pod install` in terminal
|
||||
4. Build and run
|
||||
|
||||
### Installation
|
||||
|
||||
SwiftAudioPlayer is available through [CocoaPods](https://cocoapods.org). To install
|
||||
it, simply add the following line to your Podfile:
|
||||
@@ -20,10 +48,288 @@ it, simply add the following line to your Podfile:
|
||||
pod 'SwiftAudioPlayer'
|
||||
```
|
||||
|
||||
## Author
|
||||
### Usage
|
||||
|
||||
tanhakabir, tanhakabir.ca@gmail.com
|
||||
Import the player at the top:
|
||||
```swift
|
||||
import SwiftAudioPlayer
|
||||
```
|
||||
|
||||
## License
|
||||
**Important:** For app in background downloading please refer to [note](#important-step-for-background-downloads).
|
||||
|
||||
To play remote audio:
|
||||
```swift
|
||||
let url = URL(string: "https://randomwebsite.com/audio.mp3")!
|
||||
SAPlayer.shared.startRemoteAudio(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 (for buffer progress %):
|
||||
```swift
|
||||
@IBOutlet weak var bufferProgress: UIProgressView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudioUrl else { return }
|
||||
|
||||
let progress = Float((buffer.totalDurationBuffered + buffer.startingBufferTimePositon) / self.duration)
|
||||
|
||||
self.bufferProgress.progress = progress
|
||||
|
||||
self.isPlayable = buffer.isReadyForPlaying
|
||||
}
|
||||
}
|
||||
```
|
||||
Look at the [Updates](#saplayerupdates) section to see usage details and other updates to follow.
|
||||
|
||||
|
||||
For realtime audio manipulations, [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes are used. For example to adjust the reverb through a slider in the UI:
|
||||
```swift
|
||||
@IBOutlet weak var reverbSlider: UISlider!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let node = AVAudioUnitReverb()
|
||||
SAPlayer.shared.audioModifiers.append(node)
|
||||
node.wetDryMix = 300
|
||||
}
|
||||
|
||||
@IBAction func reverbSliderChanged(_ sender: Any) {
|
||||
if let node = SAPlayer.shared.audioModifiers[1] as? AVAudioUnitReverb {
|
||||
node.wetDryMix = reverbSlider.value
|
||||
}
|
||||
}
|
||||
```
|
||||
For a more detailed explanation on usage, look at the [Realtime Audio Manipulations](#realtime-audio-manipulation) section.
|
||||
|
||||
For more details and specifics look at the [API documentation](#api-in-detail) below.
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
### Issues
|
||||
|
||||
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
|
||||
|
||||
### License
|
||||
|
||||
SwiftAudioPlayer is available under the MIT license. See the LICENSE file for more info.
|
||||
|
||||
---
|
||||
|
||||
# API in detail
|
||||
|
||||
## SAPlayer
|
||||
|
||||
Access the player and all of its fields and functions through `SAPlayer.shared`.
|
||||
|
||||
### Supported file types
|
||||
|
||||
Known supported file types are `.mp3` and `.wav`.
|
||||
|
||||
### Playing Audio (Basic Commands)
|
||||
|
||||
To set up player with audio to play, use either:
|
||||
* `startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
|
||||
* `startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
|
||||
|
||||
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen. For streamed audio you can optionally set the bitrate to be `.high` or `.low`. High is more performant but won't work well for radio streams; for radio streams you should use low. The default bitrate if you don't set it is `.high`.
|
||||
|
||||
For streaming remote audio, subscribe to `SAPlayer.Updates.StreamingBuffer` for updates on streaming progress.
|
||||
|
||||
Basic controls available:
|
||||
```swift
|
||||
play()
|
||||
pause()
|
||||
togglePlayAndPause()
|
||||
seekTo(seconds: Double)
|
||||
skipForward()
|
||||
skipBackwards()
|
||||
```
|
||||
|
||||
### Queuing Audio for Autoplay
|
||||
|
||||
You can queue either remote or locally saved audio to be played automatically next.
|
||||
|
||||
To queue:
|
||||
```swift
|
||||
SAPlayer.shared.queueSavedAudio(withSavedUrl: C://random_folder/audio.mp3) // or
|
||||
SAPlayer.shared.queueRemoteAudio(withRemoteUrl: https://randomwebsite.com/audio.mp3)
|
||||
```
|
||||
|
||||
#### Important
|
||||
|
||||
The engine can handle audio manipulations like speed, pitch, effects, etc. To do this, nodes for effects must be finalized before initialize is called. Look at [audio manipulation documentation](#realtime-audio-manipulation) for more information.
|
||||
|
||||
### Lockscreen Media Player
|
||||
|
||||
Update and set what displays on the lockscreen's media player when the player is active.
|
||||
|
||||
`skipForwardSeconds` and `skipBackwardSeconds` for the intervals to skip forward and back with.
|
||||
|
||||
`mediaInfo` for the audio's information to display on the lockscreen. Is of type `SALockScreenInfo` which contains:
|
||||
```swift
|
||||
title: String
|
||||
artist: String
|
||||
artwork: UIImage?
|
||||
releaseDate: UTC // Int
|
||||
```
|
||||
|
||||
`playbackRateOfAudioChanged(rate: Float)` is used to update the lockscreen media player that the playback rate has changed.
|
||||
|
||||
## 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
|
||||
|
||||
All downloads will be paused when audio is streamed from a URL. They will automatically resume 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.
|
||||
|
||||
Subscribe to `SAPlayer.Updates.AudioDownloading` for downloading progress updates.
|
||||
|
||||
And use the following to stop any active or prevent future downloads of the corresponding remote URL:
|
||||
|
||||
```swift
|
||||
func cancelDownload(withRemoteUrl url: URL)
|
||||
```
|
||||
|
||||
By default downloading will be allowed on cellular data. If you would like to turn this off set:
|
||||
```swift
|
||||
SAPlayer.Downloader.allowUsingCellularData = false
|
||||
```
|
||||
You can also retrieve what preference you have set for cellular downloads through `allowUsingCellularData`.
|
||||
|
||||
### 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)
|
||||
```
|
||||
|
||||
**NOTE:** You're in charge or clearing downloads when your don't need them anymore
|
||||
|
||||
## 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. The engine makes a best effort guess as to the duration of the audio. The guess gets better with more bytes streamed from the web.
|
||||
|
||||
### PlayingStatus
|
||||
Payload = `SAPlayingStatus`
|
||||
|
||||
Changes in the playing status of the player. Can be one of the following 4: `playing`, `paused`, `buffering`, `ended` (audio ended).
|
||||
|
||||
### 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.
|
||||
|
||||
## Audio Effects
|
||||
|
||||
### Realtime Audio Manipulation
|
||||
|
||||
All audio effects on the player is done through [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes. These include adding reverb, changing pitch and playback rate, and adding distortion. Full list of effects available [here](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
|
||||
|
||||
The effects intended to use are stored in `audioModifiers` as a list of nodes. These nodes are in the order that the engine will attach them to one another.
|
||||
|
||||
**Note:** By default `SAPlayer` starts off with one node, an [AVAudioUnitTimePitch](https://developer.apple.com/documentation/avfoundation/avaudiounittimepitch) node, that is set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
|
||||
|
||||
#### Important
|
||||
All the nodes intended to be used on the playing audio must be finalized before calling `initializeSavedAudio(...)` or `initializeRemoteAudio(...)`. Any changes to list of nodes after initialize is called for a given audio file will not be reflected in playback.
|
||||
|
||||
Once all nodes are added to `audioModifiers` and the player has been initialized, any manipulations done with the nodes are performed in realtime. The example app shows manipulating the playback rate in realtime:
|
||||
|
||||
```swift
|
||||
let speed = rateSlider.value
|
||||
if let node = SAPlayer.shared.audioModifiers[0] as? AVAudioUnitTimePitch {
|
||||
node.rate = speed
|
||||
SAPlayer.shared.playbackRateOfAudioChanged(rate: speed)
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** if the rate of the audio is changed, `playbackRateOfAudioChanged` should also be called to update the lockscreen's media player.
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// AudioClockDirector.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
import CoreMedia
|
||||
|
||||
class AudioClockDirector {
|
||||
static let shared = AudioClockDirector()
|
||||
|
||||
private var needleClosures: DirectorThreadSafeClosures<Needle> = DirectorThreadSafeClosures()
|
||||
private var durationClosures: DirectorThreadSafeClosures<Duration> = DirectorThreadSafeClosures()
|
||||
private var playingStatusClosures: DirectorThreadSafeClosures<SAPlayingStatus> = DirectorThreadSafeClosures()
|
||||
private var bufferClosures: DirectorThreadSafeClosures<SAAudioAvailabilityRange> = DirectorThreadSafeClosures()
|
||||
|
||||
private init() {}
|
||||
|
||||
func create() {}
|
||||
|
||||
func clear() {
|
||||
needleClosures.clear()
|
||||
durationClosures.clear()
|
||||
playingStatusClosures.clear()
|
||||
bufferClosures.clear()
|
||||
}
|
||||
|
||||
// MARK: - Attaches
|
||||
|
||||
// Needle
|
||||
func attachToChangesInNeedle(closure: @escaping (Key, Needle) throws -> Void) -> UInt {
|
||||
return needleClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
|
||||
// Duration
|
||||
func attachToChangesInDuration(closure: @escaping (Key, Duration) throws -> Void) -> UInt {
|
||||
return durationClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
|
||||
// Playing status
|
||||
func attachToChangesInPlayingStatus(closure: @escaping (Key, SAPlayingStatus) throws -> Void) -> UInt{
|
||||
return playingStatusClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
|
||||
// Buffer
|
||||
func attachToChangesInBufferedRange(closure: @escaping (Key, SAAudioAvailabilityRange) throws -> Void) -> UInt{
|
||||
return bufferClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Detaches
|
||||
func detachFromChangesInNeedle(withID id: UInt) {
|
||||
needleClosures.detach(id: id)
|
||||
}
|
||||
|
||||
func detachFromChangesInDuration(withID id: UInt) {
|
||||
durationClosures.detach(id: id)
|
||||
}
|
||||
|
||||
func detachFromChangesInPlayingStatus(withID id: UInt) {
|
||||
playingStatusClosures.detach(id: id)
|
||||
}
|
||||
|
||||
func detachFromChangesInBufferedRange(withID id: UInt) {
|
||||
bufferClosures.detach(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Receives notifications from AudioEngine on ticks
|
||||
extension AudioClockDirector {
|
||||
func needleTick(_ key: Key, needle: Needle) {
|
||||
needleClosures.broadcast(key: key, payload: needle)
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioClockDirector {
|
||||
func durationWasChanged(_ key: Key, duration: Duration) {
|
||||
durationClosures.broadcast(key: key, payload: duration)
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioClockDirector {
|
||||
func audioPlayingStatusWasChanged(_ key: Key, status: SAPlayingStatus) {
|
||||
playingStatusClosures.broadcast(key: key, payload: status)
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioClockDirector {
|
||||
func changeInAudioBuffered(_ key: Key, buffered: SAAudioAvailabilityRange) {
|
||||
bufferClosures.broadcast(key: key, payload: buffered)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// AudioQueueDirector.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Joe Williams on 3/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AudioQueueDirector {
|
||||
static let shared = AudioQueueDirector()
|
||||
var closures: DirectorThreadSafeClosures<URL> = DirectorThreadSafeClosures()
|
||||
private init() {}
|
||||
|
||||
func create() {}
|
||||
|
||||
func clear() {
|
||||
closures.clear()
|
||||
}
|
||||
|
||||
func attach(closure: @escaping (Key, URL) throws -> Void) -> UInt {
|
||||
return closures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func detach(withID id: UInt) {
|
||||
closures.detach(id: id)
|
||||
}
|
||||
|
||||
func changeInQueue(_ key: Key, url: URL) {
|
||||
closures.broadcast(key: key, payload: url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// DownloadProgressDirector.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-02-17.
|
||||
// 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
|
||||
|
||||
class DownloadProgressDirector {
|
||||
static let shared = DownloadProgressDirector()
|
||||
|
||||
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
|
||||
|
||||
private init() {
|
||||
AudioDataManager.shared.attach { [weak self] (key, progress) in
|
||||
self?.closures.broadcast(key: key, payload: progress)
|
||||
}
|
||||
}
|
||||
|
||||
func create() {}
|
||||
|
||||
func clear() {
|
||||
closures.clear()
|
||||
}
|
||||
|
||||
func attach(closure: @escaping (Key, Double) throws -> Void) -> UInt {
|
||||
return closures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func detach(withID id: UInt) {
|
||||
closures.detach(id: id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// StreamingDownloadDirector.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 4/16/21.
|
||||
//
|
||||
// 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
|
||||
|
||||
class StreamingDownloadDirector {
|
||||
static let shared = StreamingDownloadDirector()
|
||||
|
||||
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
|
||||
|
||||
private init() {}
|
||||
|
||||
func create() {}
|
||||
|
||||
func clear() {
|
||||
closures.clear()
|
||||
}
|
||||
|
||||
func attach(closure: @escaping (Key, Double) throws -> Void) -> UInt {
|
||||
return closures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func detach(withID id: UInt) {
|
||||
closures.detach(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension StreamingDownloadDirector {
|
||||
func didUpdate(_ key: Key, networkStreamProgress: Double) {
|
||||
closures.broadcast(key: key, payload: networkStreamProgress)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// AudioDiskEngine.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
class AudioDiskEngine: AudioEngine {
|
||||
var audioFormat: AVAudioFormat?
|
||||
var audioSampleRate: Float = 0
|
||||
var audioLengthSamples: AVAudioFramePosition = 0
|
||||
var seekFrame: AVAudioFramePosition = 0
|
||||
var currentPosition: AVAudioFramePosition = 0
|
||||
|
||||
var audioFile: AVAudioFile?
|
||||
|
||||
var currentFrame: AVAudioFramePosition {
|
||||
guard let lastRenderTime = playerNode.lastRenderTime,
|
||||
let playerTime = playerNode.playerTime(forNodeTime: lastRenderTime) else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return playerTime.sampleTime
|
||||
}
|
||||
|
||||
var audioLengthSeconds: Float = 0
|
||||
|
||||
init(withSavedUrl url: AudioURL, delegate:AudioEngineDelegate?) {
|
||||
Log.info(url.key)
|
||||
|
||||
do {
|
||||
audioFile = try AVAudioFile(forReading: url)
|
||||
} catch {
|
||||
Log.monitor(error.localizedDescription)
|
||||
}
|
||||
|
||||
super.init(url: url, delegate: delegate, engineAudioFormat: audioFile?.processingFormat ?? AudioEngine.defaultEngineAudioFormat)
|
||||
|
||||
if let file = audioFile {
|
||||
Log.debug("Audio file exists")
|
||||
audioLengthSamples = file.length
|
||||
audioFormat = file.processingFormat
|
||||
audioSampleRate = Float(audioFormat?.sampleRate ?? 44100)
|
||||
audioLengthSeconds = Float(audioLengthSamples) / audioSampleRate
|
||||
duration = Duration(audioLengthSeconds)
|
||||
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: 0, durationLoadedByNetwork: duration, predictedDurationToLoad: duration, isPlayable: true)
|
||||
} else {
|
||||
Log.monitor("Could not load downloaded file with url: \(url)")
|
||||
}
|
||||
|
||||
doRepeatedly(timeInterval: 0.2) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard self.playingStatus != .ended else { return }
|
||||
|
||||
self.updateIsPlaying()
|
||||
self.updateNeedle()
|
||||
}
|
||||
|
||||
scheduleAudioFile()
|
||||
}
|
||||
|
||||
private func scheduleAudioFile() {
|
||||
guard let audioFile = audioFile else { return }
|
||||
|
||||
playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
|
||||
}
|
||||
|
||||
private func updateNeedle() {
|
||||
guard engine.isRunning else { return }
|
||||
|
||||
currentPosition = currentFrame + seekFrame
|
||||
currentPosition = max(currentPosition, 0)
|
||||
currentPosition = min(currentPosition, audioLengthSamples)
|
||||
|
||||
if currentPosition >= audioLengthSamples {
|
||||
playerNode.stop()
|
||||
if state == .resumed {
|
||||
state = .suspended
|
||||
}
|
||||
playingStatus = .ended
|
||||
}
|
||||
|
||||
guard audioSampleRate != 0 else {
|
||||
Log.error("Missing audio sample rate in update needle timer function!")
|
||||
return
|
||||
}
|
||||
|
||||
needle = Double(Float(currentPosition)/audioSampleRate)
|
||||
}
|
||||
|
||||
override func seek(toNeedle needle: Needle) {
|
||||
guard let audioFile = audioFile else {
|
||||
Log.error("did not have audio file when trying to seek")
|
||||
return
|
||||
}
|
||||
|
||||
let playing = playerNode.isPlaying
|
||||
|
||||
self.needle = needle // to tick while paused
|
||||
|
||||
seekFrame = AVAudioFramePosition(Float(needle) * audioSampleRate)
|
||||
seekFrame = max(seekFrame, 0)
|
||||
seekFrame = min(seekFrame, audioLengthSamples)
|
||||
currentPosition = seekFrame
|
||||
|
||||
playerNode.stop()
|
||||
|
||||
if currentPosition < audioLengthSamples {
|
||||
playerNode.scheduleSegment(audioFile, startingFrame: seekFrame, frameCount: AVAudioFrameCount(audioLengthSamples - seekFrame), at: nil, completionHandler: nil)
|
||||
|
||||
if playing {
|
||||
playerNode.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func invalidate() {
|
||||
super.invalidate()
|
||||
//Nothing to invalidate for disk
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// AudioEngine.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
protocol AudioEngineProtocol {
|
||||
var key: Key { get }
|
||||
var engine: AVAudioEngine! { get }
|
||||
func play()
|
||||
func pause()
|
||||
func seek(toNeedle needle: Needle)
|
||||
func invalidate()
|
||||
}
|
||||
|
||||
protocol AudioEngineDelegate: AnyObject {
|
||||
func didError()
|
||||
}
|
||||
|
||||
class AudioEngine: AudioEngineProtocol {
|
||||
weak var delegate:AudioEngineDelegate?
|
||||
var key:Key
|
||||
|
||||
var engine: AVAudioEngine!
|
||||
var playerNode: AVAudioPlayerNode!
|
||||
|
||||
static let defaultEngineAudioFormat: AVAudioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
|
||||
|
||||
var state:TimerState = .suspended
|
||||
enum TimerState {
|
||||
case suspended
|
||||
case resumed
|
||||
}
|
||||
|
||||
var needle: Needle = -1 {
|
||||
didSet {
|
||||
if needle >= 0 && oldValue != needle {
|
||||
AudioClockDirector.shared.needleTick(key, needle: needle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var duration: Duration = -1 {
|
||||
didSet {
|
||||
if duration >= 0 && oldValue != duration {
|
||||
AudioClockDirector.shared.durationWasChanged(key, duration: duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var playingStatus: SAPlayingStatus? = nil {
|
||||
didSet {
|
||||
guard playingStatus != oldValue, let status = playingStatus else {
|
||||
return
|
||||
}
|
||||
|
||||
AudioClockDirector.shared.audioPlayingStatusWasChanged(key, status: status)
|
||||
}
|
||||
}
|
||||
|
||||
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false)
|
||||
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false) {
|
||||
didSet {
|
||||
if bufferedSeconds.startingNeedle == 0.0 && bufferedSeconds.durationLoadedByNetwork == 0.0 {
|
||||
bufferedSecondsDebouncer = bufferedSeconds
|
||||
AudioClockDirector.shared.changeInAudioBuffered(key, buffered: bufferedSeconds)
|
||||
return
|
||||
}
|
||||
|
||||
if bufferedSeconds.startingNeedle == oldValue.startingNeedle && bufferedSeconds.durationLoadedByNetwork == oldValue.durationLoadedByNetwork {
|
||||
return
|
||||
}
|
||||
|
||||
if bufferedSeconds.durationLoadedByNetwork - DEBOUNCING_BUFFER_TIME < bufferedSecondsDebouncer.durationLoadedByNetwork {
|
||||
Log.debug("skipping pushing buffer: \(bufferedSeconds)")
|
||||
return
|
||||
}
|
||||
|
||||
bufferedSecondsDebouncer = bufferedSeconds
|
||||
AudioClockDirector.shared.changeInAudioBuffered(key, buffered: bufferedSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
init(url: AudioURL, delegate:AudioEngineDelegate?, engineAudioFormat: AVAudioFormat) {
|
||||
self.key = url.key
|
||||
self.delegate = delegate
|
||||
|
||||
engine = AVAudioEngine()
|
||||
playerNode = AVAudioPlayerNode()
|
||||
|
||||
initHelper(engineAudioFormat)
|
||||
}
|
||||
|
||||
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 {
|
||||
engine.connect(playerNode, to: engine.mainMixerNode, format: engineAudioFormat)
|
||||
}
|
||||
|
||||
engine.prepare()
|
||||
}
|
||||
|
||||
deinit {
|
||||
if state == .resumed {
|
||||
engine.stop()
|
||||
}
|
||||
|
||||
engine.disconnectNodeInput(self.playerNode)
|
||||
engine.detach(self.playerNode)
|
||||
|
||||
engine = nil
|
||||
playerNode = nil
|
||||
Log.info("deinit AVAudioEngine for \(key)")
|
||||
}
|
||||
|
||||
func doRepeatedly(timeInterval: Double, _ closure: @escaping () -> ()) {
|
||||
// A common error in AVAudioEngine is 'required condition is false: nil == owningEngine || GetEngine() == owningEngine'
|
||||
// where there can only be one instance of engine running at a time and if there is already one when trying to start
|
||||
// a new one then this error will be thrown.
|
||||
|
||||
// To handle this error we need to make sure we properly dispose of the engine when done using. In the case of timers, a
|
||||
// repeating timer will maintain a strong reference to the body even if you state that you wanted a weak reference to self
|
||||
// to mitigate this for repeating timers, you can either call timer.invalidate() properly or don't use repeat block timers.
|
||||
// To be in better control of references and to mitigate any unforeseen issues, I decided to implement a recurisive version
|
||||
// of the repeat block timer so I'm in full control of when to invalidate.
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] (timer: Timer) in
|
||||
guard let self = self else { return }
|
||||
guard self.playingStatus != .ended else {
|
||||
self.delegate = nil
|
||||
return
|
||||
}
|
||||
closure()
|
||||
self.doRepeatedly(timeInterval: timeInterval, closure)
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsPlaying() {
|
||||
if !bufferedSeconds.isPlayable {
|
||||
if bufferedSeconds.reachedEndOfAudio(needle: needle) {
|
||||
playingStatus = .ended
|
||||
} else {
|
||||
playingStatus = .buffering
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let isPlaying = engine.isRunning && playerNode.isPlaying
|
||||
playingStatus = isPlaying ? .playing : .paused
|
||||
|
||||
// playingStatus = .paused
|
||||
}
|
||||
|
||||
func play() {
|
||||
// https://stackoverflow.com/questions/36754934/update-mpremotecommandcenter-play-pause-button
|
||||
if !(engine.isRunning) {
|
||||
do {
|
||||
try engine.start()
|
||||
|
||||
} catch let error {
|
||||
Log.monitor(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
playerNode.play()
|
||||
|
||||
if state == .suspended {
|
||||
state = .resumed
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
// https://stackoverflow.com/questions/36754934/update-mpremotecommandcenter-play-pause-button
|
||||
playerNode.pause()
|
||||
engine.pause()
|
||||
|
||||
if state == .resumed {
|
||||
state = .suspended
|
||||
}
|
||||
}
|
||||
|
||||
func seek(toNeedle needle: Needle) {
|
||||
fatalError("No implementation for seek inAudioEngine, should be using streaming or disk type")
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
//
|
||||
// AudioStreamEngine.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
/**
|
||||
Start of the streaming chain. Get PCM buffer from lower chain and feed it to
|
||||
engine
|
||||
|
||||
Main responsibilities:
|
||||
POLL FOR BUFFER. When we start a stream it takes time for the lower chain to
|
||||
receive audio format. We don't know how long this would take. Therefore we poll
|
||||
continually. We also poll continually when user seeks because they could have
|
||||
seeked beyond pcm buffer, and down-chain buffer. We keep polling until we fill
|
||||
N buffers. If we stick to one buffer the audio sounds choppy because sometimes
|
||||
the parser takes longer than usual to parse a buffer
|
||||
|
||||
RECURSE FOR BUFFER. When we receive N buffers we switch to recursive mode. This
|
||||
means we only ask for the next buffer when one of the loaded buffers are
|
||||
used up. This is to prevent high CPU usage (100%) because otherwise we keep
|
||||
polling and parser keeps parsing even though the user is nowhere near that
|
||||
part of audio
|
||||
|
||||
UPDATES FOR UI. Duration, needle ticking, playing status, etc.
|
||||
|
||||
HANDLE PLAYING. Ensure the engine is in the correct state when playing,
|
||||
pausing, or seeking
|
||||
*/
|
||||
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 var PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
|
||||
|
||||
private let queue = DispatchQueue(label: "SwiftAudioPlayer.StreamEngine", qos: .userInitiated)
|
||||
|
||||
//From init
|
||||
private var converter: AudioConvertable!
|
||||
|
||||
//Fields
|
||||
private var currentTimeOffset: TimeInterval = 0
|
||||
private var streamChangeListenerId: UInt?
|
||||
|
||||
private var numberOfBuffersScheduledInTotal = 0 {
|
||||
didSet {
|
||||
Log.debug("number of buffers scheduled in total: \(numberOfBuffersScheduledInTotal)")
|
||||
if numberOfBuffersScheduledInTotal == 0 {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var wasPlaying = false
|
||||
private var numberOfBuffersScheduledFromPoll = 0 {
|
||||
didSet {
|
||||
if numberOfBuffersScheduledFromPoll > MAX_POLL_BUFFER_COUNT {
|
||||
shouldPollForNextBuffer = false
|
||||
}
|
||||
|
||||
if numberOfBuffersScheduledFromPoll > MIN_BUFFERS_TO_BE_PLAYABLE {
|
||||
if wasPlaying {
|
||||
play()
|
||||
wasPlaying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldPollForNextBuffer = true {
|
||||
didSet {
|
||||
if shouldPollForNextBuffer {
|
||||
numberOfBuffersScheduledFromPoll = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Prediction keeps fluctuating. We debounce to keep the UI from jitter
|
||||
private var predictedStreamDurationDebounceHelper: Duration = 0
|
||||
private var predictedStreamDuration: Duration = 0 {
|
||||
didSet {
|
||||
let d = predictedStreamDuration
|
||||
let s = predictedStreamDurationDebounceHelper
|
||||
if d/DEBOUNCING_BUFFER_TIME != s/DEBOUNCING_BUFFER_TIME {
|
||||
predictedStreamDurationDebounceHelper = predictedStreamDuration
|
||||
duration = predictedStreamDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var seekNeedleCommandBeforeEngineWasReady: Needle?
|
||||
private var isPlayable = false {
|
||||
didSet {
|
||||
if isPlayable != oldValue {
|
||||
Log.info("isPlayable status changed: \(isPlayable)")
|
||||
}
|
||||
|
||||
if isPlayable, let needle = seekNeedleCommandBeforeEngineWasReady {
|
||||
seekNeedleCommandBeforeEngineWasReady = nil
|
||||
seek(toNeedle: needle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(withRemoteUrl url: AudioURL, delegate:AudioEngineDelegate?, bitrate: SAPlayerBitrate) {
|
||||
Log.info(url)
|
||||
super.init(url: url, delegate: delegate, engineAudioFormat: AudioEngine.defaultEngineAudioFormat)
|
||||
|
||||
switch bitrate {
|
||||
case .high:
|
||||
PCM_BUFFER_SIZE = 8192
|
||||
case .low:
|
||||
PCM_BUFFER_SIZE = 4096
|
||||
}
|
||||
|
||||
do {
|
||||
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat, withPCMBufferSize: PCM_BUFFER_SIZE)
|
||||
} catch {
|
||||
delegate?.didError()
|
||||
}
|
||||
|
||||
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (key, progress) in
|
||||
guard let self = self else { return }
|
||||
guard key == url.key else { return }
|
||||
|
||||
// polling for buffers when we receive data. This won't be throttled on fresh new audio or seeked audio but in all other cases it most likely will be throttled
|
||||
self.pollForNextBuffer() // no buffer updates because thread issues if I try to update buffer status in streaming listener
|
||||
}
|
||||
|
||||
|
||||
let timeInterval = 1 / (converter.engineAudioFormat.sampleRate / Double(PCM_BUFFER_SIZE))
|
||||
|
||||
doRepeatedly(timeInterval: timeInterval) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard self.playingStatus != .ended else { return }
|
||||
|
||||
self.repeatedUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let id = streamChangeListenerId {
|
||||
StreamingDownloadDirector.shared.detach(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
private func repeatedUpdates() {
|
||||
self.pollForNextBuffer()
|
||||
self.updateNetworkBufferRange() // thread issues if I try to update buffer status in streaming listener
|
||||
self.updateNeedle()
|
||||
self.updateIsPlaying()
|
||||
self.updateDuration()
|
||||
}
|
||||
|
||||
//MARK:- Timer loop
|
||||
|
||||
//Called when
|
||||
//1. First time audio is finally parsed
|
||||
//2. When we run to the end of the network buffer and we're waiting again
|
||||
private func pollForNextBuffer() {
|
||||
guard shouldPollForNextBuffer else { return }
|
||||
|
||||
pollForNextBufferRecursive()
|
||||
}
|
||||
|
||||
private func pollForNextBufferRecursive() {
|
||||
do {
|
||||
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer()
|
||||
numberOfBuffersScheduledFromPoll += 1
|
||||
numberOfBuffersScheduledInTotal += 1
|
||||
|
||||
Log.debug("processed buffer for engine of frame length \(nextScheduledBuffer.frameLength)")
|
||||
queue.async { [weak self] in
|
||||
if #available(iOS 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
|
||||
self?.numberOfBuffersScheduledInTotal -= 1
|
||||
self?.pollForNextBufferRecursive()
|
||||
})
|
||||
} else {
|
||||
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
|
||||
nextScheduledBuffer = nil
|
||||
self?.numberOfBuffersScheduledInTotal -= 1
|
||||
self?.pollForNextBufferRecursive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: re-do how to pass and log these errors
|
||||
} catch ConverterError.reachedEndOfFile {
|
||||
Log.info(ConverterError.reachedEndOfFile.localizedDescription)
|
||||
} catch ConverterError.notEnoughData {
|
||||
Log.debug(ConverterError.notEnoughData.localizedDescription)
|
||||
} catch ConverterError.superConcerningShouldNeverHappen {
|
||||
Log.error(ConverterError.superConcerningShouldNeverHappen.localizedDescription)
|
||||
} catch {
|
||||
Log.debug(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNetworkBufferRange() { //for ui
|
||||
let range = converter.pollNetworkAudioAvailabilityRange()
|
||||
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, predictedDurationToLoad: predictedStreamDuration, isPlayable: isPlayable)
|
||||
}
|
||||
|
||||
private func updateNeedle() {
|
||||
guard engine.isRunning else { return }
|
||||
|
||||
guard let nodeTime = playerNode.lastRenderTime,
|
||||
let playerTime = playerNode.playerTime(forNodeTime: nodeTime) else {
|
||||
return
|
||||
}
|
||||
|
||||
//NOTE: playerTime can sometimes be < 0 when seeking. Reason pasted below
|
||||
//"The usual AVAudioNode sample times (as observed by lastRenderTime ) have an arbitrary zero point.
|
||||
//AVAudioPlayerNode superimposes a second “player timeline” on top of this, to reflect when the
|
||||
//player was started, and intervals during which it was paused."
|
||||
var currentTime = TimeInterval(playerTime.sampleTime) / playerTime.sampleRate
|
||||
currentTime = currentTime > 0 ? currentTime : 0
|
||||
|
||||
needle = (currentTime + currentTimeOffset)
|
||||
}
|
||||
|
||||
private func updateDuration() {
|
||||
if let d = converter.pollPredictedDuration() {
|
||||
self.predictedStreamDuration = d
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//MARK:- Overriden From Parent
|
||||
override func seek(toNeedle needle: Needle) {
|
||||
Log.info("didSeek to needle: \(needle)")
|
||||
guard needle < (ceil(predictedStreamDuration)) else {
|
||||
if !isPlayable {
|
||||
seekNeedleCommandBeforeEngineWasReady = needle
|
||||
}
|
||||
Log.error("tried to seek beyond duration")
|
||||
return
|
||||
}
|
||||
|
||||
self.needle = needle //to tick while paused
|
||||
|
||||
queue.sync { [weak self] in
|
||||
self?.seekHelperDispatchQueue(needle: needle)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The UI would freeze when we tried to call playerNode.stop() while
|
||||
simultaneously filling a buffer on another thread. Solution was to put
|
||||
playerNode related commands in a DispatchQueue
|
||||
*/
|
||||
private func seekHelperDispatchQueue(needle: Needle) {
|
||||
wasPlaying = playerNode.isPlaying
|
||||
|
||||
//NOTE: Order matters
|
||||
//seek needs to be called before stop
|
||||
//Why? Stop will clear all buffers. Each buffer being cleared
|
||||
//will call the callback which then fills the buffers with things to the
|
||||
//right of the needle. If the order of these two were reversed we would
|
||||
//schedule things to the right of the old needle then actually schedule everything
|
||||
//after the new needle
|
||||
//We also need to poll right after the seek to give us more buffers
|
||||
converter.seek(needle)
|
||||
currentTimeOffset = TimeInterval(needle)
|
||||
|
||||
playerNode.stop()
|
||||
|
||||
shouldPollForNextBuffer = true
|
||||
|
||||
updateNetworkBufferRange()
|
||||
}
|
||||
|
||||
override func pause() {
|
||||
queue.async { [weak self] in
|
||||
self?.pauseHelperDispatchQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private func pauseHelperDispatchQueue() {
|
||||
super.pause()
|
||||
}
|
||||
|
||||
override func play() {
|
||||
queue.async { [weak self] in
|
||||
self?.playHelperDispatchQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private func playHelperDispatchQueue() {
|
||||
super.play()
|
||||
}
|
||||
|
||||
override func invalidate() {
|
||||
super.invalidate()
|
||||
converter.invalidate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// AudioThrottler.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
protocol AudioThrottleDelegate: AnyObject {
|
||||
func didUpdate(totalBytesExpected bytes: Int64)
|
||||
}
|
||||
|
||||
protocol AudioThrottleable {
|
||||
init(withRemoteUrl url: AudioURL, withDelegate delegate: AudioThrottleDelegate)
|
||||
func pullNextDataPacket(_ callback: @escaping (Data?) -> ())
|
||||
func tellSeek(offset: UInt64)
|
||||
func pollRangeOfBytesAvailable() -> (UInt64, UInt64)
|
||||
func invalidate()
|
||||
}
|
||||
|
||||
class AudioThrottler: AudioThrottleable {
|
||||
private let queue = DispatchQueue(label: "SwiftAudioPlayer.Throttler", qos: .userInitiated)
|
||||
|
||||
//Init
|
||||
let url: AudioURL
|
||||
weak var delegate: AudioThrottleDelegate?
|
||||
|
||||
private var networkData: [Data] = [] {
|
||||
didSet {
|
||||
// Log.test("NETWORK DATA \(networkData.count)")
|
||||
}
|
||||
}
|
||||
private var lastSentDataPacketIndex = -1
|
||||
|
||||
var shouldThrottle = false
|
||||
var byteOffsetBecauseOfSeek: UInt = 0
|
||||
|
||||
//This will be sent once at beginning of stream and every network seek
|
||||
var totalBytesExpected: Int64? {
|
||||
didSet {
|
||||
if let bytes = totalBytesExpected {
|
||||
delegate?.didUpdate(totalBytesExpected: Int64(byteOffsetBecauseOfSeek) + bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var largestPollingOffsetDifference: UInt64 = 1
|
||||
|
||||
required init(withRemoteUrl url: AudioURL, withDelegate delegate: AudioThrottleDelegate) {
|
||||
self.url = url
|
||||
self.delegate = delegate
|
||||
|
||||
AudioDataManager.shared.startStream(withRemoteURL: url) { [weak self] (pto: StreamProgressPTO) in
|
||||
guard let self = self else {return}
|
||||
Log.debug("received stream data of size \(pto.getData().count) and progress: \(pto.getProgress())")
|
||||
|
||||
if let totalBytesExpected = pto.getTotalBytesExpected() {
|
||||
self.totalBytesExpected = totalBytesExpected
|
||||
}
|
||||
|
||||
self.queue.async { [weak self] in
|
||||
self?.networkData.append(pto.getData())
|
||||
StreamingDownloadDirector.shared.didUpdate(url.key, networkStreamProgress: pto.getProgress())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func tellSeek(offset: UInt64) {
|
||||
Log.info("seek with offset: \(offset)")
|
||||
|
||||
self.queue.async { [weak self] in
|
||||
self?.seekQueueHelper(offset)
|
||||
}
|
||||
}
|
||||
|
||||
func seekQueueHelper(_ offset: UInt64) {
|
||||
let offsetToFind = Int(offset) - Int(byteOffsetBecauseOfSeek)
|
||||
|
||||
var shouldStartNewStream: Bool = false
|
||||
|
||||
// if we have no data start a new stream after seek
|
||||
if networkData.count == 0 {
|
||||
shouldStartNewStream = true
|
||||
}
|
||||
|
||||
// if what we're looking for is outside of available data, start a new stream
|
||||
if offset < byteOffsetBecauseOfSeek || offsetToFind > networkData.sum {
|
||||
shouldStartNewStream = true
|
||||
}
|
||||
|
||||
// we should have the data within our cache. find it and save the index for the next pull
|
||||
if let indexOfDataContainingOffset = networkData.getIndexContainingByteOffset(offsetToFind) {
|
||||
lastSentDataPacketIndex = indexOfDataContainingOffset - 1
|
||||
}
|
||||
|
||||
if shouldStartNewStream {
|
||||
byteOffsetBecauseOfSeek = UInt(offset)
|
||||
lastSentDataPacketIndex = -1
|
||||
AudioDataManager.shared.seekStream(withRemoteURL: url, toByteOffset: offset)
|
||||
|
||||
networkData = []
|
||||
return
|
||||
}
|
||||
|
||||
Log.error("83672 Should not get here")
|
||||
}
|
||||
|
||||
func pollRangeOfBytesAvailable() -> (UInt64, UInt64) {
|
||||
let start = byteOffsetBecauseOfSeek
|
||||
let end = networkData.sum + Int(byteOffsetBecauseOfSeek)
|
||||
|
||||
return (UInt64(start), UInt64(end))
|
||||
}
|
||||
|
||||
func pullNextDataPacket(_ callback: @escaping (Data?) -> ()) {
|
||||
queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard self.lastSentDataPacketIndex < self.networkData.count - 1 else {
|
||||
callback(nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.lastSentDataPacketIndex += 1
|
||||
|
||||
callback(self.networkData[self.lastSentDataPacketIndex])
|
||||
}
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
AudioDataManager.shared.deleteStream(withRemoteURL: url)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Data {
|
||||
var sum: Int {
|
||||
get {
|
||||
return self.reduce(0) { $0 + $1.count }
|
||||
}
|
||||
}
|
||||
|
||||
func getIndexContainingByteOffset(_ offset: Int) -> Int? {
|
||||
var dataCount = 0
|
||||
|
||||
for (i, data) in self.enumerated() {
|
||||
if offset >= dataCount && offset <= dataCount + data.count {
|
||||
return i
|
||||
}
|
||||
|
||||
dataCount += data.count
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//
|
||||
// AudioConverter.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
import AudioToolbox
|
||||
|
||||
protocol AudioConvertable {
|
||||
var engineAudioFormat: AVAudioFormat {get}
|
||||
|
||||
init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat, withPCMBufferSize size: AVAudioFrameCount) throws
|
||||
func pullBuffer() throws -> AVAudioPCMBuffer
|
||||
func pollPredictedDuration() -> Duration?
|
||||
func pollNetworkAudioAvailabilityRange() -> (Needle, Duration)
|
||||
func seek(_ needle: Needle)
|
||||
func invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
Creates PCM Buffers for the audio engine
|
||||
|
||||
Main Responsibilities:
|
||||
|
||||
CREATE CONVERTER. Waits for parser to give back audio format then creates a
|
||||
converter.
|
||||
|
||||
USE CONVERTER. The converter takes parsed audio packets and 1. transforms them
|
||||
into a format that the engine can take. 2. Fills a buffer of a certain size.
|
||||
Note that we might not need a converted if the format that the engine takes in
|
||||
is the same as what the parser outputs.
|
||||
|
||||
KEEP AUDIO INDEX: The engine keeps trying to pull a buffer from converter. The
|
||||
converter will keep pulling from parser. The converter calculates the exact
|
||||
index that it wants to convert and keeps pulling at that index until the parser
|
||||
passes up a value.
|
||||
*/
|
||||
class AudioConverter: AudioConvertable {
|
||||
let queue = DispatchQueue(label: "SwiftAudioPlayer.audio_reader_queue")
|
||||
|
||||
//From Init
|
||||
var parser: AudioParsable!
|
||||
|
||||
//From protocol
|
||||
public var engineAudioFormat: AVAudioFormat
|
||||
let pcmBufferSize: AVAudioFrameCount
|
||||
|
||||
//Field
|
||||
var converter: AudioConverterRef? //set by AudioConverterNew
|
||||
var currentAudioPacketIndex: AVAudioPacketCount = 0
|
||||
|
||||
// use to store reference to the allocated buffers from the converter to properly deallocate them before the next packet is being converted
|
||||
var converterBuffer: UnsafeMutableRawPointer?
|
||||
var converterDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?
|
||||
|
||||
required init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat, withPCMBufferSize size: AVAudioFrameCount) throws {
|
||||
self.engineAudioFormat = toEngineAudioFormat
|
||||
self.pcmBufferSize = size
|
||||
|
||||
do {
|
||||
parser = try AudioParser(withRemoteUrl: url, bufferSize: Int(size), parsedFileAudioFormatCallback: {
|
||||
[weak self] (fileAudioFormat: AVAudioFormat) in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
let sourceFormat = fileAudioFormat.streamDescription
|
||||
let destinationFormat = strongSelf.engineAudioFormat.streamDescription
|
||||
let result = AudioConverterNew(sourceFormat, destinationFormat, &strongSelf.converter)
|
||||
|
||||
guard result == noErr else {
|
||||
Log.monitor(ConverterError.unableToCreateConverter(result).errorDescription as Any)
|
||||
return
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
throw ConverterError.failedToCreateParser
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
guard let converter = converter else {
|
||||
Log.error("No converter n deinit!")
|
||||
return
|
||||
}
|
||||
|
||||
guard AudioConverterDispose(converter) == noErr else {
|
||||
Log.monitor("failed to dispose audio converter")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func pullBuffer() throws -> AVAudioPCMBuffer {
|
||||
guard let converter = converter else {
|
||||
Log.debug("reader_error trying to read before converter has been created")
|
||||
throw ConverterError.cannotCreatePCMBufferWithoutConverter
|
||||
}
|
||||
|
||||
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: engineAudioFormat, frameCapacity: pcmBufferSize) else {
|
||||
Log.monitor(ConverterError.failedToCreatePCMBuffer.errorDescription as Any)
|
||||
throw ConverterError.failedToCreatePCMBuffer
|
||||
}
|
||||
pcmBuffer.frameLength = pcmBufferSize
|
||||
|
||||
/**
|
||||
The whole thing is wrapped in queue.sync() because the converter listener
|
||||
needs to eventually increment the audioPatcketIndex. We don't want threads
|
||||
to mess this up
|
||||
*/
|
||||
return try queue.sync { () -> AVAudioPCMBuffer in
|
||||
let framesPerPacket = engineAudioFormat.streamDescription.pointee.mFramesPerPacket
|
||||
var numberOfPacketsWeWantTheBufferToFill = pcmBuffer.frameLength / framesPerPacket
|
||||
|
||||
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
|
||||
let status = AudioConverterFillComplexBuffer(converter, ConverterListener, context, &numberOfPacketsWeWantTheBufferToFill, pcmBuffer.mutableAudioBufferList, nil)
|
||||
|
||||
guard status == noErr else {
|
||||
switch status {
|
||||
case ReaderMissingSourceFormatError:
|
||||
throw ConverterError.parserMissingDataFormat
|
||||
case ReaderReachedEndOfDataError:
|
||||
throw ConverterError.reachedEndOfFile
|
||||
case ReaderNotEnoughDataError:
|
||||
throw ConverterError.notEnoughData
|
||||
case ReaderShouldNotHappenError:
|
||||
throw ConverterError.superConcerningShouldNeverHappen
|
||||
default:
|
||||
throw ConverterError.converterFailed(status)
|
||||
}
|
||||
}
|
||||
return pcmBuffer
|
||||
}
|
||||
}
|
||||
|
||||
func seek(_ needle: Needle) {
|
||||
guard let audioPacketIndex = getPacketIndex(forNeedle: needle) else {
|
||||
return
|
||||
}
|
||||
Log.info("didSeek to packet index: \(audioPacketIndex)")
|
||||
queue.sync {
|
||||
currentAudioPacketIndex = audioPacketIndex
|
||||
parser.tellSeek(toIndex: audioPacketIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func pollPredictedDuration() -> Duration? {
|
||||
return parser.predictedDuration
|
||||
}
|
||||
|
||||
func pollNetworkAudioAvailabilityRange() -> (Needle, Duration) {
|
||||
return parser.pollRangeOfSecondsAvailableFromNetwork()
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
parser.invalidate()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private func frameOffset(forTime time: TimeInterval) -> AVAudioFramePosition? {
|
||||
guard let _ = parser.fileAudioFormat?.streamDescription.pointee, let frameCount = parser.totalPredictedAudioFrameCount, let duration = parser.predictedDuration else { return nil }
|
||||
let ratio = time / duration
|
||||
return AVAudioFramePosition(Double(frameCount) * ratio)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// AudioConverterErrors.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
import AudioToolbox
|
||||
|
||||
|
||||
let ReaderReachedEndOfDataError: OSStatus = 932332581
|
||||
let ReaderNotEnoughDataError: OSStatus = 932332582
|
||||
let ReaderMissingSourceFormatError: OSStatus = 932332583
|
||||
let ReaderMissingParserError: OSStatus = 932332584
|
||||
let ReaderShouldNotHappenError: OSStatus = 932332585
|
||||
|
||||
public enum ConverterError: LocalizedError {
|
||||
case cannotLockQueue
|
||||
case converterFailed(OSStatus)
|
||||
case cannotCreatePCMBufferWithoutConverter
|
||||
case failedToCreateDestinationFormat
|
||||
case failedToCreatePCMBuffer
|
||||
case notEnoughData
|
||||
case parserMissingDataFormat
|
||||
case reachedEndOfFile
|
||||
case unableToCreateConverter(OSStatus)
|
||||
case superConcerningShouldNeverHappen
|
||||
case throttleParsingBuffersForEngine
|
||||
case failedToCreateParser
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .cannotLockQueue:
|
||||
Log.warn("Failed to lock queue")
|
||||
return "Failed to lock queue"
|
||||
case .converterFailed(let status):
|
||||
Log.warn(localizedDescriptionFromConverterError(status))
|
||||
return localizedDescriptionFromConverterError(status)
|
||||
case .failedToCreateDestinationFormat:
|
||||
Log.warn("Failed to create a destination (processing) format")
|
||||
return "Failed to create a destination (processing) format"
|
||||
case .failedToCreatePCMBuffer:
|
||||
Log.warn("Failed to create PCM buffer for reading data")
|
||||
return "Failed to create PCM buffer for reading data"
|
||||
case .notEnoughData:
|
||||
Log.warn("Not enough data for read-conversion operation")
|
||||
return "Not enough data for read-conversion operation"
|
||||
case .parserMissingDataFormat:
|
||||
Log.warn("Parser is missing a valid data format")
|
||||
return "Parser is missing a valid data format"
|
||||
case .reachedEndOfFile:
|
||||
Log.warn("Reached the end of the file")
|
||||
return "Reached the end of the file"
|
||||
case .unableToCreateConverter(let status):
|
||||
return localizedDescriptionFromConverterError(status)
|
||||
case .superConcerningShouldNeverHappen:
|
||||
Log.warn("Weird unexpected reader error. Should not have happened")
|
||||
return "Weird unexpected reader error. Should not have happened"
|
||||
case .cannotCreatePCMBufferWithoutConverter:
|
||||
Log.debug("Could not create a PCM Buffer because reader does not have a converter yet")
|
||||
return "Could not create a PCM Buffer because reader does not have a converter yet"
|
||||
case .throttleParsingBuffersForEngine:
|
||||
Log.warn("Preventing the reader from creating more PCM buffers since the player has more than 60 seconds of audio already to play")
|
||||
return "Preventing the reader from creating more PCM buffers since the player has more than 60 seconds of audio already to play"
|
||||
case .failedToCreateParser:
|
||||
Log.warn("Could not create a parser")
|
||||
return "Could not create a parser"
|
||||
}
|
||||
}
|
||||
|
||||
func localizedDescriptionFromConverterError(_ status: OSStatus) -> String {
|
||||
switch status {
|
||||
case kAudioConverterErr_FormatNotSupported:
|
||||
return "Format not supported"
|
||||
case kAudioConverterErr_OperationNotSupported:
|
||||
return "Operation not supported"
|
||||
case kAudioConverterErr_PropertyNotSupported:
|
||||
return "Property not supported"
|
||||
case kAudioConverterErr_InvalidInputSize:
|
||||
return "Invalid input size"
|
||||
case kAudioConverterErr_InvalidOutputSize:
|
||||
return "Invalid output size"
|
||||
case kAudioConverterErr_BadPropertySizeError:
|
||||
return "Bad property size error"
|
||||
case kAudioConverterErr_RequiresPacketDescriptionsError:
|
||||
return "Requires packet descriptions"
|
||||
case kAudioConverterErr_InputSampleRateOutOfRange:
|
||||
return "Input sample rate out of range"
|
||||
case kAudioConverterErr_OutputSampleRateOutOfRange:
|
||||
return "Output sample rate out of range"
|
||||
case kAudioConverterErr_HardwareInUse:
|
||||
return "Hardware is in use"
|
||||
case kAudioConverterErr_NoHardwarePermission:
|
||||
return "No hardware permission"
|
||||
default:
|
||||
return "Unspecified error"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// AudioConverterListener.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
import AudioToolbox
|
||||
|
||||
func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMutablePointer<UInt32>, _ ioData: UnsafeMutablePointer<AudioBufferList>, _ outPacketDescriptions: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>?>?, _ context: UnsafeMutableRawPointer?) -> OSStatus {
|
||||
let selfAudioConverter = Unmanaged<AudioConverter>.fromOpaque(context!).takeUnretainedValue()
|
||||
|
||||
guard let parser = selfAudioConverter.parser else {
|
||||
Log.monitor("ReaderMissingParserError")
|
||||
return ReaderMissingParserError
|
||||
}
|
||||
|
||||
guard let fileAudioFormat = parser.fileAudioFormat else {
|
||||
Log.monitor("ReaderMissingSourceFormatError")
|
||||
return ReaderMissingSourceFormatError
|
||||
}
|
||||
|
||||
var audioPacketFromParser:(AudioStreamPacketDescription?, Data)?
|
||||
do {
|
||||
audioPacketFromParser = try parser.pullPacket(atIndex: selfAudioConverter.currentAudioPacketIndex)
|
||||
Log.debug("received packet from parser at index: \(selfAudioConverter.currentAudioPacketIndex)")
|
||||
} catch ParserError.notEnoughDataForReader {
|
||||
return ReaderNotEnoughDataError
|
||||
} catch ParserError.readerAskingBeyondEndOfFile {
|
||||
//On output, the number of packets of audio data provided for conversion,
|
||||
//or 0 if there is no more data to convert.
|
||||
packetCount.pointee = 0
|
||||
return ReaderReachedEndOfDataError
|
||||
} catch {
|
||||
return ReaderShouldNotHappenError
|
||||
}
|
||||
|
||||
guard let audioPacket = audioPacketFromParser else {
|
||||
return ReaderShouldNotHappenError
|
||||
}
|
||||
|
||||
if let lastBuffer = selfAudioConverter.converterBuffer {
|
||||
lastBuffer.deallocate()
|
||||
}
|
||||
|
||||
// Copy data over (note we've only processing a single packet of data at a time)
|
||||
var packet = audioPacket.1
|
||||
let packetByteCount = packet.count //this is not the count of an array
|
||||
ioData.pointee.mNumberBuffers = 1
|
||||
ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: packetByteCount, alignment: 0)
|
||||
_ = packet.accessMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
|
||||
memcpy((ioData.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self))!, bytes, packetByteCount)
|
||||
})
|
||||
ioData.pointee.mBuffers.mDataByteSize = UInt32(packetByteCount)
|
||||
|
||||
selfAudioConverter.converterBuffer = ioData.pointee.mBuffers.mData
|
||||
|
||||
if let lastDescription = selfAudioConverter.converterDescriptions {
|
||||
lastDescription.deallocate()
|
||||
}
|
||||
|
||||
// Handle packet descriptions for compressed formats (MP3, AAC, etc)
|
||||
let fileFormatDescription = fileAudioFormat.streamDescription.pointee
|
||||
if fileFormatDescription.mFormatID != kAudioFormatLinearPCM {
|
||||
if outPacketDescriptions?.pointee == nil {
|
||||
outPacketDescriptions?.pointee = UnsafeMutablePointer<AudioStreamPacketDescription>.allocate(capacity: 1)
|
||||
}
|
||||
outPacketDescriptions?.pointee?.pointee.mDataByteSize = UInt32(packetByteCount)
|
||||
outPacketDescriptions?.pointee?.pointee.mStartOffset = 0
|
||||
outPacketDescriptions?.pointee?.pointee.mVariableFramesInPacket = 0
|
||||
}
|
||||
|
||||
selfAudioConverter.converterDescriptions = outPacketDescriptions?.pointee
|
||||
|
||||
packetCount.pointee = 1
|
||||
|
||||
//we've successfully given a packet to the LPCM buffer now we can process the next audio packet
|
||||
selfAudioConverter.currentAudioPacketIndex = selfAudioConverter.currentAudioPacketIndex + 1
|
||||
|
||||
return noErr
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// AudioParsable.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
protocol AudioParsable { //For the layer above us
|
||||
var fileAudioFormat: AVAudioFormat? {get}
|
||||
var totalPredictedPacketCount: AVAudioPacketCount { get }
|
||||
func tellSeek(toIndex index: AVAudioPacketCount)
|
||||
func pollRangeOfSecondsAvailableFromNetwork() -> (Needle, Duration)
|
||||
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data)
|
||||
func invalidate() //deinit caused concurrency problems
|
||||
}
|
||||
|
||||
extension AudioParsable { //For the layer above us
|
||||
var predictedDuration: Duration? {
|
||||
guard let sampleRate = fileAudioFormat?.sampleRate else { return nil }
|
||||
guard let totalPredictedFrameCount = totalPredictedAudioFrameCount else { return nil }
|
||||
return Duration(totalPredictedFrameCount)/Duration(sampleRate)
|
||||
}
|
||||
|
||||
|
||||
var totalPredictedAudioFrameCount: AUAudioFrameCount? {
|
||||
guard let framesPerPacket = fileAudioFormat?.streamDescription.pointee.mFramesPerPacket else {return nil }
|
||||
return AVAudioFrameCount(totalPredictedPacketCount) * AVAudioFrameCount(framesPerPacket)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
//
|
||||
// AudioParser.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
/**
|
||||
DEFINITIONS
|
||||
|
||||
An audio stream is a continuous series of data that represents a sound, such as a song.
|
||||
|
||||
A channel is a discrete track of monophonic audio. A monophonic stream has one channel; a stereo stream has two channels.
|
||||
|
||||
A sample is single numerical value for a single audio channel in an audio stream.
|
||||
|
||||
A frame is a collection of time-coincident samples. For instance, a linear PCM stereo sound file has two samples per frame, one for the left channel and one for the right channel.
|
||||
|
||||
A packet is a collection of one or more contiguous frames. A packet defines the smallest meaningful set of frames for a given audio data format, and is the smallest data unit for which time can be measured. In linear PCM audio, a packet holds a single frame. In compressed formats, it typically holds more; in some formats, the number of frames per packet varies.
|
||||
|
||||
The sample rate for a stream is the number of frames per second of uncompressed (or, for compressed formats, the equivalent in decompressed) audio.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
//TODO: what if user seeks beyond the data we have? What if we're done but user seeks even further than what we have
|
||||
|
||||
class AudioParser: AudioParsable {
|
||||
private var MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING = 8192 // this will be modified when we know the file format to be just enough packets to fill up 1 pcm buffer
|
||||
private var framesPerBuffer: Int = 1
|
||||
|
||||
//MARK:- For OS parser class
|
||||
var parsedAudioHeaderPacketCount: UInt64 = 0
|
||||
var parsedAudioPacketDataSize: UInt64 = 0
|
||||
var parsedAudioDataOffset: UInt64 = 0
|
||||
var streamID: AudioFileStreamID?
|
||||
public var fileAudioFormat: AVAudioFormat? {
|
||||
didSet {
|
||||
if let format = fileAudioFormat, oldValue == nil {
|
||||
MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING = framesPerBuffer/Int(format.streamDescription.pointee.mFramesPerPacket)
|
||||
parsedFileAudioFormatCallback(format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK:- Our vars
|
||||
//Init
|
||||
let url: AudioURL
|
||||
var throttler: AudioThrottleable!
|
||||
|
||||
//Our use
|
||||
var expectedFileSizeInBytes: UInt64?
|
||||
var networkProgress: Double = 0
|
||||
var parsedFileAudioFormatCallback: (AVAudioFormat) -> ()
|
||||
var indexSeekOffset: AVAudioPacketCount = 0
|
||||
var shouldPreventPacketFromFillingUp = false
|
||||
|
||||
public var totalPredictedPacketCount: AVAudioPacketCount {
|
||||
if parsedAudioHeaderPacketCount != 0 {
|
||||
//TODO: we should log the duration to the server for better user experience
|
||||
return max(AVAudioPacketCount(parsedAudioHeaderPacketCount), AVAudioPacketCount(audioPackets.count))
|
||||
}
|
||||
|
||||
let sizeOfFileInBytes: UInt64 = expectedFileSizeInBytes != nil ? expectedFileSizeInBytes! : 0
|
||||
|
||||
guard let bytesPerPacket = averageBytesPerPacket else {
|
||||
return AVAudioPacketCount(0)
|
||||
}
|
||||
|
||||
let predictedCount = AVAudioPacketCount(Double(sizeOfFileInBytes) / bytesPerPacket)
|
||||
|
||||
guard networkProgress != 1.0 else {
|
||||
return max(AVAudioPacketCount(audioPackets.count), predictedCount)
|
||||
}
|
||||
|
||||
return predictedCount
|
||||
}
|
||||
|
||||
var sumOfParsedAudioBytes:UInt32 = 0
|
||||
var numberOfPacketsParsed:UInt32 = 0
|
||||
var audioPackets: [(AudioStreamPacketDescription?,Data)] = [] {
|
||||
didSet {
|
||||
if let audioPacketByteSize = audioPackets.last?.0?.mDataByteSize {
|
||||
sumOfParsedAudioBytes += audioPacketByteSize
|
||||
} else if let audioPacketByteSize = audioPackets.last?.1.count { // for uncompressed audio there are no descriptors to say how many bytes of audio are in this packet so we approximate by data size
|
||||
sumOfParsedAudioBytes += UInt32(audioPacketByteSize)
|
||||
}
|
||||
|
||||
numberOfPacketsParsed += 1
|
||||
|
||||
//TODO: duration will not be accurate with WAV or AIFF
|
||||
}
|
||||
}
|
||||
private let lockQueue = DispatchQueue(label: "SwiftAudioPlayer.Parser.packets.lock")
|
||||
var lastSentAudioPacketIndex = -1
|
||||
|
||||
/**
|
||||
Audio packets varry in size. The first one parsed in a batch of audio
|
||||
packets is usually off by 1 from the others. We use the
|
||||
averageByesPerPacket for two things. 1. Predicting total audio packet count
|
||||
which is used for duration. 2. Calculate seeking spot for throttler and
|
||||
network seek. This used to be an Int but caused inacuracies for longer
|
||||
podcasts. Since Double->Int is floored the parser would ask for byte 979312
|
||||
but that spot is actually suppose to be 982280 from the throttler's perspective
|
||||
*/
|
||||
var averageBytesPerPacket:Double? {
|
||||
if numberOfPacketsParsed == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Double(sumOfParsedAudioBytes)/Double(numberOfPacketsParsed)
|
||||
}
|
||||
|
||||
var isParsingComplete: Bool {
|
||||
guard fileAudioFormat != nil else {
|
||||
return false
|
||||
}
|
||||
//TODO: will this ever return true? Predicted uses MAX of prediction of total packet length
|
||||
return audioPackets.count == totalPredictedPacketCount
|
||||
}
|
||||
|
||||
var streamChangeListenerId: UInt?
|
||||
|
||||
init(withRemoteUrl url: AudioURL, bufferSize: Int, parsedFileAudioFormatCallback: @escaping(AVAudioFormat) -> ()) throws {
|
||||
self.url = url
|
||||
self.framesPerBuffer = bufferSize
|
||||
self.parsedFileAudioFormatCallback = parsedFileAudioFormatCallback
|
||||
|
||||
self.throttler = AudioThrottler(withRemoteUrl: url, withDelegate: self)
|
||||
|
||||
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (key, progress) in
|
||||
guard let self = self else { return }
|
||||
guard key == url.key else { return }
|
||||
self.networkProgress = progress
|
||||
|
||||
// initially parse a bunch of packets
|
||||
self.lockQueue.sync {
|
||||
if self.fileAudioFormat == nil {
|
||||
self.processNextDataPacket()
|
||||
} else if self.audioPackets.count - self.lastSentAudioPacketIndex < self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
|
||||
//Open the stream and when we call parse data is fed into this stream
|
||||
guard AudioFileStreamOpen(context, ParserPropertyListener, ParserPacketListener, kAudioFileMP3Type, &streamID) == noErr else {
|
||||
throw ParserError.couldNotOpenStream
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let id = streamChangeListenerId {
|
||||
StreamingDownloadDirector.shared.detach(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data) {
|
||||
determineIfMoreDataNeedsToBeParsed(index: index)
|
||||
|
||||
// Check if we've reached the end of the packets. We have two scenarios:
|
||||
// 1. We've reached the end of the packet data and the file has been completely parsed
|
||||
// 2. We've reached the end of the data we currently have downloaded, but not the file
|
||||
let packetIndex = index - indexSeekOffset
|
||||
|
||||
var exception: ParserError? = nil
|
||||
var packet: (AudioStreamPacketDescription?, Data) = (nil, Data())
|
||||
lockQueue.sync {
|
||||
if packetIndex >= self.audioPackets.count {
|
||||
if isParsingComplete {
|
||||
exception = ParserError.readerAskingBeyondEndOfFile
|
||||
return
|
||||
} else {
|
||||
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(self.audioPackets.count), we predict \(self.totalPredictedPacketCount) in total")
|
||||
exception = ParserError.notEnoughDataForReader
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lastSentAudioPacketIndex = Int(packetIndex)
|
||||
packet = audioPackets[Int(packetIndex)]
|
||||
}
|
||||
if let exception = exception {
|
||||
throw exception
|
||||
} else {
|
||||
return packet
|
||||
}
|
||||
}
|
||||
|
||||
private func determineIfMoreDataNeedsToBeParsed(index: AVAudioPacketCount) {
|
||||
lockQueue.sync {
|
||||
if index > self.audioPackets.count - self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tellSeek(toIndex index: AVAudioPacketCount) {
|
||||
//Already within the processed audio packets. Ignore
|
||||
var isIndexValid: Bool = true
|
||||
lockQueue.sync {
|
||||
if self.indexSeekOffset <= index && index < self.audioPackets.count + Int(self.indexSeekOffset) {
|
||||
isIndexValid = false
|
||||
}
|
||||
}
|
||||
guard isIndexValid else { return }
|
||||
|
||||
guard let byteOffset = getOffset(fromPacketIndex: index) else {
|
||||
return
|
||||
}
|
||||
Log.info("did not have processed audio for index: \(index) / offset: \(byteOffset)")
|
||||
|
||||
indexSeekOffset = index
|
||||
|
||||
// NOTE: Order matters. Need to prevent appending to the array before we clean it. Just in case
|
||||
// then we tell the throttler to send us appropriate packet
|
||||
shouldPreventPacketFromFillingUp = true
|
||||
lockQueue.sync {
|
||||
self.audioPackets = []
|
||||
}
|
||||
|
||||
throttler.tellSeek(offset: byteOffset)
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
|
||||
private func getOffset(fromPacketIndex index: AVAudioPacketCount) -> UInt64? {
|
||||
//Clear current buffer if we have audio format
|
||||
guard fileAudioFormat != nil, let bytesPerPacket = self.averageBytesPerPacket else {
|
||||
Log.error("should not get here \(String(describing: fileAudioFormat)) and \(String(describing: self.averageBytesPerPacket))")
|
||||
return nil
|
||||
}
|
||||
|
||||
return UInt64(Double(index) * bytesPerPacket) + parsedAudioDataOffset
|
||||
}
|
||||
|
||||
func pollRangeOfSecondsAvailableFromNetwork() -> (Needle, Duration) {
|
||||
let range = throttler.pollRangeOfBytesAvailable()
|
||||
|
||||
let startPacket = getPacket(fromOffset: range.0) != nil ? getPacket(fromOffset: range.0)! : 0
|
||||
|
||||
guard let startFrame = getFrame(forPacket: startPacket), let startNeedle = getNeedle(forFrame: startFrame) else {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
guard let endPacket = getPacket(fromOffset: range.1), let endFrame = getFrame(forPacket: endPacket), let endNeedle = getNeedle(forFrame: endFrame) else {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
return (startNeedle, Duration(endNeedle))
|
||||
}
|
||||
|
||||
private func getPacket(fromOffset offset: UInt64) -> AVAudioPacketCount? {
|
||||
guard fileAudioFormat != nil, let bytesPerPacket = self.averageBytesPerPacket else { return nil }
|
||||
let audioDataBytes = Int(offset) - Int(parsedAudioDataOffset)
|
||||
|
||||
guard audioDataBytes > 0 else { // Because we error out if we try to set a negative number as AVAudioPacketCount which is a UInt32
|
||||
return nil
|
||||
}
|
||||
|
||||
return AVAudioPacketCount(Double(audioDataBytes) / bytesPerPacket)
|
||||
}
|
||||
|
||||
private func getFrame(forPacket packet: AVAudioPacketCount) -> AVAudioFrameCount? {
|
||||
guard let framesPerPacket = fileAudioFormat?.streamDescription.pointee.mFramesPerPacket else { return nil }
|
||||
return packet * framesPerPacket
|
||||
}
|
||||
|
||||
private func getNeedle(forFrame frame: AVAudioFrameCount) -> Needle? {
|
||||
guard let _ = fileAudioFormat?.streamDescription.pointee, let frameCount = totalPredictedAudioFrameCount, let duration = predictedDuration else { return nil }
|
||||
|
||||
guard duration > 0 else { return nil }
|
||||
|
||||
return Needle(TimeInterval(frame)/TimeInterval(frameCount)*duration)
|
||||
}
|
||||
|
||||
func append(description: AudioStreamPacketDescription?, data: Data) {
|
||||
lockQueue.sync {
|
||||
self.audioPackets.append((description, data))
|
||||
}
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
throttler.invalidate()
|
||||
|
||||
//FIXME: See Note below. Don't remove this until the problem has been properly solved
|
||||
//if let sId = streamID {
|
||||
// let result = AudioFileStreamClose(sId)
|
||||
// if result != noErr {
|
||||
// Log.monitor("parser_error", ParserError.failedToParseBytes(result).errorDescription)
|
||||
// }
|
||||
//}
|
||||
/**
|
||||
We saw a bad access in the parser. We think this is because AudioFileStreamClose is called before the parser finished parsing a set of networkPackets.
|
||||
|
||||
Three solutions we thought of:
|
||||
1. Make parser a singleton and have callbacks that use and ID
|
||||
2. Do some math on network data size and parsed packets. The parsed packets get 99.9% to the network data
|
||||
3. Uncomment AudioFileStreamClose. There will be potential memory leaks
|
||||
|
||||
We chose option 3 because:
|
||||
+ we looked at memory hit and it was neglegible
|
||||
+ simplest solution
|
||||
– we might forget about commenting this out and run into a bug
|
||||
*/
|
||||
|
||||
|
||||
}
|
||||
|
||||
private func processNextDataPacket() {
|
||||
throttler.pullNextDataPacket { [weak self] (d) in
|
||||
guard let self = self else { return }
|
||||
guard let data = d else { return }
|
||||
|
||||
self.lockQueue.sync {
|
||||
Log.debug("processing data count: \(data.count) :: already had \(self.audioPackets.count) audio packets")
|
||||
}
|
||||
self.shouldPreventPacketFromFillingUp = false
|
||||
do {
|
||||
let sID = self.streamID!
|
||||
let dataSize = data.count
|
||||
|
||||
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
|
||||
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
|
||||
guard result == noErr else {
|
||||
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
|
||||
throw ParserError.failedToParseBytes(result)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
Log.monitor(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK:- AudioThrottleDelegate
|
||||
extension AudioParser: AudioThrottleDelegate {
|
||||
func didUpdate(totalBytesExpected bytes: Int64) {
|
||||
expectedFileSizeInBytes = UInt64(bytes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// AudioParserErrors.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
enum ParserError: LocalizedError {
|
||||
case couldNotOpenStream
|
||||
case failedToParseBytes(OSStatus)
|
||||
case notEnoughDataForReader
|
||||
case readerAskingBeyondEndOfFile
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .couldNotOpenStream:
|
||||
return "Could not open stream for parsing"
|
||||
case .failedToParseBytes(let status):
|
||||
return localizedDescriptionFromParseError(status)
|
||||
case .notEnoughDataForReader:
|
||||
return "Not enough data for reader. Will attemp to seek"
|
||||
case .readerAskingBeyondEndOfFile:
|
||||
return "Reader asking for packets beyond the end of file"
|
||||
}
|
||||
}
|
||||
|
||||
func localizedDescriptionFromParseError(_ status: OSStatus) -> String {
|
||||
switch status {
|
||||
case kAudioFileStreamError_UnsupportedFileType:
|
||||
return "The file type is not supported"
|
||||
case kAudioFileStreamError_UnsupportedDataFormat:
|
||||
return "The data format is not supported by this file type"
|
||||
case kAudioFileStreamError_UnsupportedProperty:
|
||||
return "The property is not supported"
|
||||
case kAudioFileStreamError_BadPropertySize:
|
||||
return "The size of the property data was not correct"
|
||||
case kAudioFileStreamError_NotOptimized:
|
||||
return "It is not possible to produce output packets because the file's packet table or other defining"
|
||||
case kAudioFileStreamError_InvalidPacketOffset:
|
||||
return "A packet offset was less than zero, or past the end of the file,"
|
||||
case kAudioFileStreamError_InvalidFile:
|
||||
return "The file is malformed, or otherwise not a valid instance of an audio file of its type, or is not recognized as an audio file"
|
||||
case kAudioFileStreamError_ValueUnknown:
|
||||
return "The property value is not present in this file before the audio data"
|
||||
case kAudioFileStreamError_DataUnavailable:
|
||||
return "The amount of data provided to the parser was insufficient to produce any result"
|
||||
case kAudioFileStreamError_IllegalOperation:
|
||||
return "An illegal operation was attempted"
|
||||
default:
|
||||
return "An unspecified error occurred"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// This extension just helps us print out the name of an `AudioFileStreamPropertyID`. Purely for debugging and not essential to the main functionality of the parser.
|
||||
extension AudioFileStreamPropertyID {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case kAudioFileStreamProperty_ReadyToProducePackets:
|
||||
return "Ready to produce packets"
|
||||
case kAudioFileStreamProperty_FileFormat:
|
||||
return "File format"
|
||||
case kAudioFileStreamProperty_DataFormat:
|
||||
return "Data format"
|
||||
case kAudioFileStreamProperty_AudioDataByteCount:
|
||||
return "Byte count"
|
||||
case kAudioFileStreamProperty_AudioDataPacketCount:
|
||||
return "Packet count"
|
||||
case kAudioFileStreamProperty_DataOffset:
|
||||
return "Data offset"
|
||||
case kAudioFileStreamProperty_BitRate:
|
||||
return "Bit rate"
|
||||
case kAudioFileStreamProperty_FormatList:
|
||||
return "Format list"
|
||||
case kAudioFileStreamProperty_MagicCookieData:
|
||||
return "Magic cookie"
|
||||
case kAudioFileStreamProperty_MaximumPacketSize:
|
||||
return "Max packet size"
|
||||
case kAudioFileStreamProperty_ChannelLayout:
|
||||
return "Channel layout"
|
||||
case kAudioFileStreamProperty_PacketToFrame:
|
||||
return "Packet to frame"
|
||||
case kAudioFileStreamProperty_FrameToPacket:
|
||||
return "Frame to packet"
|
||||
case kAudioFileStreamProperty_PacketToByte:
|
||||
return "Packet to byte"
|
||||
case kAudioFileStreamProperty_ByteToPacket:
|
||||
return "Byte to packet"
|
||||
case kAudioFileStreamProperty_PacketTableInfo:
|
||||
return "Packet table"
|
||||
case kAudioFileStreamProperty_PacketSizeUpperBound:
|
||||
return "Packet size upper bound"
|
||||
case kAudioFileStreamProperty_AverageBytesPerPacket:
|
||||
return "Average bytes per packet"
|
||||
case kAudioFileStreamProperty_InfoDictionary:
|
||||
return "Info dictionary"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// AudioParserPacketListener.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer, Moy Inzunza
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
#if swift(>=5.3)
|
||||
func ParserPacketListener (_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ streamData: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?) {
|
||||
parserPacket(context, byteCount, packetCount, streamData, packetDescriptions)
|
||||
}
|
||||
|
||||
#else
|
||||
func ParserPacketListener (_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ streamData: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {
|
||||
parserPacket(context, byteCount, packetCount, streamData, packetDescriptions)
|
||||
}
|
||||
#endif
|
||||
|
||||
func parserPacket(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ streamData: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?){
|
||||
|
||||
let selfAudioParser = Unmanaged<AudioParser>.fromOpaque(context).takeUnretainedValue()
|
||||
|
||||
guard let fileAudioFormat = selfAudioParser.fileAudioFormat else {
|
||||
Log.monitor("should not have reached packet listener without a data format")
|
||||
return
|
||||
}
|
||||
|
||||
guard selfAudioParser.shouldPreventPacketFromFillingUp == false else {
|
||||
Log.error("skipping parsing packets because of seek")
|
||||
return
|
||||
}
|
||||
|
||||
//TODO refactor this after we get it working
|
||||
if let compressedPacketDescriptions = packetDescriptions { // is compressed audio (.mp3)
|
||||
Log.debug("compressed audio")
|
||||
for i in 0 ..< Int(packetCount) {
|
||||
let audioPacketDescription = compressedPacketDescriptions[i]
|
||||
let audioPacketStart = Int(audioPacketDescription.mStartOffset)
|
||||
let audioPacketSize = Int(audioPacketDescription.mDataByteSize)
|
||||
let audioPacketData = Data(bytes: streamData.advanced(by: audioPacketStart), count: audioPacketSize)
|
||||
selfAudioParser.append(description: audioPacketDescription, data: audioPacketData)
|
||||
}
|
||||
} else { // not compressed audio (.wav)
|
||||
Log.debug("uncompressed audio")
|
||||
let format = fileAudioFormat.streamDescription.pointee
|
||||
let bytesPerAudioPacket = Int(format.mBytesPerPacket)
|
||||
for i in 0 ..< Int(packetCount) {
|
||||
let audioPacketStart = i * bytesPerAudioPacket
|
||||
let audioPacketSize = bytesPerAudioPacket
|
||||
let audioPacketData = Data(bytes: streamData.advanced(by: audioPacketStart), count: audioPacketSize)
|
||||
selfAudioParser.append(description: nil, data: audioPacketData)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// AudioParserPropertyListener.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// 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 is under the name Credited_LICENSE.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
|
||||
func ParserPropertyListener(_ context: UnsafeMutableRawPointer, _ streamId: AudioFileStreamID, _ propertyId: AudioFileStreamPropertyID, _ flags: UnsafeMutablePointer<AudioFileStreamPropertyFlags>) {
|
||||
let selfAudioParser = Unmanaged<AudioParser>.fromOpaque(context).takeUnretainedValue()
|
||||
|
||||
Log.info("audio file stream property: \(propertyId.description)")
|
||||
switch propertyId {
|
||||
case kAudioFileStreamProperty_DataFormat:
|
||||
var fileAudioFormat = AudioStreamBasicDescription()
|
||||
GetPropertyValue(&fileAudioFormat, streamId, propertyId)
|
||||
selfAudioParser.fileAudioFormat = AVAudioFormat(streamDescription: &fileAudioFormat)
|
||||
break
|
||||
case kAudioFileStreamProperty_AudioDataPacketCount:
|
||||
GetPropertyValue(&selfAudioParser.parsedAudioHeaderPacketCount, streamId, propertyId)
|
||||
break
|
||||
case kAudioFileStreamProperty_AudioDataByteCount:
|
||||
GetPropertyValue(&selfAudioParser.parsedAudioPacketDataSize, streamId, propertyId)
|
||||
selfAudioParser.expectedFileSizeInBytes = selfAudioParser.parsedAudioDataOffset + selfAudioParser.parsedAudioPacketDataSize
|
||||
break;
|
||||
case kAudioFileStreamProperty_DataOffset:
|
||||
GetPropertyValue(&selfAudioParser.parsedAudioDataOffset, streamId, propertyId)
|
||||
|
||||
if(selfAudioParser.parsedAudioPacketDataSize != 0) {
|
||||
selfAudioParser.expectedFileSizeInBytes = selfAudioParser.parsedAudioDataOffset + selfAudioParser.parsedAudioPacketDataSize
|
||||
}
|
||||
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//property is like the medatada of
|
||||
func GetPropertyValue<T>(_ value: inout T, _ streamId: AudioFileStreamID, _ propertyId: AudioFileStreamPropertyID) {
|
||||
var propertySize: UInt32 = 0
|
||||
guard AudioFileStreamGetPropertyInfo(streamId, propertyId, &propertySize, nil) == noErr else {//try to get the size of the property
|
||||
Log.monitor("failed to get info for property:\(propertyId.description)")
|
||||
return
|
||||
}
|
||||
|
||||
guard AudioFileStreamGetProperty(streamId, propertyId, &propertySize, &value) == noErr else {
|
||||
Log.monitor("failed to get propery value for: \(propertyId.description)")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// SAAudioAvailabilityRange.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-02-18.
|
||||
// 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
|
||||
|
||||
//Think of it as the grey buffer line from youtube
|
||||
public struct SAAudioAvailabilityRange {
|
||||
let startingNeedle: Needle
|
||||
let durationLoadedByNetwork: Duration
|
||||
let predictedDurationToLoad: Duration
|
||||
let isPlayable: Bool
|
||||
|
||||
public var bufferingProgress: Double {
|
||||
get {
|
||||
return (startingNeedle + durationLoadedByNetwork) / predictedDurationToLoad
|
||||
}
|
||||
}
|
||||
|
||||
public var startingBufferTimePositon: Double {
|
||||
get {
|
||||
return startingNeedle
|
||||
}
|
||||
}
|
||||
|
||||
public var totalDurationBuffered: Double {
|
||||
get {
|
||||
return durationLoadedByNetwork
|
||||
}
|
||||
}
|
||||
|
||||
public var isReadyForPlaying: Bool {
|
||||
get {
|
||||
return isPlayable
|
||||
}
|
||||
}
|
||||
|
||||
var secondsLeftToBuffer: Double {
|
||||
get {
|
||||
return predictedDurationToLoad - (startingNeedle + durationLoadedByNetwork)
|
||||
}
|
||||
}
|
||||
|
||||
public func contains(_ needle: Double) -> Bool {
|
||||
return needle >= startingNeedle && (needle - startingNeedle) < durationLoadedByNetwork
|
||||
}
|
||||
|
||||
public func reachedEndOfAudio(needle: Double) -> Bool {
|
||||
var needleAtEnd = false
|
||||
|
||||
if(totalDurationBuffered > 0 && needle > 0) {
|
||||
needleAtEnd = needle >= totalDurationBuffered - 1
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
let isBuffered = (bufferingProgress > 0.99 || secondsLeftToBuffer < 5)
|
||||
|
||||
return isBuffered && needleAtEnd
|
||||
}
|
||||
|
||||
public func isCompletelyBuffered() -> Bool {
|
||||
return startingNeedle + durationLoadedByNetwork >= predictedDurationToLoad
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// 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
|
||||
case ended
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// LockScreenViewProtocol.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
import MediaPlayer
|
||||
import UIKit
|
||||
|
||||
// MARK: - Set up lockscreen audio controls
|
||||
// Documentation: https://developer.apple.com/documentation/avfoundation/media_assets_playback_and_editing/creating_a_basic_video_player_ios_and_tvos/controlling_background_audio
|
||||
protocol LockScreenViewProtocol {
|
||||
var skipForwardSeconds: Double { get set }
|
||||
var skipBackwardSeconds: Double { get set }
|
||||
}
|
||||
|
||||
extension LockScreenViewProtocol {
|
||||
func clearLockScreenInfo() {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo?, duration: Duration) {
|
||||
var nowPlayingInfo:[String : Any] = [:]
|
||||
|
||||
guard let info = info else {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
||||
return
|
||||
}
|
||||
|
||||
let title = info.title
|
||||
let artist = info.artist
|
||||
let releaseDate = info.releaseDate
|
||||
|
||||
// For some reason we need to set a duration here for the needle?
|
||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = NSNumber(floatLiteral: duration)
|
||||
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = title
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = artist
|
||||
//nowPlayingInfo[MPMediaItemPropertyGenre] = //maybe later when we have it
|
||||
//nowPlayingInfo[MPMediaItemPropertyIsExplicit] = //maybe later when we have it
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = artist
|
||||
nowPlayingInfo[MPMediaItemPropertyMediaType] = MPMediaType.podcast.rawValue
|
||||
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))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/36754934/update-mpremotecommandcenter-play-pause-button
|
||||
func setLockScreenControls(presenter: SAPlayerPresenter) { //FIXME: this is weird
|
||||
// Get the shared MPRemoteCommandCenter
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
|
||||
// Add handler for Play Command
|
||||
commandCenter.playCommand.addTarget { [weak presenter] event in
|
||||
guard let presenter = presenter else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
if !presenter.getIsPlaying() {
|
||||
presenter.handlePlay()
|
||||
return .success
|
||||
}
|
||||
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
// Add handler for Pause Command
|
||||
commandCenter.pauseCommand.addTarget { [weak presenter] event in
|
||||
guard let presenter = presenter else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
if presenter.getIsPlaying() {
|
||||
presenter.handlePause()
|
||||
return .success
|
||||
}
|
||||
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
commandCenter.skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
|
||||
commandCenter.skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
|
||||
|
||||
commandCenter.skipBackwardCommand.addTarget { [weak presenter] event in
|
||||
guard let presenter = presenter else {
|
||||
return .commandFailed
|
||||
}
|
||||
presenter.handleSkipBackward()
|
||||
return .success
|
||||
}
|
||||
|
||||
commandCenter.skipForwardCommand.addTarget { [weak presenter] event in
|
||||
guard let presenter = presenter else {
|
||||
return .commandFailed
|
||||
}
|
||||
presenter.handleSkipForward()
|
||||
return .success
|
||||
}
|
||||
|
||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak presenter] event in
|
||||
guard let presenter = presenter else {
|
||||
return .commandFailed
|
||||
}
|
||||
if let positionEvent = event as? MPChangePlaybackPositionCommandEvent {
|
||||
presenter.handleSeek(toNeedle: Needle(positionEvent.positionTime))
|
||||
return .success
|
||||
}
|
||||
|
||||
return .commandFailed
|
||||
}
|
||||
}
|
||||
|
||||
func updateLockscreenElapsedTime(needle: Needle) {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: Double(needle))
|
||||
}
|
||||
|
||||
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: Float){
|
||||
if speed > 0.0{
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = speed
|
||||
}
|
||||
}
|
||||
|
||||
func updateLockscreenSkipIntervals() {
|
||||
MPRemoteCommandCenter.shared().skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
|
||||
MPRemoteCommandCenter.shared().skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// AudioDataManager.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
protocol AudioDataManagable {
|
||||
var numberOfQueued: Int { get }
|
||||
var numberOfActive: Int { get }
|
||||
|
||||
var allowCellular: Bool { get set }
|
||||
|
||||
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ())
|
||||
func setAllowCellularDownloadPreference(_ preference: Bool)
|
||||
|
||||
func clear()
|
||||
|
||||
//Director pattern
|
||||
func attach(callback: @escaping (_ id: ID, _ progress: Double)->())
|
||||
|
||||
func startStream(withRemoteURL url: AudioURL, callback: @escaping (StreamProgressPTO) -> ()) //called by throttler
|
||||
func pauseStream(withRemoteURL url: AudioURL)
|
||||
func resumeStream(withRemoteURL url: AudioURL)
|
||||
func seekStream(withRemoteURL url: AudioURL, toByteOffset offset: UInt64)
|
||||
func deleteStream(withRemoteURL url: AudioURL)
|
||||
|
||||
func getPersistedUrl(withRemoteURL url: AudioURL) -> URL?
|
||||
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL) -> ())
|
||||
func cancelDownload(withRemoteURL url: AudioURL)
|
||||
func deleteDownload(withLocalURL url: URL)
|
||||
}
|
||||
|
||||
class AudioDataManager: AudioDataManagable {
|
||||
var allowCellular: Bool = true
|
||||
|
||||
static let shared: AudioDataManagable = AudioDataManager()
|
||||
|
||||
// When we're streaming we want to stagger the size of data push up from disk to prevent the phone from freezing. We push up data of this chunk size every couple milliseconds.
|
||||
private let MAXIMUM_DATA_SIZE_TO_PUSH = 37744
|
||||
private let TIME_IN_BETWEEN_STREAM_DATA_PUSH = 198
|
||||
|
||||
var backgroundCompletion: ()-> Void = {} // set by AppDelegate
|
||||
|
||||
//This is the first case where a DAO passes a closure to a singleon that receives delegate calls from the OS. When the delegate from the OS is called, this class calls the DAO's closure. We pretty much set up a stream from the delegate call to the director (and all the items subscribed to that director)
|
||||
private var globalDownloadProgressCallback: (String, Double)-> Void = {_,_ in }
|
||||
|
||||
private var downloadWorker: AudioDataDownloadable!
|
||||
private var streamWorker: AudioDataStreamable!
|
||||
|
||||
private var streamingCallbacks = [(ID, (StreamProgressPTO)->())]()
|
||||
|
||||
private var originalDataCountForDownloadedAudio = 0
|
||||
|
||||
var numberOfQueued: Int {
|
||||
return downloadWorker.numberOfQueued
|
||||
}
|
||||
|
||||
var numberOfActive: Int {
|
||||
return downloadWorker.numberOfActive
|
||||
}
|
||||
|
||||
private init() {
|
||||
downloadWorker = AudioDownloadWorker(
|
||||
allowCellular: allowCellular,
|
||||
progressCallback: downloadProgressListener,
|
||||
doneCallback: downloadDoneListener,
|
||||
backgroundDownloadCallback: backgroundCompletion)
|
||||
|
||||
streamWorker = AudioStreamWorker(
|
||||
progressCallback: streamProgressListener,
|
||||
doneCallback: streamDoneListener)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
streamingCallbacks = []
|
||||
}
|
||||
|
||||
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
|
||||
backgroundCompletion = completionHandler
|
||||
}
|
||||
|
||||
func setAllowCellularDownloadPreference(_ preference: Bool) {
|
||||
allowCellular = preference
|
||||
}
|
||||
|
||||
func attach(callback: @escaping (_ id: ID, _ progress: Double)->()) {
|
||||
globalDownloadProgressCallback = callback
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- Streaming
|
||||
extension AudioDataManager {
|
||||
func startStream(withRemoteURL url: AudioURL, callback: @escaping (StreamProgressPTO) -> ()) {
|
||||
if let data = FileStorage.Audio.read(url.key) {
|
||||
let dto = StreamProgressDTO.init(progress: 1.0, data: data, totalBytesExpected: Int64(data.count))
|
||||
callback(StreamProgressPTO(dto: dto))
|
||||
return
|
||||
}
|
||||
|
||||
let exists = streamingCallbacks.contains { (cb: (ID, (StreamProgressPTO) -> ())) -> Bool in
|
||||
return cb.0 == url.key
|
||||
}
|
||||
|
||||
if !exists {
|
||||
streamingCallbacks.append((url.key, callback))
|
||||
}
|
||||
|
||||
downloadWorker.stop(withID: url.key) { [weak self] (fetchedData: Data?, totalBytesExpected: Int64?) in
|
||||
self?.downloadWorker.pauseAllActive()
|
||||
self?.streamWorker.start(withID: url.key, withRemoteURL: url, withInitialData: fetchedData, andTotalBytesExpectedPreviously: totalBytesExpected)
|
||||
}
|
||||
}
|
||||
|
||||
func pauseStream(withRemoteURL url: AudioURL) {
|
||||
guard streamWorker.getRunningID() == url.key else { return }
|
||||
streamWorker.pause(withId: url.key)
|
||||
}
|
||||
|
||||
func resumeStream(withRemoteURL url: AudioURL) {
|
||||
streamWorker.resume(withId: url.key)
|
||||
}
|
||||
func seekStream(withRemoteURL url: AudioURL, toByteOffset offset: UInt64) {
|
||||
streamWorker.seek(withId: url.key, withByteOffset: offset)
|
||||
}
|
||||
|
||||
func deleteStream(withRemoteURL url: AudioURL) {
|
||||
streamWorker.stop(withId: url.key)
|
||||
streamingCallbacks.removeAll { (cb: (ID, (StreamProgressPTO) -> ())) -> Bool in
|
||||
return cb.0 == url.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- Download
|
||||
extension AudioDataManager {
|
||||
func getPersistedUrl(withRemoteURL url: AudioURL) -> URL? {
|
||||
return FileStorage.Audio.locate(url.key)
|
||||
}
|
||||
|
||||
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL) -> ()) {
|
||||
let key = url.key
|
||||
|
||||
if let savedUrl = FileStorage.Audio.locate(key), FileStorage.Audio.isStored(key) {
|
||||
globalDownloadProgressCallback(key, 1.0)
|
||||
completion(savedUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if let currentProgress = downloadWorker.getProgressOfDownload(withID: key) {
|
||||
globalDownloadProgressCallback(key, currentProgress)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: check if we already streaming and convert streaming to download when we have persistent play button
|
||||
guard streamWorker.getRunningID() != key else {
|
||||
Log.debug("already streaming audio, don't need to download key: \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
downloadWorker.start(withID: key, withRemoteUrl: url, completion: completion)
|
||||
}
|
||||
|
||||
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
|
||||
extension AudioDataManager {
|
||||
private func downloadProgressListener(id: ID, progress: Double) {
|
||||
globalDownloadProgressCallback(id, progress)
|
||||
}
|
||||
|
||||
private func streamProgressListener(id: ID, dto: StreamProgressDTO) {
|
||||
for c in streamingCallbacks {
|
||||
if c.0 == id {
|
||||
c.1(StreamProgressPTO(dto: dto))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadDoneListener(id: ID, error: Error?) {
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
globalDownloadProgressCallback(id, 1.0)
|
||||
}
|
||||
|
||||
private func streamDoneListener(id: ID, error: Error?) -> Bool {
|
||||
if error != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
downloadWorker.resumeAllActive()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// AudioQueue.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Joe Williams on 3/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// wrapper for array of urls
|
||||
struct AudioQueue<T> {
|
||||
private var audioUrls: [T] = []
|
||||
|
||||
var isQueueEmpty: Bool {
|
||||
return audioUrls.isEmpty
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return audioUrls.count
|
||||
}
|
||||
|
||||
var front: T? {
|
||||
return audioUrls.first
|
||||
}
|
||||
|
||||
mutating func append(item: T) {
|
||||
audioUrls.append(item)
|
||||
}
|
||||
|
||||
mutating func dequeue() -> T? {
|
||||
guard !isQueueEmpty else { return nil }
|
||||
return audioUrls.removeFirst()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
//
|
||||
// AudioDownloadWorker.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
protocol AudioDataDownloadable: AnyObject {
|
||||
init(allowCellular: Bool, progressCallback: @escaping (_ id: ID, _ progress: Double)->(), doneCallback: @escaping (_ id: ID, _ error: Error?)->(), backgroundDownloadCallback: @escaping ()->())
|
||||
|
||||
var numberOfActive: Int { get }
|
||||
var numberOfQueued: Int { get }
|
||||
|
||||
func getProgressOfDownload(withID id: ID) -> Double?
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
class AudioDownloadWorker: NSObject, AudioDataDownloadable {
|
||||
private let MAX_CONCURRENT_DOWNLOADS = 3
|
||||
|
||||
// Given by the AppDelegate
|
||||
private let backgroundCompletion: () -> ()
|
||||
|
||||
private let progressHandler: (ID, Double) -> ()
|
||||
private let completionHandler: (ID, Error?) -> ()
|
||||
|
||||
private let allowsCellularDownload: Bool
|
||||
private lazy var session: URLSession = {
|
||||
let config = URLSessionConfiguration.background(withIdentifier: "SwiftAudioPlayer.background_downloader_\(Date.getUTC())")
|
||||
config.isDiscretionary = !allowsCellularDownload
|
||||
config.sessionSendsLaunchEvents = true
|
||||
config.allowsCellularAccess = allowsCellularDownload
|
||||
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
||||
}()
|
||||
|
||||
private var activeDownloads: [ActiveDownload] = []
|
||||
private var queuedDownloads = Set<DownloadInfo>()
|
||||
|
||||
var numberOfActive: Int {
|
||||
return activeDownloads.count
|
||||
}
|
||||
|
||||
var numberOfQueued: Int {
|
||||
return queuedDownloads.count
|
||||
}
|
||||
|
||||
required init(allowCellular: Bool,
|
||||
progressCallback: @escaping (_ id: ID, _ progress: Double)->(),
|
||||
doneCallback: @escaping (_ id: ID, _ error: Error?)->(),
|
||||
backgroundDownloadCallback: @escaping ()->()) {
|
||||
Log.info("init with allowCellular: \(allowCellular)")
|
||||
self.progressHandler = progressCallback
|
||||
self.completionHandler = doneCallback
|
||||
self.backgroundCompletion = backgroundDownloadCallback
|
||||
self.allowsCellularDownload = allowCellular
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func getProgressOfDownload(withID id: ID) -> Double? {
|
||||
return activeDownloads.filter { $0.info.id == id }.first?.progress
|
||||
}
|
||||
|
||||
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 info = queuedDownloads.updatePreservingOldCompletionHandlers(withID: id, withRemoteUrl: remoteUrl, completion: completion)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
guard numberOfActive < MAX_CONCURRENT_DOWNLOADS else {
|
||||
_ = queuedDownloads.updatePreservingOldCompletionHandlers(withID: info.id, withRemoteUrl: info.remoteUrl)
|
||||
return
|
||||
}
|
||||
|
||||
queuedDownloads.remove(info)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func pauseAllActive() {
|
||||
Log.info("activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
|
||||
for download in activeDownloads {
|
||||
if download.task.state == .running {
|
||||
download.task.suspend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resumeAllActive() {
|
||||
Log.info("activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
|
||||
for download in activeDownloads {
|
||||
download.task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func stop(withID id: ID, callback: ((_ dataSoFar: Data?, _ totalBytesExpected: Int64?) -> ())?) {
|
||||
Log.info("paramId: \(id), activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
|
||||
for download in activeDownloads {
|
||||
if download.info.id == id && download.task.state == .running {
|
||||
download.task.cancel { (data: Data?) in
|
||||
callback?(nil, nil)
|
||||
// Could not achieve this because this resume data isn't actually the data downloaded so far but instead metadata. Not sure how to get the actual data that download task is downloading
|
||||
// callback?(data, download.totalBytesExpected)
|
||||
}
|
||||
activeDownloads = activeDownloads.filter { $0.info.id != id }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
queuedDownloads.remove(withMatchingId: id)
|
||||
callback?(nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioDownloadWorker: URLSessionDownloadDelegate {
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
let activeTask = activeDownloads.filter { $0.task == downloadTask }.first
|
||||
|
||||
guard let task = activeTask else {
|
||||
Log.monitor("could not find corresponding active download task when done downloading: \(downloadTask.currentRequest?.url?.absoluteString ?? "nil url")")
|
||||
return
|
||||
}
|
||||
|
||||
guard let fileType = downloadTask.response?.suggestedFilename?.pathExtension else {
|
||||
Log.monitor("No file type exists for file from downloading.. id: \(downloadTask.taskDescription ?? "nil") :: url: \(task.info.remoteUrl) where it suggested filename: \(downloadTask.response?.suggestedFilename ?? "nil")")
|
||||
return
|
||||
}
|
||||
|
||||
let destinationUrl = FileStorage.Audio.getUrl(givenId: task.info.id, andFileExtension: fileType)
|
||||
Log.info("Writing download file with id: \(task.info.id) to file named: \(destinationUrl.lastPathComponent)")
|
||||
|
||||
// https://stackoverflow.com/questions/20251432/cant-move-file-after-background-download-no-such-file
|
||||
// Apparently, the data of the temporary location get deleted outside of this function immediately, so others recommended extracting the data and writing it, this is why I'm not using DiskUtil
|
||||
do {
|
||||
_ = try FileManager.default.replaceItemAt(destinationUrl, withItemAt: location)
|
||||
|
||||
Log.info("Successful write file to url: \(destinationUrl.absoluteString)")
|
||||
progressHandler(task.info.id, 1.0)
|
||||
} catch {
|
||||
if (error as NSError).code == NSFileWriteFileExistsError {
|
||||
do {
|
||||
Log.info("File already existed at attempted download url: \(destinationUrl.absoluteString)")
|
||||
try FileManager.default.removeItem(at: destinationUrl)
|
||||
_ = try FileManager.default.replaceItemAt(destinationUrl, withItemAt: location)
|
||||
Log.info("Replaced previous file at url: \(destinationUrl.absoluteString)")
|
||||
} catch {
|
||||
Log.monitor("Error moving file after download for task id: \(task.info.id) and error: \(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
Log.monitor("Error moving file after download for task id: \(task.info.id) and error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(task.info.id, nil)
|
||||
|
||||
for handler in task.info.completionHandlers {
|
||||
handler(destinationUrl)
|
||||
}
|
||||
|
||||
activeDownloads = activeDownloads.filter { $0 != task }
|
||||
|
||||
if let queued = queuedDownloads.popHighestRanked() {
|
||||
start(withInfo: queued)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
|
||||
DispatchQueue.main.async {
|
||||
self.backgroundCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if let e = error {
|
||||
if let err: NSError = error as NSError? {
|
||||
if err.domain == NSURLErrorDomain && err.code == NSURLErrorCancelled {
|
||||
Log.info("cancelled downloading")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let err: NSError = error as NSError? {
|
||||
if err.domain == NSPOSIXErrorDomain && err.code == 2 {
|
||||
Log.error("download error where file says it doesn't exist, this could be because of bad network")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for download in activeDownloads {
|
||||
if download.task == task {
|
||||
completionHandler(download.info.id, e)
|
||||
activeDownloads = activeDownloads.filter { $0.task != task }
|
||||
}
|
||||
}
|
||||
|
||||
Log.monitor("\(task.currentRequest?.url?.absoluteString ?? "nil url") error: \(e.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
var found: Bool = false
|
||||
|
||||
for download in activeDownloads {
|
||||
if download.task == downloadTask {
|
||||
found = true
|
||||
download.progress = Double(totalBytesWritten)/Double(totalBytesExpectedToWrite)
|
||||
download.totalBytesExpected = totalBytesExpectedToWrite
|
||||
if download.progress != 1.0 {
|
||||
progressHandler(download.info.id, download.progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
Log.monitor("could not find active download when receiving progress updates")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- Helpers
|
||||
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
|
||||
}
|
||||
|
||||
let id: ID
|
||||
let remoteUrl: URL
|
||||
let rank: Int
|
||||
var completionHandlers: [(URL) -> ()]
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
hasher.combine(remoteUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private class ActiveDownload: Hashable {
|
||||
static func == (lhs: AudioDownloadWorker.ActiveDownload, rhs: AudioDownloadWorker.ActiveDownload) -> Bool {
|
||||
return lhs.info.id == rhs.info.id
|
||||
}
|
||||
|
||||
let info: DownloadInfo
|
||||
var totalBytesExpected: Int64?
|
||||
var progress: Double = 0.0
|
||||
let task: URLSessionDownloadTask
|
||||
|
||||
init(info: DownloadInfo, task: URLSessionDownloadTask) {
|
||||
self.info = info
|
||||
self.task = task
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(info.id)
|
||||
hasher.combine(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Set where Element == AudioDownloadWorker.DownloadInfo {
|
||||
mutating func popHighestRanked() -> AudioDownloadWorker.DownloadInfo? {
|
||||
guard self.count > 0 else { return nil }
|
||||
|
||||
var ret: AudioDownloadWorker.DownloadInfo = self.first!
|
||||
|
||||
for info in self {
|
||||
if info.rank > ret.rank {
|
||||
ret = info
|
||||
}
|
||||
}
|
||||
|
||||
self.remove(ret)
|
||||
|
||||
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 {
|
||||
var pathExtension: String? {
|
||||
let cleaned = self.replacingOccurrences(of: " ", with: "_")
|
||||
let ext = URL(string: cleaned)?.pathExtension
|
||||
return ext == "" ? nil : ext
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// FileStorage.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
/**
|
||||
Utility class to access audio files saved on the phone.
|
||||
*/
|
||||
struct FileStorage {
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
Generates a URL for a file that would be saved locally.
|
||||
|
||||
Note: It is not guaranteed that the file actually exists.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
static func delete(_ url: URL) {
|
||||
if !isStored(url) {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch let error {
|
||||
Log.error("Could not delete a file: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- Audio
|
||||
extension FileStorage {
|
||||
struct Audio {
|
||||
private static let directory: FileManager.SearchPathDirectory = .documentDirectory
|
||||
private init() {}
|
||||
|
||||
static func isStored(_ id: ID) -> Bool {
|
||||
guard let url = locate(id)?.path else {
|
||||
return false
|
||||
}
|
||||
|
||||
//FIXME: This is an unreliable API. Maybe use a map instead?
|
||||
return FileManager.default.fileExists(atPath: url)
|
||||
}
|
||||
|
||||
static func delete(_ id: ID) {
|
||||
guard let url = locate(id) else {
|
||||
Log.warn("trying to delete audio file that doesn't exist with id: \(id)")
|
||||
return
|
||||
}
|
||||
return FileStorage.delete(url)
|
||||
}
|
||||
|
||||
static func write(_ id: ID, fileExtension: String, data: Data) {
|
||||
do {
|
||||
let url = FileStorage.getUrl(givenAName: getAudioFileName(id, fileExtension: fileExtension), inDirectory: directory)
|
||||
try data.write(to: url)
|
||||
} catch {
|
||||
Log.monitor(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
static func read(_ id: ID) -> Data? {
|
||||
guard let url = locate(id) else {
|
||||
Log.debug("Trying to get data for audio file that doesn't exist: \(id)")
|
||||
return nil
|
||||
}
|
||||
let data = try? Data(contentsOf: url)
|
||||
return data
|
||||
}
|
||||
|
||||
static func locate(_ id: ID) -> URL? {
|
||||
let folderUrls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
||||
guard folderUrls.count != 0 else { return nil }
|
||||
|
||||
if let urls = try? FileManager.default.contentsOfDirectory(at: folderUrls[0], includingPropertiesForKeys: nil) {
|
||||
for url in urls {
|
||||
if url.absoluteString.contains(id) && url.pathExtension != "" {
|
||||
_ = getUrl(givenId: id, andFileExtension: url.pathExtension)
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func getUrl(givenId id: ID, andFileExtension fileExtension: String) -> URL {
|
||||
let url = FileStorage.getUrl(givenAName: getAudioFileName(id, fileExtension: fileExtension), inDirectory: directory)
|
||||
return url
|
||||
}
|
||||
|
||||
private static func getAudioFileName(_ id: ID, fileExtension: String) -> NameFile {
|
||||
return "\(id).\(fileExtension)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// StreamProgressPTO.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
|
||||
struct StreamProgressPTO {
|
||||
let dto: StreamProgressDTO
|
||||
|
||||
func getProgress() -> Double {
|
||||
return dto.progress
|
||||
}
|
||||
|
||||
func getData() -> Data {
|
||||
return dto.data
|
||||
}
|
||||
|
||||
func getTotalBytesExpected() -> Int64? {
|
||||
return dto.totalBytesExpected
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
//
|
||||
// AudioStreamWorker.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
/**
|
||||
init task
|
||||
+
|
||||
|
|
||||
|
|
||||
+-----v-----+ suspend() +---------+ +-----------+
|
||||
| suspended <-----------------> running +----------> completed |
|
||||
+-----+-----+ resume() +----+----+ +-----------+
|
||||
| |
|
||||
| | cancel()
|
||||
| |
|
||||
| cancel() +------v------+
|
||||
+---------------------> cancelling |
|
||||
+-------------+
|
||||
*/
|
||||
|
||||
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
|
||||
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data?, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64?)
|
||||
func pause(withId id: ID)
|
||||
func resume(withId id: ID)
|
||||
func stop(withId id: ID)//FIXME: with persistent play we should return a Data so that download can resume
|
||||
func seek(withId id: ID, withByteOffset offset: UInt64)
|
||||
func getRunningID() -> ID?
|
||||
}
|
||||
|
||||
///Policy for streaming
|
||||
///- only one stream at a time
|
||||
///- starting a stream will cancel the previous
|
||||
///- when seeking, assume that previous data is discarded
|
||||
class AudioStreamWorker:NSObject, AudioDataStreamable {
|
||||
private let TIMEOUT = 60.0
|
||||
|
||||
fileprivate let progressCallback: (_ id: ID, _ dto: StreamProgressDTO) -> ()
|
||||
//Will ony be called when the task object will no longer be active
|
||||
//Why? So upper layer knows that current streaming activity for this ID is done
|
||||
//Why? To know if we should persist the stream data assuming successful completion
|
||||
fileprivate let doneCallback: (_ id: ID, _ error: Error?) -> Bool
|
||||
private var session: URLSession!
|
||||
|
||||
private var id: ID?
|
||||
private var url: URL?
|
||||
private var task: URLSessionDataTask?
|
||||
private var previousTotalBytesExpectedFromInitalData: Int64?
|
||||
private var initialDataBytesCount: Int64 = 0
|
||||
fileprivate var totalBytesExpectedForWholeFile: Int64?
|
||||
fileprivate var totalBytesExpectedForCurrentStream: Int64?
|
||||
fileprivate var totalBytesReceived: Int64 = 0
|
||||
private var corruptedBecauseOfSeek = false
|
||||
|
||||
|
||||
/// Init
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - progressCallback: generic callback
|
||||
/// - doneCallback: when finished
|
||||
required init(progressCallback: @escaping (_ id: ID, _ dto: StreamProgressDTO) -> (), doneCallback: @escaping (_ id: ID, _ error: Error?) -> Bool) {
|
||||
self.progressCallback = progressCallback
|
||||
self.doneCallback = doneCallback
|
||||
super.init()
|
||||
|
||||
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, *) {
|
||||
config.waitsForConnectivity = true
|
||||
}
|
||||
self.session = URLSession(configuration: config, delegate: self, delegateQueue: nil) //TODO: should we use ephemeral
|
||||
}
|
||||
|
||||
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data? = nil, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64? = nil) {
|
||||
Log.info("selfID: \(self.id ?? "none"), paramID: \(id) initialData: \(data?.count ?? 0)")
|
||||
|
||||
killPreviousTaskIfNeeded()
|
||||
self.id = id
|
||||
self.url = url
|
||||
self.previousTotalBytesExpectedFromInitalData = previousTotalBytesExpected
|
||||
|
||||
if let data = data {
|
||||
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: TIMEOUT)
|
||||
request.addValue("bytes=\(data.count)-", forHTTPHeaderField: "Range")
|
||||
task = session.dataTask(with: request)
|
||||
task?.taskDescription = id
|
||||
|
||||
initialDataBytesCount = Int64(data.count)
|
||||
totalBytesReceived = initialDataBytesCount
|
||||
totalBytesExpectedForWholeFile = previousTotalBytesExpected
|
||||
|
||||
let progress = previousTotalBytesExpected != nil ? Double(initialDataBytesCount)/Double(previousTotalBytesExpected!) : 0
|
||||
|
||||
let dto = StreamProgressDTO(progress: progress, data: data, totalBytesExpected: totalBytesExpectedForWholeFile)
|
||||
|
||||
progressCallback(id, dto)
|
||||
|
||||
task?.resume()
|
||||
} else {
|
||||
task = session.dataTask(with: url)
|
||||
task?.resume()
|
||||
|
||||
task?.taskDescription = id
|
||||
}
|
||||
}
|
||||
|
||||
private func killPreviousTaskIfNeeded() {
|
||||
guard let task = task else {return}
|
||||
if task.state == .running || task.state == .suspended {
|
||||
task.cancel()
|
||||
}
|
||||
self.task = nil
|
||||
corruptedBecauseOfSeek = false
|
||||
totalBytesExpectedForWholeFile = nil
|
||||
totalBytesReceived = 0
|
||||
initialDataBytesCount = 0
|
||||
}
|
||||
|
||||
func pause(withId id: ID) {
|
||||
Log.info("selfID: \(self.id ?? "none"), paramID: \(id)")
|
||||
guard self.id == id else {
|
||||
Log.error("incorrect ID for command")
|
||||
return
|
||||
}
|
||||
|
||||
guard let task = task else {
|
||||
Log.error("tried to stop a non-existent task")
|
||||
return
|
||||
}
|
||||
|
||||
if task.state == .running {
|
||||
task.suspend()
|
||||
} else {
|
||||
Log.monitor("tried to pause a task that's already suspended")
|
||||
}
|
||||
}
|
||||
|
||||
func resume(withId id: ID) {
|
||||
Log.info("selfID: \(self.id ?? "none"), paramID: \(id)")
|
||||
guard self.id == id else {
|
||||
Log.error("incorrect ID for command")
|
||||
return
|
||||
}
|
||||
|
||||
guard let task = task else {
|
||||
Log.error("tried to resume a non-existent task")
|
||||
return
|
||||
}
|
||||
|
||||
if task.state == .suspended {
|
||||
task.resume()
|
||||
} else {
|
||||
Log.monitor("tried to resume a non-suspended task")
|
||||
}
|
||||
}
|
||||
|
||||
func stop(withId id: ID) {
|
||||
Log.info("selfID: \(self.id ?? "none"), paramID: \(id)")
|
||||
guard self.id == id else {
|
||||
Log.warn("incorrect ID for command")
|
||||
return
|
||||
}
|
||||
|
||||
guard let task = task else {
|
||||
Log.error("tried to stop a non-existent task")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if task.state == .running || task.state == .suspended {
|
||||
task.cancel()
|
||||
self.task = nil
|
||||
} else {
|
||||
Log.error("stream_error tried to stop a task that's in state: \(task.state.rawValue)")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func seek(withId id: ID, withByteOffset offset: UInt64) {
|
||||
Log.info("selfID: \(self.id ?? "none"), paramID: \(id), offset: \(offset)")
|
||||
guard self.id == id else {
|
||||
Log.error("incorrect ID for command")
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = url else {
|
||||
Log.monitor("tried to seek without having URL")
|
||||
return
|
||||
}
|
||||
stop(withId: id)
|
||||
totalBytesReceived = 0
|
||||
corruptedBecauseOfSeek = true
|
||||
self.progressCallback(id, StreamProgressDTO(progress: 0, data: Data(), totalBytesExpected: totalBytesExpectedForWholeFile))
|
||||
|
||||
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: TIMEOUT)
|
||||
request.addValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
task = session.dataTask(with: request)
|
||||
task?.resume()
|
||||
}
|
||||
|
||||
|
||||
func getRunningID() -> ID? {
|
||||
if let task = task, task.state == .running, let id = id {
|
||||
return id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//MARK:- URLSessionDataDelegate
|
||||
extension AudioStreamWorker: URLSessionDataDelegate {
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
Log.debug("selfID: ", id, " dataTaskID: ", dataTask.taskDescription, " dataSize: ", data.count, " expected: ", totalBytesExpectedForWholeFile, " received: ", totalBytesReceived)
|
||||
guard let id = id else {
|
||||
//FIXME: should be an error when done with testing phase
|
||||
Log.monitor("stream worker in weird state 9847467")
|
||||
return
|
||||
}
|
||||
|
||||
guard self.task == dataTask else {
|
||||
Log.error("stream_error not the same task 638283") //Probably because of seek
|
||||
return
|
||||
}
|
||||
|
||||
guard var totalBytesExpected = totalBytesExpectedForCurrentStream else {
|
||||
Log.monitor("should not be called 223r2")
|
||||
return
|
||||
}
|
||||
|
||||
if totalBytesExpected <= 0 {
|
||||
totalBytesExpected = totalBytesReceived
|
||||
}
|
||||
|
||||
totalBytesReceived = totalBytesReceived + Int64(data.count)
|
||||
let progress = Double(totalBytesReceived)/Double(totalBytesExpected)
|
||||
|
||||
Log.debug("network streaming progress \(progress)")
|
||||
self.progressCallback(id, StreamProgressDTO(progress: progress, data: data, totalBytesExpected: totalBytesExpected))
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
Log.debug(dataTask.taskDescription, id, response.description)
|
||||
guard id != nil else {
|
||||
Log.monitor("stream worker in weird state 2049jg3")
|
||||
return
|
||||
}
|
||||
|
||||
guard self.task == dataTask else {
|
||||
Log.error("stream_error not the same task 517253")
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("response length: \(response.expectedContentLength)")
|
||||
|
||||
//the value will smaller if you seek. But we want to hold the OG total for duration calculations
|
||||
if !corruptedBecauseOfSeek {
|
||||
totalBytesExpectedForWholeFile = response.expectedContentLength + initialDataBytesCount
|
||||
}
|
||||
|
||||
totalBytesExpectedForCurrentStream = response.expectedContentLength
|
||||
completionHandler(.allow)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
Log.debug(task.taskDescription, id)
|
||||
guard let id = id else {
|
||||
Log.error("stream_error stream worker in weird state 345b45")
|
||||
return
|
||||
}
|
||||
|
||||
if self.task != task && self.task != nil {
|
||||
Log.error("stream_error not the same task 3901833")
|
||||
return
|
||||
}
|
||||
|
||||
if let err: NSError = error as NSError? {
|
||||
if err.domain == NSURLErrorDomain && err.code == NSURLErrorCancelled {
|
||||
Log.info("cancelled downloading")
|
||||
let _ = doneCallback(id, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err.domain == NSURLErrorDomain && err.code == NSURLErrorNetworkConnectionLost {
|
||||
Log.error("lost connection")
|
||||
let _ = doneCallback(id, nil)
|
||||
return
|
||||
}
|
||||
|
||||
Log.monitor("\(task.currentRequest?.url?.absoluteString ?? "nil url") error: \(err.localizedDescription)")
|
||||
|
||||
let _ = doneCallback(id, err)
|
||||
}
|
||||
|
||||
let shouldSave = doneCallback(id, nil)
|
||||
if shouldSave && !corruptedBecauseOfSeek {
|
||||
// TODO want to save file after streaming so we do not have to download again
|
||||
// guard (task.response?.suggestedFilename?.pathExtension) != nil else {
|
||||
// Log.monitor("Could not determine file type for file from id: \(task.taskDescription ?? "nil") and url: \(task.currentRequest?.url?.absoluteString ?? "nil")")
|
||||
// return
|
||||
// }
|
||||
|
||||
// TODO no longer saving streamed files
|
||||
// FileStorage.Audio.write(id, fileExtension: fileType, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
|
||||
// TODO: Notify to user that waiting for better connection
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// StreamProgressDTO.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
//Just a helper because it got too messy
|
||||
struct StreamProgressDTO {
|
||||
let progress: Double
|
||||
let data: Data
|
||||
let totalBytesExpected: Int64?
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// SALockScreenInfo.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-02-18.
|
||||
// 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
|
||||
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 releaseDate: UTC
|
||||
|
||||
public init(title: String, artist: String, artwork: UIImage?, releaseDate: UTC) {
|
||||
self.title = title
|
||||
self.artist = artist
|
||||
self.artwork = artwork
|
||||
self.releaseDate = releaseDate
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
//
|
||||
// SAPlayer.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
import AVFoundation
|
||||
|
||||
public class SAPlayer {
|
||||
public var DEBUG_MODE: Bool = false {
|
||||
didSet {
|
||||
if(DEBUG_MODE) {
|
||||
logLevel = LogLevel.EXTERNAL_DEBUG
|
||||
} else {
|
||||
logLevel = LogLevel.MONITOR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Access to the player.
|
||||
*/
|
||||
public static let shared: SAPlayer = SAPlayer()
|
||||
|
||||
private var presenter: SAPlayerPresenter!
|
||||
private var player: AudioEngine?
|
||||
|
||||
/**
|
||||
Access the engine of the player. Engine is nil if player has not been initialized with audio.
|
||||
|
||||
- Important: Changes to the engine are not safe guarded, thus unknown behaviour can arise from changing the engine. Just be wary and read [documentation of AVAudioEngine](https://developer.apple.com/documentation/avfoundation/avaudioengine) well when modifying,
|
||||
*/
|
||||
public var engine: AVAudioEngine? {
|
||||
get {
|
||||
return player?.engine
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Unique ID for the current engine. This will be nil if no audio has been initialized which means no engine exists.
|
||||
*/
|
||||
public var engineUID: String? {
|
||||
get {
|
||||
return player?.key
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Access the player node of the engine. Node is nil if player has not been initialized with audio.
|
||||
|
||||
- Important: Changes to the engine and this node are not safe guarded, thus unknown behaviour can arise from changing the engine or this node. Just be wary and read [documentation of AVAudioEngine](https://developer.apple.com/documentation/avfoundation/avaudioengine) well when modifying,
|
||||
*/
|
||||
public var playerNode: AVAudioPlayerNode? {
|
||||
get {
|
||||
return player?.playerNode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Corresponding to the overall volume of the player. Volume's default value is 1.0 and the range of valid values is 0.0 to 1.0. Volume is nil if no audio has been initialized yet.
|
||||
*/
|
||||
public var volume: Float? {
|
||||
get {
|
||||
return player?.playerNode.volume
|
||||
}
|
||||
|
||||
set {
|
||||
guard let value = newValue else { return }
|
||||
guard value >= 0.0 && value <= 1.0 else { return }
|
||||
|
||||
player?.playerNode.volume = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Corresponding to the rate of audio playback. This rate assumes use of the default rate modifier at the first index of `audioModifiers`; if you removed that modifier than this will be nil. If no audio has been initialized then this will also be nil.
|
||||
|
||||
- Note: By default this engine has added a pitch modifier node to change the pitch so that on playback rate changes of spoken word the pitch isn't shifted.
|
||||
|
||||
The component description of this node is:
|
||||
````
|
||||
var componentDescription: AudioComponentDescription {
|
||||
get {
|
||||
var ret = AudioComponentDescription()
|
||||
ret.componentType = kAudioUnitType_FormatConverter
|
||||
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
|
||||
return ret
|
||||
}
|
||||
}
|
||||
````
|
||||
Please look at [forums.developer.apple.com/thread/5874](https://forums.developer.apple.com/thread/5874) and [forums.developer.apple.com/thread/6050](https://forums.developer.apple.com/thread/6050) for more details.
|
||||
|
||||
For more details on pitch modifiers for playback rate changes please look at [developer.apple.com/forums/thread/6050](https://developer.apple.com/forums/thread/6050).
|
||||
*/
|
||||
public var rate: Float? {
|
||||
get {
|
||||
return (audioModifiers.first as? AVAudioUnitTimePitch)?.rate
|
||||
}
|
||||
|
||||
set {
|
||||
guard let value = newValue else { return }
|
||||
guard let node = audioModifiers.first as? AVAudioUnitTimePitch else { return }
|
||||
|
||||
node.rate = value
|
||||
playbackRateOfAudioChanged(rate: value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Corresponding to the skipping forward button on the media player on the lockscreen. Default is set to 30 seconds.
|
||||
*/
|
||||
public var skipForwardSeconds: Double = 30 {
|
||||
didSet {
|
||||
presenter.handleScrubbingIntervalsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Corresponding to the skipping backwards button on the media player on the lockscreen. Default is set to 15 seconds.
|
||||
*/
|
||||
public var skipBackwardSeconds: Double = 15 {
|
||||
didSet {
|
||||
presenter.handleScrubbingIntervalsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
List of [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers to pass to the engine on initialization.
|
||||
|
||||
- Important: To have the intended effects, the list of modifiers must be finalized before initializing the audio to be played. The modifers are added to the engine in order of the list.
|
||||
|
||||
- Note: The default list already has an AVAudioUnitTimePitch node first in the list. This node is specifically set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
|
||||
|
||||
The component description of this node is:
|
||||
````
|
||||
var componentDescription: AudioComponentDescription {
|
||||
get {
|
||||
var ret = AudioComponentDescription()
|
||||
ret.componentType = kAudioUnitType_FormatConverter
|
||||
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
|
||||
return ret
|
||||
}
|
||||
}
|
||||
````
|
||||
Please look at [forums.developer.apple.com/thread/5874](https://forums.developer.apple.com/thread/5874) and [forums.developer.apple.com/thread/6050](https://forums.developer.apple.com/thread/6050) for more details.
|
||||
|
||||
For more details on pitch modifiers for playback rate changes please look at [developer.apple.com/forums/thread/6050](https://developer.apple.com/forums/thread/6050).
|
||||
|
||||
To remove this default pitch modifier for playback rate changes, remove the node by calling `SAPlayer.shared.clearAudioModifiers()`.
|
||||
*/
|
||||
public var audioModifiers: [AVAudioUnit] = []
|
||||
|
||||
/**
|
||||
List of audio URLs queued for playback.
|
||||
*/
|
||||
public var audioQueued: [URL] {
|
||||
get {
|
||||
return presenter.audioQueue.map { (queued) -> URL in
|
||||
return queued.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Total duration of current audio initialized. Returns nil if no audio is initialized in player.
|
||||
|
||||
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, this value will be constantly updating to best known value at the time.
|
||||
*/
|
||||
public var duration: Double? {
|
||||
get {
|
||||
return presenter.duration
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A textual representation of the duration of the current audio initialized. Returns nil if no audio is initialized in player.
|
||||
*/
|
||||
public var prettyDuration: String? {
|
||||
get {
|
||||
guard let d = duration else { return nil }
|
||||
return SAPlayer.prettifyTimestamp(d)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Elapsed playback time of the current audio initialized. Returns nil if no audio is initialized in player.
|
||||
*/
|
||||
public var elapsedTime: Double? {
|
||||
get {
|
||||
return presenter.needle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A textual representation of the elapsed playback time of the current audio initialized. Returns nil if no audio is initialized in player.
|
||||
*/
|
||||
public var prettyElapsedTime: String? {
|
||||
get {
|
||||
guard let e = elapsedTime else { return nil }
|
||||
return SAPlayer.prettifyTimestamp(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Corresponding to the media info to display on the lockscreen for the current audio.
|
||||
|
||||
- Note: Setting this to nil clears the information displayed on the lockscreen media player.
|
||||
*/
|
||||
public var mediaInfo: SALockScreenInfo? = nil
|
||||
|
||||
private init() {
|
||||
presenter = SAPlayerPresenter(delegate: self)
|
||||
|
||||
// https://forums.developer.apple.com/thread/5874
|
||||
// https://forums.developer.apple.com/thread/6050
|
||||
// AVAudioTimePitchAlgorithm.timeDomain (just in case we want it)
|
||||
var componentDescription: AudioComponentDescription {
|
||||
get {
|
||||
var ret = AudioComponentDescription()
|
||||
ret.componentType = kAudioUnitType_FormatConverter
|
||||
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
audioModifiers.append(AVAudioUnitTimePitch(audioComponentDescription: componentDescription))
|
||||
}
|
||||
|
||||
/**
|
||||
Clears all [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) modifiers intended to be used for realtime audio manipulation.
|
||||
*/
|
||||
public func clearAudioModifiers() {
|
||||
audioModifiers.removeAll()
|
||||
}
|
||||
|
||||
/**
|
||||
Append an [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) modifier to the list of modifiers used for realtime audio manipulation. The modifier will be added to the end of the list.
|
||||
|
||||
- Parameter modifier: The modifier to append.
|
||||
*/
|
||||
public func addAudioModifier(_ modifer: AVAudioUnit) {
|
||||
audioModifiers.append(modifer)
|
||||
}
|
||||
|
||||
/**
|
||||
Formats a textual representation of a given timestamp for display in hh:MM:SS format, that is hours:minutes:seconds.
|
||||
|
||||
- Parameter timestamp: The timestamp to format.
|
||||
- Returns: A textual representation of the given timestamp
|
||||
*/
|
||||
public static func prettifyTimestamp(_ timestamp: Double) -> String {
|
||||
let hours = Int(timestamp / 60 / 60)
|
||||
let minutes = Int((timestamp - Double(hours * 60 * 60)) / 60)
|
||||
let secondsLeft = Int(timestamp - Double(hours * 60 * 60) - Double(minutes * 60))
|
||||
|
||||
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", secondsLeft))"
|
||||
}
|
||||
|
||||
func getUrl(forKey key: Key) -> URL? {
|
||||
return presenter.getUrl(forKey: key)
|
||||
}
|
||||
|
||||
func addUrlToMapping(url: URL) {
|
||||
presenter.addUrlToKeyMap(url)
|
||||
}
|
||||
}
|
||||
|
||||
public enum SAPlayerBitrate {
|
||||
/// This bitrate is good for radio streams that are passing ittle amounts of audio data at a time. This will allow the player to process the audio data in a fast enough rate to not pause or get stuck playing. This rate however ends up using more CPU and is worse for your battery-life and performance of your app.
|
||||
case low
|
||||
|
||||
/// This bitrate is good for streaming saved audio files like podcasts where most of the audio data will be received from the remote server at the beginning in a short time. This rate is more performant by using much less CPU and being better for your battery-life and app performance.
|
||||
case high // go for audio files being streamed. This is uses less CPU and
|
||||
}
|
||||
|
||||
//MARK: - External Player Controls
|
||||
extension SAPlayer {
|
||||
/**
|
||||
Toggles between the play and pause state of the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
|
||||
|
||||
- Note: If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
|
||||
*/
|
||||
public func togglePlayAndPause() {
|
||||
presenter.handleTogglePlayingAndPausing()
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to play the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
|
||||
|
||||
- Note: If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
|
||||
*/
|
||||
public func play() {
|
||||
presenter.handlePlay()
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to pause the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
|
||||
|
||||
- Note:If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
|
||||
*/
|
||||
public func pause() {
|
||||
presenter.handlePause()
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to skip forward in audio even if nothing playable is loaded (aka still in buffering state or no audio is initialized). The interval to which to skip forward is defined by `SAPlayer.shared.skipForwardSeconds`.
|
||||
|
||||
- Note: The skipping is limited to the duration of the audio, if the intended skip is past the duration of the current audio, the skip will just go to the end.
|
||||
*/
|
||||
public func skipForward() {
|
||||
presenter.handleSkipForward()
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to skip backwards in audio even if nothing playable is loaded (aka still in buffering state or no audio is initialized). The interval to which to skip backwards is defined by `SAPlayer.shared.skipBackwardSeconds`.
|
||||
|
||||
- Note: The skipping is limited to the playable timestamps, if the intended skip is below 0 seconds, the skip will just go to 0 seconds.
|
||||
*/
|
||||
public func skipBackwards() {
|
||||
presenter.handleSkipBackward()
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to seek/scrub through the audio even if nothing playable is loaded (aka still in buffering state or no audio is initialized).
|
||||
|
||||
- Parameter seconds: The intended seconds within the audio to seek to.
|
||||
|
||||
- Note: The seeking is limited to the playable timestamps, if the intended seek is below 0 seconds, the skip will just go to 0 seconds. If the intended seek is past the curation of the current audio, the seek will just go to the end.
|
||||
*/
|
||||
public func seekTo(seconds: Double) {
|
||||
presenter.handleSeek(toNeedle: seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
If using an AVAudioUnitTimePitch, it's important to notify the player that the rate at which the audio playing has changed to keep the media player in the lockscreen up to date. This is only important for playback rate changes.
|
||||
|
||||
- Note: By default this engine has added a pitch modifier node to change the pitch so that on playback rate changes of spoken word the pitch isn't shifted.
|
||||
|
||||
The component description of this node is:
|
||||
````
|
||||
var componentDescription: AudioComponentDescription {
|
||||
get {
|
||||
var ret = AudioComponentDescription()
|
||||
ret.componentType = kAudioUnitType_FormatConverter
|
||||
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
|
||||
return ret
|
||||
}
|
||||
}
|
||||
````
|
||||
Please look at [forums.developer.apple.com/thread/5874](https://forums.developer.apple.com/thread/5874) and [forums.developer.apple.com/thread/6050](https://forums.developer.apple.com/thread/6050) for more details.
|
||||
|
||||
For more details on pitch modifiers for playback rate changes please look at [developer.apple.com/forums/thread/6050](https://developer.apple.com/forums/thread/6050).
|
||||
|
||||
- Parameter rate: The current rate at which the audio is playing.
|
||||
*/
|
||||
public func playbackRateOfAudioChanged(rate: Float) {
|
||||
presenter.handleAudioRateChanged(rate: rate)
|
||||
}
|
||||
|
||||
/**
|
||||
Sets up player to play audio that has been saved on the device.
|
||||
|
||||
- Important: If intending to use [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers during playback, the list of audio modifiers under `SAPlayer.shared.audioModifiers` must be finalized before calling this function. After all realtime audio manipulations within the this will be effective.
|
||||
|
||||
- Note: The default list already has an AVAudioUnitTimePitch node first in the list. This node is specifically set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
|
||||
|
||||
The component description of this node is:
|
||||
````
|
||||
var componentDescription: AudioComponentDescription {
|
||||
get {
|
||||
var ret = AudioComponentDescription()
|
||||
ret.componentType = kAudioUnitType_FormatConverter
|
||||
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
|
||||
return ret
|
||||
}
|
||||
}
|
||||
````
|
||||
Please look at [forums.developer.apple.com/thread/5874](https://forums.developer.apple.com/thread/5874) and [forums.developer.apple.com/thread/6050](https://forums.developer.apple.com/thread/6050) for more details.
|
||||
|
||||
To remove this default pitch modifier for playback rate changes, remove the node by calling `SAPlayer.shared.clearAudioModifiers()`.
|
||||
|
||||
- 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 startSavedAudio(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()
|
||||
|
||||
presenter.handlePlaySavedAudio(withSavedUrl: url)
|
||||
self.mediaInfo = mediaInfo
|
||||
}
|
||||
|
||||
/**
|
||||
Sets up player to play audio that will be streamed from a remote location. After this is called, it will connect to the server and start to receive and process data. The player is not playable the SAAudioAvailabilityRange notifies that player is ready for playing (you can subscribe to these updates through `SAPlayer.Updates.StreamingBuffer`). You can alternatively see when the player is available to play by subscribing to `SAPlayer.Updates.PlayingStatus` and waiting for a status that isn't `.buffering`.
|
||||
|
||||
- Important: If intending to use [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers during playback, the list of audio modifiers under `SAPlayer.shared.audioModifiers` must be finalized before calling this function. After all realtime audio manipulations within the this will be effective.
|
||||
|
||||
- Note: The default list already has an AVAudioUnitTimePitch node first in the list. This node is specifically set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
|
||||
|
||||
The component description of this node is:
|
||||
````
|
||||
var componentDescription: AudioComponentDescription {
|
||||
get {
|
||||
var ret = AudioComponentDescription()
|
||||
ret.componentType = kAudioUnitType_FormatConverter
|
||||
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
|
||||
return ret
|
||||
}
|
||||
}
|
||||
````
|
||||
Please look at [forums.developer.apple.com/thread/5874](https://forums.developer.apple.com/thread/5874) and [forums.developer.apple.com/thread/6050](https://forums.developer.apple.com/thread/6050) for more details.
|
||||
|
||||
To remove this default pitch modifier for playback rate changes, remove the node by calling `SAPlayer.shared.clearAudioModifiers()`.
|
||||
|
||||
- Note: Subscribe to `SAPlayer.Updates.StreamingBuffer` to see updates in streaming progress.
|
||||
|
||||
- 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 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()
|
||||
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: bitrate)
|
||||
self.mediaInfo = mediaInfo
|
||||
}
|
||||
|
||||
/**
|
||||
Stops any streaming in progress.
|
||||
*/
|
||||
public func stopStreamingRemoteAudio() {
|
||||
presenter.handleStopStreamingAudio()
|
||||
}
|
||||
|
||||
/**
|
||||
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, 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, mediaInfo: SALockScreenInfo? = nil) {
|
||||
presenter.handleQueueSavedAudio(withSavedUrl: url, mediaInfo: mediaInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
Resets the player to the state before initializing audio and setting media info.
|
||||
*/
|
||||
public func clear() {
|
||||
presenter.handleClear()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//MARK: - Internal implementation of delegate
|
||||
extension SAPlayer: SAPlayerDelegate {
|
||||
func startAudioDownloaded(withSavedUrl url: AudioURL) {
|
||||
player = AudioDiskEngine(withSavedUrl: url, delegate: presenter)
|
||||
}
|
||||
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate) {
|
||||
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter, bitrate: bitrate)
|
||||
}
|
||||
|
||||
func clearEngine() {
|
||||
player?.pause()
|
||||
player?.invalidate()
|
||||
player = nil
|
||||
Log.info("cleared engine")
|
||||
}
|
||||
|
||||
func playEngine() {
|
||||
becomeDeviceAudioPlayer()
|
||||
player?.play()
|
||||
}
|
||||
|
||||
//Start taking control as the device's player
|
||||
private func becomeDeviceAudioPlayer() {
|
||||
do {
|
||||
if #available(iOS 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)
|
||||
}
|
||||
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
|
||||
} catch {
|
||||
Log.monitor("Problem setting up AVAudioSession to play in:: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func pauseEngine() {
|
||||
player?.pause()
|
||||
}
|
||||
|
||||
func seekEngine(toNeedle needle: Needle) {
|
||||
var seekToNeedle = needle < 0 ? 0 : needle
|
||||
seekToNeedle = needle > Needle(duration ?? 0) ? Needle(duration ?? 0) : needle
|
||||
player?.seek(toNeedle: seekToNeedle)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertFromAVAudioSessionMode(_ input: AVAudioSession.Mode) -> String {
|
||||
return input.rawValue
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// SAPlayerDelegate.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
import CoreMedia
|
||||
|
||||
protocol SAPlayerDelegate: AnyObject, LockScreenViewProtocol {
|
||||
var mediaInfo: SALockScreenInfo? { get set }
|
||||
var skipForwardSeconds: Double { get set }
|
||||
var skipBackwardSeconds: Double { get set }
|
||||
|
||||
func startAudioDownloaded(withSavedUrl url: AudioURL)
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate)
|
||||
func clearEngine()
|
||||
func playEngine()
|
||||
func pauseEngine()
|
||||
func seekEngine(toNeedle needle: Needle) //TODO ensure that engine cleans up out of bounds
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
- Note: Subscribe to `SAPlayer.Updates.AudioDownloading` to see updates in downloading progress.
|
||||
|
||||
- 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)
|
||||
}
|
||||
|
||||
/**
|
||||
Whether downloading audio on cellular data is allowed. By default this is set to `true`.
|
||||
*/
|
||||
public static var allowUsingCellularData = true {
|
||||
didSet {
|
||||
AudioDataManager.shared.setAllowCellularDownloadPreference(allowUsingCellularData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// SAPlayerFeature.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 3/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
extension SAPlayer {
|
||||
/**
|
||||
Special features for audio manipulation. These are examples of manipulations you can do with the player outside of this library. This is just an aggregation of community contibuted ones.
|
||||
|
||||
- Note: These features assume default state of the player and `audioModifiers` meaning some expect the first audio modifier to be the default `AVAudioUnitTimePitch` that comes with the SAPlayer.
|
||||
*/
|
||||
public struct Features {
|
||||
|
||||
/**
|
||||
Feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected.
|
||||
|
||||
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
|
||||
*/
|
||||
public struct SkipSilences {
|
||||
|
||||
static var enabled: Bool = false
|
||||
static var originalRate: Float = 1.0
|
||||
|
||||
/**
|
||||
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.
|
||||
|
||||
- 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 }
|
||||
|
||||
Log.info("enabling skip silences feature")
|
||||
enabled = true
|
||||
originalRate = SAPlayer.shared.rate ?? 1.0
|
||||
let format = engine.mainMixerNode.outputFormat(forBus: 0)
|
||||
|
||||
|
||||
// look at documentation here to get an understanding of what is happening here: https://www.raywenderlich.com/5154-avaudioengine-tutorial-for-ios-getting-started#toc-anchor-005
|
||||
engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, when in
|
||||
guard let channelData = buffer.floatChannelData else {
|
||||
return
|
||||
}
|
||||
|
||||
let channelDataValue = channelData.pointee
|
||||
let channelDataValueArray = stride(from: 0,
|
||||
to: Int(buffer.frameLength),
|
||||
by: buffer.stride).map { channelDataValue[$0] }
|
||||
|
||||
let rms = sqrt(channelDataValueArray.map { $0 * $0 }.reduce(0, +) / Float(buffer.frameLength))
|
||||
|
||||
let avgPower = 20 * log10(rms)
|
||||
|
||||
let meterLevel = self.scaledPower(power: avgPower)
|
||||
Log.debug("meterLevel: \(meterLevel)")
|
||||
if meterLevel < 0.6 { // below 0.6 decibels is below audible audio
|
||||
SAPlayer.shared.rate = originalRate + 0.5
|
||||
Log.debug("speed up rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
} else {
|
||||
SAPlayer.shared.rate = originalRate
|
||||
Log.debug("slow down rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
|
||||
- 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 }
|
||||
Log.info("disabling skip silences feature")
|
||||
engine.mainMixerNode.removeTap(onBus: 0)
|
||||
SAPlayer.shared.rate = originalRate
|
||||
enabled = false
|
||||
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
|
||||
if power < minDb {
|
||||
return 0.0
|
||||
} else if power >= 1.0 {
|
||||
return 1.0
|
||||
} else {
|
||||
return (abs(minDb) - abs(power)) / abs(minDb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Feature to pause the player after a delay. This will happen regardless of if another audio clip has started.
|
||||
*/
|
||||
public struct SleepTimer {
|
||||
static var timer: Timer?
|
||||
|
||||
/**
|
||||
Enable feature to pause the player after a delay. This will happen regardless of if another audio clip has started.
|
||||
|
||||
- Parameter afterDelay: The number of seconds to wait before pausing the audio
|
||||
*/
|
||||
public static func enable(afterDelay delay: Double) {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in
|
||||
SAPlayer.shared.pause()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Disable feature to pause the player after a delay.
|
||||
*/
|
||||
public static func disable() {
|
||||
timer?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
//
|
||||
// SAPlayerPresenter.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
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
|
||||
}
|
||||
|
||||
weak var delegate: SAPlayerDelegate?
|
||||
var shouldPlayImmediately = false //for auto-play
|
||||
|
||||
var needle: Needle?
|
||||
var duration: Duration?
|
||||
|
||||
private var key: String?
|
||||
private var isPlaying: SAPlayingStatus = .buffering
|
||||
|
||||
private var urlKeyMap: [Key: URL] = [:]
|
||||
|
||||
var durationRef:UInt = 0
|
||||
var needleRef:UInt = 0
|
||||
var playingStatusRef:UInt = 0
|
||||
var audioQueue: [QueueItem] = []
|
||||
|
||||
init(delegate: SAPlayerDelegate?) {
|
||||
self.delegate = delegate
|
||||
|
||||
delegate?.setLockScreenControls(presenter: self)
|
||||
}
|
||||
|
||||
func getUrl(forKey key: Key) -> URL? {
|
||||
return urlKeyMap[key]
|
||||
}
|
||||
|
||||
func addUrlToKeyMap(_ url: URL) {
|
||||
urlKeyMap[url.key] = url
|
||||
}
|
||||
|
||||
func handleClear() {
|
||||
delegate?.clearEngine()
|
||||
|
||||
needle = nil
|
||||
duration = nil
|
||||
key = nil
|
||||
delegate?.mediaInfo = nil
|
||||
delegate?.clearLockScreenInfo()
|
||||
|
||||
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
|
||||
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
|
||||
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
|
||||
}
|
||||
|
||||
func handlePlaySavedAudio(withSavedUrl url: URL) {
|
||||
attachForUpdates(url: url)
|
||||
delegate?.startAudioDownloaded(withSavedUrl: url)
|
||||
}
|
||||
|
||||
func handlePlayStreamedAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate) {
|
||||
attachForUpdates(url: url)
|
||||
delegate?.startAudioStreamed(withRemoteUrl: url, bitrate: bitrate)
|
||||
}
|
||||
|
||||
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, mediaInfo: SALockScreenInfo?) {
|
||||
audioQueue.append(QueueItem(loc: .disk, url: url, mediaInfo: mediaInfo))
|
||||
}
|
||||
|
||||
private func attachForUpdates(url: URL) {
|
||||
detachFromUpdates()
|
||||
|
||||
self.key = url.key
|
||||
urlKeyMap[url.key] = url
|
||||
|
||||
durationRef = AudioClockDirector.shared.attachToChangesInDuration(closure: { [weak self] (key, duration) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
guard key == self.key else {
|
||||
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
|
||||
self.duration = duration
|
||||
|
||||
self.delegate?.setLockScreenInfo(withMediaInfo: self.delegate?.mediaInfo, duration: duration)
|
||||
})
|
||||
|
||||
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (key, needle) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
guard key == self.key else {
|
||||
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
self.needle = needle
|
||||
self.delegate?.updateLockscreenElapsedTime(needle: needle)
|
||||
})
|
||||
|
||||
playingStatusRef = AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { [weak self] (key, isPlaying) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
guard key == self.key else {
|
||||
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
self.isPlaying = isPlaying
|
||||
|
||||
if(self.isPlaying == .paused && self.shouldPlayImmediately) {
|
||||
self.shouldPlayImmediately = false
|
||||
self.handlePlay()
|
||||
}
|
||||
|
||||
if(self.isPlaying == .ended) {
|
||||
self.playNextAudioIfExists()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func detachFromUpdates() {
|
||||
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
|
||||
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
|
||||
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
|
||||
}
|
||||
|
||||
func handleStopStreamingAudio() {
|
||||
delegate?.clearEngine()
|
||||
detachFromUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
//MARK:- Used by outside world including:
|
||||
// SPP, lock screen, directors
|
||||
extension SAPlayerPresenter {
|
||||
func handlePause() {
|
||||
delegate?.pauseEngine()
|
||||
self.delegate?.updateLockscreenPaused()
|
||||
}
|
||||
|
||||
func handlePlay() {
|
||||
delegate?.playEngine()
|
||||
self.delegate?.updateLockscreenPlaying()
|
||||
}
|
||||
|
||||
func handleTogglePlayingAndPausing() {
|
||||
if isPlaying == .playing {
|
||||
handlePause()
|
||||
} else if isPlaying == .paused {
|
||||
handlePlay()
|
||||
}
|
||||
}
|
||||
|
||||
func handleSkipForward() {
|
||||
guard let forward = delegate?.skipForwardSeconds else { return }
|
||||
handleSeek(toNeedle: (needle ?? 0) + forward)
|
||||
}
|
||||
|
||||
func handleSkipBackward() {
|
||||
guard let backward = delegate?.skipForwardSeconds else { return }
|
||||
handleSeek(toNeedle: (needle ?? 0) - backward)
|
||||
}
|
||||
|
||||
func handleSeek(toNeedle needle: Needle) {
|
||||
delegate?.seekEngine(toNeedle: needle)
|
||||
}
|
||||
|
||||
func handleAudioRateChanged(rate: Float) {
|
||||
delegate?.updateLockscreenChangePlaybackRate(speed: rate)
|
||||
}
|
||||
|
||||
func handleScrubbingIntervalsChanged() {
|
||||
delegate?.updateLockscreenSkipIntervals()
|
||||
}
|
||||
}
|
||||
|
||||
//MARK:- For lock screen
|
||||
extension SAPlayerPresenter {
|
||||
func getIsPlaying() -> Bool {
|
||||
return isPlaying == .playing
|
||||
}
|
||||
}
|
||||
|
||||
//MARK:- AVAudioEngineDelegate
|
||||
extension SAPlayerPresenter: AudioEngineDelegate {
|
||||
func didError() {
|
||||
Log.monitor("We should have handled engine error")
|
||||
}
|
||||
}
|
||||
|
||||
//MARK:- Autoplay
|
||||
extension SAPlayerPresenter {
|
||||
func playNextAudioIfExists() {
|
||||
Log.info("looking foor next audio in queue to play")
|
||||
guard audioQueue.count > 0 else {
|
||||
Log.info("no queued audio")
|
||||
return
|
||||
}
|
||||
let nextAudioURL = audioQueue.removeFirst()
|
||||
let key = nextAudioURL.url.key
|
||||
|
||||
|
||||
Log.info("getting ready to play \(nextAudioURL)")
|
||||
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.loc {
|
||||
case .remote:
|
||||
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.url, bitrate: nextAudioURL.bitrate)
|
||||
break
|
||||
case .disk:
|
||||
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.url)
|
||||
}
|
||||
|
||||
self.shouldPlayImmediately = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// SAPlayerUpdateSubscription.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-02-18.
|
||||
// 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 {
|
||||
|
||||
/**
|
||||
Receive updates for changing values from the player, such as the duration, elapsed time of playing audio, download progress, and etc.
|
||||
*/
|
||||
public struct Updates {
|
||||
|
||||
/**
|
||||
Updates to changes in the timestamp/elapsed time of the current initialized audio. Aka, where the scrubber's pointer of the audio should be at.
|
||||
*/
|
||||
public struct ElapsedTime {
|
||||
|
||||
/**
|
||||
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 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.
|
||||
- Parameter timePosition: The current time within the audio that is playing.
|
||||
- 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, _ timePosition: Double) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInNeedle(closure: { (key, needle) in
|
||||
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
|
||||
closure(url, needle)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in elapsed time of audio.
|
||||
|
||||
- Parameter id: The closure with this id will stop receiving updates.
|
||||
*/
|
||||
public static func unsubscribe(_ id: UInt) {
|
||||
AudioClockDirector.shared.detachFromChangesInNeedle(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Updates to changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data.
|
||||
|
||||
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
|
||||
*/
|
||||
public struct Duration {
|
||||
|
||||
/**
|
||||
Subscribe to updates to changes in duration of the current audio initialized.
|
||||
|
||||
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
|
||||
|
||||
- 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 duration: The duration of the current initialized audio.
|
||||
- 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, _ duration: Double) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInDuration(closure: { (key, duration) in
|
||||
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
|
||||
closure(url, duration)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in duration of the current initialized audio.
|
||||
|
||||
- Parameter id: The closure with this id will stop receiving updates.
|
||||
*/
|
||||
public static func unsubscribe(_ id: UInt) {
|
||||
AudioClockDirector.shared.detachFromChangesInDuration(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Updates to changes in the playing/paused status of the player.
|
||||
*/
|
||||
public struct PlayingStatus {
|
||||
|
||||
/**
|
||||
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 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: SAPlayingStatus) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { (key, isPlaying) in
|
||||
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
|
||||
closure(url, isPlaying)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in the playing/paused status of audio.
|
||||
|
||||
- Parameter id: The closure with this id will stop receiving updates.
|
||||
*/
|
||||
public static func unsubscribe(_ id: UInt) {
|
||||
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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: For live streams that don't have an expected audio length from the beginning of the stream; the duration is constantly changing and equal to the total seconds buffered from the SAAudioAvailabilityRange.
|
||||
|
||||
- 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.
|
||||
- Parameter buffer: Availabity of audio that has been downloaded to play.
|
||||
- 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, _ buffer: SAAudioAvailabilityRange) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInBufferedRange(closure: { (key, buffer) in
|
||||
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
|
||||
closure(url, buffer)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in streaming progress.
|
||||
|
||||
- Parameter id: The closure with this id will stop receiving updates.
|
||||
*/
|
||||
public static func unsubscribe(_ id: UInt) {
|
||||
AudioClockDirector.shared.detachFromChangesInBufferedRange(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Updates to 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.
|
||||
*/
|
||||
public struct AudioDownloading {
|
||||
|
||||
/**
|
||||
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 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.
|
||||
- Parameter progress: Value from 0.0 to 1.0 indicating progress of download.
|
||||
- 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, _ progress: Double) -> ()) -> UInt {
|
||||
return DownloadProgressDirector.shared.attach(closure: { (key, progress) in
|
||||
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
|
||||
closure(url, progress)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in download progress.
|
||||
|
||||
- Parameter id: The closure with this id will stop receiving updates.
|
||||
*/
|
||||
public static func unsubscribe(_ id: UInt) {
|
||||
DownloadProgressDirector.shared.detach(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
public struct AudioQueue {
|
||||
/**
|
||||
Subscribe to updates to changes in the progress of your audio queue. When streaming audio playback completes
|
||||
and continues onto the next track, the closure is invoked.
|
||||
- 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 forthcoming audio file.
|
||||
- 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 (_ key: String, _ newUrl: URL) -> ()) -> UInt {
|
||||
return AudioQueueDirector.shared.attach(closure: { (key, url) in
|
||||
closure(key, url)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in download progress.
|
||||
- Parameter id: The closure with this id will stop receiving updates.
|
||||
*/
|
||||
public static func unsubscribe(_ id: UInt) {
|
||||
AudioQueueDirector.shared.detach(withID: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Constants.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
typealias Needle = Double
|
||||
typealias Duration = Double
|
||||
typealias Key = String
|
||||
typealias AudioURL = URL
|
||||
typealias IsPlaying = Bool
|
||||
typealias ID = String
|
||||
|
||||
typealias NameFile = String //Should have last path component (.mp3)
|
||||
|
||||
let DEBOUNCING_BUFFER_TIME: Double = 1.0
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Data.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-11-29.
|
||||
// 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 Data {
|
||||
// Introduced in Swift 5, withUnsafeBytes using UnsafePointers is deprecated
|
||||
// https://mjtsai.com/blog/2019/03/27/swift-5-released/
|
||||
func accessBytes<R>(_ body: (UnsafePointer<UInt8>) throws -> R) rethrows -> R {
|
||||
return try withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> R in
|
||||
let unsafeBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
|
||||
guard let unsafePointer = unsafeBufferPointer.baseAddress else {
|
||||
Log.error("")
|
||||
var int: UInt8 = 0
|
||||
return try body(&int)
|
||||
}
|
||||
return try body(unsafePointer)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func accessMutableBytes<R>(_ body: (UnsafeMutablePointer<UInt8>) throws -> R) rethrows -> R {
|
||||
return try withUnsafeMutableBytes { (rawBufferPointer: UnsafeMutableRawBufferPointer) -> R in
|
||||
let unsafeMutableBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
|
||||
guard let unsafeMutablePointer = unsafeMutableBufferPointer.baseAddress else {
|
||||
Log.error("")
|
||||
var int: UInt8 = 0
|
||||
return try body(&int)
|
||||
}
|
||||
return try body(unsafeMutablePointer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// Date.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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 Date {
|
||||
/**
|
||||
Finds the 64-bit representation of UTC. rand() uses UTC as a seed, so using the raw UTC should be sufficient for our case.
|
||||
|
||||
- Returns: A 64-bit representation of time.
|
||||
*/
|
||||
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."
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
return UInt(Date().timeIntervalSince1970.bitPattern)
|
||||
} else {
|
||||
let time = Date().timeIntervalSince1970.bitPattern & 0xFFFFFFFF;
|
||||
return UInt(time)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- Returns: UTC in seconds.
|
||||
*/
|
||||
static func getUTC() -> UTC {
|
||||
return Int(Date().timeIntervalSince1970)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// DirectorThreadSafeClosures.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// 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
|
||||
|
||||
enum DirectorError: Error {
|
||||
case closureIsDead
|
||||
}
|
||||
|
||||
/**
|
||||
P for payload
|
||||
*/
|
||||
class DirectorThreadSafeClosures<P> {
|
||||
typealias TypeClosure = (Key, P) throws -> Void
|
||||
private var queue: DispatchQueue = DispatchQueue(label: "SwiftAudioPlayer.thread_safe_map", attributes: .concurrent)
|
||||
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
|
||||
var iterator = self.closures.makeIterator()
|
||||
while let element = iterator.next() {
|
||||
do {
|
||||
try element.value(key, payload)
|
||||
} catch {
|
||||
helperRemove(withKey: element.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//UInt is actually 64-bits on modern devices
|
||||
func attach(closure: @escaping TypeClosure) -> UInt {
|
||||
let id: UInt = Date.getUTC64()
|
||||
|
||||
//The director may not yet have the status yet. We should only call the closure if we have it
|
||||
//Let the caller know the immediate value. If it's dead already then stop
|
||||
for (key, val) in cache {
|
||||
do {
|
||||
try closure(key, val)
|
||||
} catch {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
//Replace what's in the map with the new closure
|
||||
helperInsert(withKey: id, closure: closure)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func detach(id: UInt) {
|
||||
helperRemove(withKey: id)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
queue.async(flags: .barrier) {
|
||||
self.closures.removeAll()
|
||||
self.cache.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
private func helperRemove(withKey key: UInt) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.closures[key] = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func helperInsert(withKey key: UInt, closure: @escaping TypeClosure) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.closures[key] = closure
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// Log.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyrights to ColorLog
|
||||
// https://cocoapods.org/pods/ColorLog
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
// Possible levels of log messages to log
|
||||
enum LogLevel: Int {
|
||||
case DEBUG = 1
|
||||
case INFO = 2
|
||||
case WARN = 3
|
||||
case ERROR = 4
|
||||
case EXTERNAL_DEBUG = 5
|
||||
case MONITOR = 6
|
||||
case TEST = 7
|
||||
}
|
||||
|
||||
// Specify which types of log messages to display. Default level is set to WARN, which means Log will print any log messages of type only WARN, ERROR, MONITOR, and TEST. To print DEBUG and INFO logs, set the level to a lower value.
|
||||
var logLevel: LogLevel = LogLevel.MONITOR
|
||||
|
||||
class Log {
|
||||
private init() {}
|
||||
|
||||
// Used for OSLog
|
||||
private static let SUBSYSTEM: String = "com.SwiftAudioPlayer"
|
||||
|
||||
/**
|
||||
Used for when you're doing tests. Testing log should be removed before commiting
|
||||
|
||||
How to use: Log.test("this is my message")
|
||||
Output: 13:51:38.487 TEST ❇️❇️❇️❇️ in InputNameViewController.swift:addContainerToVC():77:: this is test
|
||||
|
||||
To change the log level, visit the LogLevel enum
|
||||
|
||||
- Parameter logMessage: The message to show
|
||||
- Parameter classPath: automatically generated based on the class that called this function
|
||||
- Parameter functionName: automatically generated based on the function that called this function
|
||||
- Parameter lineNumber: automatically generated based on the line that called this function
|
||||
*/
|
||||
public static func test(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
|
||||
let fileName = URLUtil.getNameFromStringPath(classPath)
|
||||
if logLevel.rawValue <= LogLevel.TEST.rawValue {
|
||||
let log = OSLog(subsystem: SUBSYSTEM, category: "TEST ❇️❇️❇️❇️")
|
||||
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Used when something unexpected happen, such as going out of bounds in an array. Errors are typically guarded for.
|
||||
|
||||
How to use: Log.error("this is error")
|
||||
Output: 13:51:38.487 ERROR 🛑🛑🛑🛑 in InputNameViewController.swift:addContainerToVC():76:: this is error
|
||||
|
||||
To change the log level, visit the LogLevel enum
|
||||
|
||||
- Parameter logMessage: The message to show
|
||||
- Parameter classPath: automatically generated based on the class that called this function
|
||||
- Parameter functionName: automatically generated based on the function that called this function
|
||||
- Parameter lineNumber: automatically generated based on the line that called this function
|
||||
*/
|
||||
public static func error(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
|
||||
let fileName = URLUtil.getNameFromStringPath(classPath)
|
||||
if logLevel.rawValue <= LogLevel.ERROR.rawValue {
|
||||
let log = OSLog(subsystem: SUBSYSTEM, category: "ERROR 🛑🛑🛑🛑")
|
||||
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
|
||||
}
|
||||
|
||||
if logLevel.rawValue <= LogLevel.EXTERNAL_DEBUG.rawValue {
|
||||
let log = OSLog(subsystem: SUBSYSTEM, category: "WARNING")
|
||||
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Used when something catastrophic just happened. Like app about to crash, app state is inconsistent, or possible data corruption.
|
||||
|
||||
How to use: Log.error("this is error")
|
||||
Output: 13:51:38.487 MONITOR 🔥🔥🔥🔥 in InputNameViewController.swift:addContainerToVC():76:: data in corrupted state!
|
||||
|
||||
To change the log level, visit the LogLevel enum
|
||||
|
||||
- Parameter logMessage: The message to show
|
||||
- Parameter classPath: automatically generated based on the class that called this function
|
||||
- Parameter functionName: automatically generated based on the function that called this function
|
||||
- Parameter lineNumber: automatically generated based on the line that called this function
|
||||
*/
|
||||
public static func monitor(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
|
||||
let fileName = URLUtil.getNameFromStringPath(classPath)
|
||||
if logLevel.rawValue <= LogLevel.ERROR.rawValue {
|
||||
let log = OSLog(subsystem: SUBSYSTEM, category: "ERROR 🔥🔥🔥🔥")
|
||||
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Used when something went wrong, but the app can still function.
|
||||
|
||||
How to use: Log.warn("this is warn")
|
||||
Output: 13:51:38.487 WARN ⚠️⚠️⚠️⚠️ in InputNameViewController.swift:addContainerToVC():75:: this is warn
|
||||
|
||||
To change the log level, visit the LogLevel enum
|
||||
|
||||
- Parameter logMessage: The message to show
|
||||
- Parameter classPath: automatically generated based on the class that called this function
|
||||
- Parameter functionName: automatically generated based on the function that called this function
|
||||
- Parameter lineNumber: automatically generated based on the line that called this function
|
||||
*/
|
||||
public static func warn(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
|
||||
let fileName = URLUtil.getNameFromStringPath(classPath)
|
||||
if logLevel.rawValue <= LogLevel.WARN.rawValue {
|
||||
let log = OSLog(subsystem: SUBSYSTEM, category: "WARN ⚠️⚠️⚠️⚠️")
|
||||
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
|
||||
}
|
||||
|
||||
if logLevel.rawValue <= LogLevel.EXTERNAL_DEBUG.rawValue {
|
||||
let log = OSLog(subsystem: SUBSYSTEM, category: "DEBUG")
|
||||
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Used when you want to show information like username or question asked.
|
||||
|
||||
How to use: Log.info("this is info")
|
||||
Output: 13:51:38.486 INFO 🖤🖤🖤🖤 in InputNameViewController.swift:addContainerToVC():74:: this is info
|
||||
|
||||
To change the log level, visit the LogLevel enum
|
||||
|
||||
- Parameter logMessage: The message to show
|
||||
- Parameter classPath: automatically generated based on the class that called this function
|
||||
- Parameter functionName: automatically generated based on the function that called this function
|
||||
- Parameter lineNumber: automatically generated based on the line that called this function
|
||||
*/
|
||||
public static func info(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
|
||||
let fileName = URLUtil.getNameFromStringPath(classPath)
|
||||
if logLevel.rawValue <= LogLevel.INFO.rawValue {
|
||||
let log = OSLog(subsystem: SUBSYSTEM, category: "INFO 🖤🖤🖤🖤")
|
||||
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Used for when you're rebugging and you want to follow what's happening.
|
||||
|
||||
How to use: Log.debug("this is debug")
|
||||
Output: 13:51:38.485 DEBUG 🐝🐝🐝🐝 in InputNameViewController.swift:addContainerToVC():73:: this is debug
|
||||
|
||||
To change the log level, visit the LogLevel enum
|
||||
|
||||
- Parameter logMessage: The message to show
|
||||
- Parameter classPath: automatically generated based on the class that called this function
|
||||
- Parameter functionName: automatically generated based on the function that called this function
|
||||
- Parameter lineNumber: automatically generated based on the line that called this function
|
||||
*/
|
||||
public static func debug(_ logMessage: Any?..., classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
|
||||
let fileName = URLUtil.getNameFromStringPath(classPath)
|
||||
if logLevel.rawValue <= LogLevel.DEBUG.rawValue {
|
||||
let log = OSLog(subsystem: SUBSYSTEM, category: "DEBUG 🐝🐝🐝🐝")
|
||||
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK:- Helpers for Log class
|
||||
fileprivate struct URLUtil {
|
||||
static func getNameFromStringPath(_ stringPath: String) -> String {
|
||||
//URL sees that "+" is a " "
|
||||
let stringPath = stringPath.replacingOccurrences(of: " ", with: "+")
|
||||
let url = URL(string: stringPath)
|
||||
return url!.lastPathComponent
|
||||
}
|
||||
|
||||
static func getNameFromURL(_ url: URL) -> String {
|
||||
return url.lastPathComponent
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
fileprivate func timeStamp() -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Any? {
|
||||
var toLog: String {
|
||||
var strs:[String] = []
|
||||
for element in self {
|
||||
strs.append("\(element ?? "nil")")
|
||||
}
|
||||
return strs.joined(separator: " |^| ")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// URL.swift
|
||||
// Pods-SwiftAudioPlayer_Example
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
var key: String {
|
||||
get {
|
||||
return "audio_\(self.absoluteString.hashed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate extension String {
|
||||
var hashed: UInt64 {
|
||||
get {
|
||||
var result = UInt64 (8742)
|
||||
let buf = [UInt8](self.utf8)
|
||||
for b in buf {
|
||||
result = 127 * (result & 0x00ffffffffffffff) + UInt64(b)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioPlayer'
|
||||
s.version = '0.1.0'
|
||||
s.version = '5.0.4'
|
||||
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,12 +26,12 @@ 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/<TWITTER_USERNAME>'
|
||||
s.social_media_url = 'https://twitter.com/_tanhakabir'
|
||||
|
||||
s.ios.deployment_target = '8.0'
|
||||
s.ios.deployment_target = '10.0'
|
||||
|
||||
s.source_files = 'SwiftAudioPlayer/Classes/**/*'
|
||||
s.swift_version = '4.0'
|
||||
s.source_files = 'Source/**/*'
|
||||
s.swift_version = '5.0'
|
||||
|
||||
# s.resource_bundles = {
|
||||
# 'SwiftAudioPlayer' => ['SwiftAudioPlayer/Assets/*.png']
|
||||
|
||||
Reference in New Issue
Block a user