Compare commits

...

25 Commits

Author SHA1 Message Date
Tanha af1ab75c87 Update README.md 2019-02-24 20:34:16 -08:00
Tanha e563ba2f99 update requirements to iOS 10.0 2019-02-24 12:04:20 -08:00
Tanha ea7796459a nit spelling 2019-02-24 00:45:54 -08:00
Tanha 1dfce31580 documentation for subscribing to the player 2019-02-24 00:45:03 -08:00
Tanha 5eb08dcca3 add seek functionality 2019-02-21 23:59:10 -08:00
Tanha 8565485253 add play pause functionality 2019-02-21 19:36:20 -08:00
Tanha cc744a20c7 add streaming 2019-02-21 19:27:38 -08:00
Tanha c43b10d38d nit 2019-02-21 18:44:41 -08:00
Tanha ed61a41267 example app downloading audio 2019-02-21 18:33:03 -08:00
Tanha 58d1695cba add basic UI elements 2019-02-20 16:20:46 -08:00
Tanha 6bdfc917e3 nit 2019-02-18 20:27:21 -08:00
Tanha af739b0efb add interface for external subscribing to updates 2019-02-18 19:35:26 -08:00
Tanha cbaf1cf630 nit 2019-02-18 16:22:57 -08:00
Tanha 6f203c94b0 refractor SAPlayerPresenter 2019-02-18 16:11:50 -08:00
Tanha 81852fb94c move new file to proper target 2019-02-18 12:58:19 -08:00
Tanha d0028620e6 add download progress director 2019-02-18 00:57:10 -08:00
Tanha 0e021f27fe refractor director pattern for attaches with keys in payload 2019-02-18 00:56:55 -08:00
Tanha bd0996d39b implement basic public functions 2019-02-17 19:31:34 -08:00
Tanha 0c383da201 clean up licensing 2019-02-05 10:23:45 -08:00
Tanha 8a7b9d6d7b remove extraneous functions from model layer 2019-01-30 13:41:47 -08:00
Tanha d3b9a68443 rename fix 2019-01-29 15:32:43 -08:00
Tanha 765383157d hook up pod to example project 2019-01-29 15:29:37 -08:00
Tanha 1f8ee401fa add initial engine from previous project
Not actually working, just complies.

Co-authored-by: JonMercer <mercer.jon@gmail.com>
2019-01-29 15:01:38 -08:00
Tanha 2200c4169d add logging file
Co-authored-by: JonMercer <mercer.jon@gmail.com>
2019-01-29 10:49:22 -08:00
Tanha cd485a0464 update README 2019-01-28 23:02:02 -08:00
47 changed files with 5199 additions and 37 deletions
+201
View File
@@ -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.
+1 -1
View File
@@ -9,7 +9,7 @@ EXTERNAL SOURCES:
:path: "../"
SPEC CHECKSUMS:
SwiftAudioPlayer: 6d47a258720feecb431509eb71215559621fa12d
SwiftAudioPlayer: abfeb4ac2467cdd7b5b8a5cb442780184ea172bc
PODFILE CHECKSUM: 84ea27746bf895da86125356a8d0df7a323c4c08
+6 -4
View File
@@ -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"
}
+1 -1
View File
@@ -9,7 +9,7 @@ EXTERNAL SOURCES:
:path: "../"
SPEC CHECKSUMS:
SwiftAudioPlayer: 6d47a258720feecb431509eb71215559621fa12d
SwiftAudioPlayer: abfeb4ac2467cdd7b5b8a5cb442780184ea172bc
PODFILE CHECKSUM: 84ea27746bf895da86125356a8d0df7a323c4c08
+214 -15
View File
@@ -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,38 @@
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, ); }; };
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 */; };
A4681FCA220113940018AB51 /* AudioClockDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F852200DA8B0018AB51 /* AudioClockDirector.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 */; };
A4FBA6B2221B538E00D5A353 /* DownloadProgressDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */; };
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */; };
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */; };
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 +72,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 +80,60 @@
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>"; };
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>"; };
A4681F852200DA8B0018AB51 /* AudioClockDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioClockDirector.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>"; };
A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgressDirector.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 +178,7 @@
41C7F403DA52FBC5C40644BB0E824CAA /* SwiftAudioPlayer */ = {
isa = PBXGroup;
children = (
A3EB275E4B4A4F3D26D5B3835EA81466 /* ReplaceMe.swift */,
A4681FE2220117B50018AB51 /* Source */,
840F8E752B4437107D761C28D4EE8D0B /* Pod */,
EAE1BCB45D8F275CE4428674B5151284 /* Support Files */,
);
@@ -193,10 +256,106 @@
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 */,
);
path = Util;
sourceTree = "<group>";
};
A4681F932200E2020018AB51 /* Engine */ = {
isa = PBXGroup;
children = (
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 = (
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 */,
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */,
A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */,
A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */,
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */,
A4681F852200DA8B0018AB51 /* AudioClockDirector.swift */,
A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */,
A4681F932200E2020018AB51 /* Engine */,
A4681F9B2200E4850018AB51 /* Model */,
A4681F842200D91D0018AB51 /* Util */,
);
path = Source;
sourceTree = "<group>";
};
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -318,6 +477,11 @@
attributes = {
LastSwiftUpdateCheck = 0930;
LastUpgradeCheck = 0930;
TargetAttributes = {
042ACE071BA515F4DE0E0C8007C3F0EE = {
LastSwiftMigration = 1010;
};
};
};
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
compatibilityVersion = "Xcode 3.2";
@@ -343,8 +507,39 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1606D9AEB876D4A642C3CE2985C600C9 /* ReplaceMe.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 */,
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 */,
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 */,
A4681FD3220113B60018AB51 /* AudioParserPropertyListener.swift in Sources */,
A4681FCA220113940018AB51 /* AudioClockDirector.swift in Sources */,
A4681FD0220113A70018AB51 /* AudioConverterErrors.swift in Sources */,
A4FBA6B2221B538E00D5A353 /* DownloadProgressDirector.swift in Sources */,
A4681FD7220113C30018AB51 /* StreamProgressPTO.swift in Sources */,
A4681FE0220113E40018AB51 /* Log.swift in Sources */,
A4681FCE220113A20018AB51 /* AudioConverter.swift in Sources */,
A4FBA6B9221BAF8700D5A353 /* SAAudioAvailabilityRange.swift in Sources */,
A4681FCD2201139E0018AB51 /* AudioStreamEngine.swift in Sources */,
A4681FD9220113CD0018AB51 /* AudioStreamWorker.swift in Sources */,
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */,
A4681FCB220113980018AB51 /* AudioEngine.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -461,7 +656,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;
@@ -493,7 +688,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;
@@ -515,6 +710,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 +722,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 +735,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 = "";
@@ -608,6 +805,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 +817,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 +829,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";
@@ -212,10 +212,12 @@
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
LastSwiftMigration = 0900;
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
LastSwiftMigration = 0900;
TestTargetID = 607FACCF1AFB9204008FA782;
};
@@ -410,7 +412,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;
@@ -456,7 +458,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,6 +471,7 @@
baseConfigurationReference = 65A66AB4C3016E8BB53FF3E0 /* Pods-SwiftAudioPlayer_Example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = R2392A68YQ;
INFOPLIST_FILE = SwiftAudioPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
@@ -484,6 +487,7 @@
baseConfigurationReference = 4B5DD2AE0B23A759D18926DC /* Pods-SwiftAudioPlayer_Example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = R2392A68YQ;
INFOPLIST_FILE = SwiftAudioPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
@@ -498,6 +502,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = BBD877782CC67FBCC7BF7532 /* Pods-SwiftAudioPlayer_Tests.debug.xcconfig */;
buildSettings = {
DEVELOPMENT_TEAM = R2392A68YQ;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
@@ -520,6 +525,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 0B7D1E6C00E83B4AF8AA1781 /* Pods-SwiftAudioPlayer_Tests.release.xcconfig */;
buildSettings = {
DEVELOPMENT_TEAM = R2392A68YQ;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
@@ -7,6 +7,7 @@
//
import UIKit
import SwiftAudioPlayer
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -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,11 @@
<?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">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" 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>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -20,11 +20,145 @@
<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="320" width="343" height="2"/>
<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="305" 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="valueChanged" id="jDA-wR-wxk"/>
</connections>
</slider>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jUc-tP-CC5">
<rect key="frame" x="172.5" y="250" 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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tFH-sY-Xu9">
<rect key="frame" x="62.5" y="250" 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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0QE-3F-a4G">
<rect key="frame" x="282.5" y="250" 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="564" width="347" height="31"/>
<connections>
<action selector="rateChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="FDJ-jA-bm8"/>
</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="80" width="343" height="29"/>
<segments>
<segment title="20k Hertz"/>
<segment title="Acquired"/>
<segment title="Y Combinator"/>
</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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KDu-ea-kF8">
<rect key="frame" x="78" y="140" 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>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6d9-Bc-hIz">
<rect key="frame" x="244" y="140" 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>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="rate: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yUQ-mI-ozK">
<rect key="frame" x="157" y="535" width="61" height="21"/>
<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="297" 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="297" width="40" height="15"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<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="Urj-Dv-41y" firstAttribute="centerY" secondItem="j3w-gr-HzF" secondAttribute="centerY" id="Fvd-7V-Rr8"/>
<constraint firstItem="0QE-3F-a4G" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="jUc-tP-CC5" secondAttribute="trailing" constant="8" symbolic="YES" id="JP5-yW-eVB"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="w2a-RA-zmI" secondAttribute="bottom" constant="200" id="K1K-8N-SpD"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="NOY-IO-NIJ"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="centerY" secondItem="jUc-tP-CC5" secondAttribute="centerY" id="Rre-EY-kVY"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="78" id="SRU-sX-z5b"/>
<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="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="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="62.5" id="cH6-q6-Lel"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cgM-Nj-yit"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="dLw-rF-Pfb"/>
<constraint firstItem="w2a-RA-zmI" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="daz-b0-eCC"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="tFH-sY-Xu9" secondAttribute="trailing" constant="8" symbolic="YES" id="fS9-Ce-4ph"/>
<constraint firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" constant="16" id="gdg-7Y-7la"/>
<constraint firstItem="6d9-Bc-hIz" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="m9s-An-IWV"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="top" secondItem="yUQ-mI-ozK" secondAttribute="bottom" constant="8" id="oaW-rr-UVN"/>
<constraint firstAttribute="trailing" secondItem="0QE-3F-a4G" secondAttribute="trailing" constant="62.5" id="tg1-gr-hdd"/>
<constraint firstAttribute="trailing" secondItem="6d9-Bc-hIz" secondAttribute="trailing" constant="82" 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="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="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="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>
+205 -3
View File
@@ -7,18 +7,220 @@
//
import UIKit
import SwiftAudioPlayer
class ViewController: UIViewController {
struct AudioInfo {
let index: Int
var url: URL {
switch index {
case 0:
return URL(string: "https://traffic.megaphone.fm/TTH7630150098.mp3")!
case 1:
return URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!
case 2:
return URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&amp;sd=1&amp;u=1549423185")!
default:
return URL(string: "https://traffic.megaphone.fm/TTH7630150098.mp3")!
}
}
var title: String {
switch index {
case 0:
return "Twenty Thousand Hertz"
case 1:
return "Acquired"
case 2:
return "Y Combinator"
default:
return "Twenty Thousand Hertz"
}
}
let artist: String = "SwiftAudioPlayer Sample App"
let releaseDate: Int = 1550790640
}
var selectedAudio: AudioInfo = AudioInfo(index: 0) {
didSet {
if SAPlayer.Downloader.isDownloaded(withRemoteUrl: selectedAudio.url) {
downloadButton.setTitle("Delete downloaded", for: .normal)
streamButton.isEnabled = false
}
}
}
@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 durationLabel: UILabel!
@IBOutlet weak var currentTimestampLabel: UILabel!
var isDownloading: Bool = false
var isStreaming: Bool = false
var duration: Double = 0.0
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.
adjustSpeed()
isPlayable = false
_ = SAPlayer.Updates.Duration.subscribe { [weak self] (url, duration) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
self.durationLabel.text = SAPlayer.prettifyTimestamp(duration)
self.duration = duration
}
_ = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
guard self.duration != 0 else { return }
self.scrubberSlider.value = Float(position/self.duration)
}
_ = SAPlayer.Updates.AudioDownloading.subscribe { [weak self] (url, progress) in
print(progress)
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
if self.isDownloading {
self.downloadButton.setTitle("Cancel \(String(format: "%02d", (progress * 100)))%", for: .normal)
}
}
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
if self.duration == 0.0 { return }
let progress = Float((buffer.totalDurationBuffered + buffer.startingBufferTimePositon) / self.duration)
self.bufferProgress.progress = progress
if progress >= 0.99 {
self.streamButton.isEnabled = false
}
self.isPlayable = buffer.isReadyForPlaying
}
_ = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (url, playing) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
if playing {
self.playPauseButton.setTitle("Pause", for: .normal)
} else {
self.playPauseButton.setTitle("Play", for: .normal)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func audioSelected(_ sender: Any) {
let selected = audioSelector.selectedSegmentIndex
selectedAudio = AudioInfo(index: selected)
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
}
@IBAction func scrubberSeeked(_ sender: Any) {
SAPlayer.shared.seekTo(seconds: Double(scrubberSlider.value))
}
@IBAction func rateChanged(_ sender: Any) {
adjustSpeed()
}
@IBAction func downloadTouched(_ sender: Any) {
if !isDownloading {
if SAPlayer.Downloader.isDownloaded(withRemoteUrl: selectedAudio.url) {
SAPlayer.Downloader.deleteDownload(withRemoteUrl: selectedAudio.url)
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)
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 {
SAPlayer.shared.initializeAudio(withRemoteUrl: selectedAudio.url)
streamButton.setTitle("Cancel streaming", for: .normal)
downloadButton.isEnabled = false
} else {
// TODO
}
}
@IBAction func playPauseTouched(_ sender: Any) {
SAPlayer.shared.togglePlayAndPause()
}
@IBAction func skipBackwardTouched(_ sender: Any) {
SAPlayer.shared.skipBackwards()
}
@IBAction func skipForwardTouched(_ sender: Any) {
SAPlayer.shared.skipForward()
}
private func adjustSpeed() {
let speed = rateSlider.value
rateLabel.text = "rate: \(speed)x"
SAPlayer.shared.rate = Double(speed)
}
}
+1 -1
View File
@@ -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
+61 -6
View File
@@ -5,13 +5,23 @@
[![License](https://img.shields.io/cocoapods/l/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
## Example
Swift based audio player that is able to both stream remote audio and play locally saved audio, while performing audio manipulations in real-time. Underlying using AVAudioEngine, and you can change the rate of audio (up to 32x), change pitch, and [other audio enhancements](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
This player was originally developed to be used in a [podcast player](https://chameleonpodcast.com/). We had 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.
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).
### Requirements
SwiftAudioPlayer is only available for iOS 10.0 and higher.
## Getting Started
### Example Project
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
## Installation
### Installation
SwiftAudioPlayer is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
@@ -20,9 +30,54 @@ it, simply add the following line to your Podfile:
pod 'SwiftAudioPlayer'
```
## Author
### Usage
tanhakabir, tanhakabir.ca@gmail.com
To play remote audio:
```
let url = URL(string: "https://randomwebsite.com/audio.mp3")!
SAPlayer.shared.initializeAudio(withRemoteUrl: url)
SAPlayer.shared.play()
```
To set the display information for the lockscreen:
```
let info = SALockScreenInfo(title: "Random audio", artist: "Foo", artwork: UIImage(), releaseDate: 123456789)
SAPlayer.shared.mediaInfo = info
```
To receive streaming progress:
```
@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
}
}
```
## 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
+119
View File
@@ -0,0 +1,119 @@
//
// 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<IsPlaying> = 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, IsPlaying) 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 audioPaused(_ key: Key) {
playingStatusClosures.broadcast(key: key, payload: false)
}
func audioPlaying(_ key: Key) {
playingStatusClosures.broadcast(key: key, payload: true)
}
}
extension AudioClockDirector {
func changeInAudioBuffered(_ key: Key, buffered: SAAudioAvailabilityRange) {
bufferClosures.broadcast(key: key, payload: buffered)
}
}
+52
View File
@@ -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)
}
}
+141
View File
@@ -0,0 +1,141 @@
//
// 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, isPlayable: true)
} else {
Log.monitor("Could not load downloaded file with url: \(url)")
}
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] (timer: Timer) in
guard let _ = self else { return }
self?.timer = timer
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
}
delegate?.didEndPlaying()
}
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() {
//Nothing to invalidate for disk
}
}
+194
View File
@@ -0,0 +1,194 @@
//
// 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 {
func play()
func pause()
func seek(toNeedle needle: Needle)
func setSpeed(speed: Double)
func invalidate()
}
protocol AudioEngineDelegate: AnyObject {
func didEndPlaying() //for auto play
func didError()
}
class AudioEngine: AudioEngineProtocol {
weak var delegate:AudioEngineDelegate?
let key:Key
let engine = AVAudioEngine()
let playerNode = AVAudioPlayerNode()
let rateNode: AVAudioUnitTimePitch
var timer: Timer?
static let defaultEngineAudioFormat: AVAudioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
var state:TimerState = .suspended
enum TimerState {
case suspended
case resumed
}
var audioSpeed: Double = 1.0 {
didSet {
rateNode.rate = Float(audioSpeed)
}
}
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 isPlaying = false {
didSet {
guard isPlaying != oldValue else {
return
}
if isPlaying {
AudioClockDirector.shared.audioPlaying(key)
} else {
AudioClockDirector.shared.audioPaused(key)
}
}
}
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, isPlayable: false)
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, 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
// 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
}
}
rateNode = AVAudioUnitTimePitch(audioComponentDescription: componentDescription)
engine.attach(playerNode)
engine.attach(rateNode)
engine.connect(playerNode, to: rateNode, format: engineAudioFormat)
engine.connect(rateNode, to: engine.mainMixerNode, format: engineAudioFormat)
engine.prepare()
}
deinit {
timer?.invalidate()
if state == .resumed {
engine.stop()
}
}
func updateIsPlaying() {
isPlaying = engine.isRunning && playerNode.isPlaying
}
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 setSpeed(speed: Double) {
audioSpeed = speed
}
func invalidate() {
}
}
+298
View File
@@ -0,0 +1,298 @@
//
// 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 he have 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 PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
private let queue = DispatchQueue(label: "SwiftAudioPlayer.engine", qos: .userInitiated)
//From init
private var converter: AudioConvertable!
//Fields
private var currentTimeOffset: TimeInterval = 0
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 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?) {
Log.info(url)
super.init(url: url, delegate: delegate, engineAudioFormat: AudioEngine.defaultEngineAudioFormat)
do {
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat)
} catch {
delegate?.didError()
}
let timeInterval = 1 / (converter.engineAudioFormat.sampleRate / Double(PCM_BUFFER_SIZE))
Timer.scheduledTimer(withTimeInterval: timeInterval / 32, repeats: true) { [weak self] (timer: Timer) in
self?.timer = timer
self?.pollForNextBuffer()
self?.updateNetworkBufferRange()
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 }
do {
let nextScheduledBuffer = try converter.pullBuffer(withSize: PCM_BUFFER_SIZE)
numberOfBuffersScheduledFromPoll += 1
numberOfBuffersScheduledInTotal += 1
Log.debug("processed buffer for engine of frame length \(nextScheduledBuffer.frameLength)")
queue.async { [weak self] in
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
}
}
//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 pollForNextBufferRecursionHelper() {
do {
let nextScheduledBuffer = try converter.pullBuffer(withSize: PCM_BUFFER_SIZE)
Log.debug("processed buffer for engine of frame lengthL \(nextScheduledBuffer.frameLength)")
numberOfBuffersScheduledInTotal += 1
queue.async { [weak self] in
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
}
}
} catch ConverterError.reachedEndOfFile {
Log.info(ConverterError.reachedEndOfFile.localizedDescription)
} catch ConverterError.notEnoughData {
shouldPollForNextBuffer = true
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 > 0 && range.1 > 0) && predictedStreamDuration > 0
Log.debug("loaded \(range), numberOfBuffersScheduledInTotal: \(numberOfBuffersScheduledInTotal), isPlayable: \(isPlayable)")
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: range.0, durationLoadedByNetwork: range.1, 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
if currentTime > predictedStreamDuration {
Log.info("reached end of audio")
seek(toNeedle: 0)
pause()
delegate?.didEndPlaying()
}
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
}
override func invalidate() {
converter.invalidate()
}
}
+209
View File
@@ -0,0 +1,209 @@
//
// 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)
func didUpdate(networkStreamProgress progress: Double)
func shouldProcess(networkData data: Data)
}
protocol AudioThrottleable {
init(withRemoteUrl url: AudioURL, withDelegate delegate: AudioThrottleDelegate)
func tellAudioFormatFound()
func tellByteOffset(offset: UInt64)
func tellSeek(offset: UInt64)
func pollRangeOfBytesAvailable() -> (UInt64, UInt64)
func invalidate()
}
class AudioThrottler: AudioThrottleable {
private class NetworkDataWrapper: NSObject {
let startOffset: UInt
var data: Data
var alreadySent: Bool
var next: NetworkDataWrapper?
var endOffset: UInt {
return startOffset + UInt(data.count) - 1
}
init(startingOffset: UInt, data: Data) {
self.startOffset = startingOffset
self.data = data
self.alreadySent = false
}
func containsOffset(_ offset: UInt) -> Bool {
return startOffset <= offset && offset <= endOffset
}
func isNextSent() -> Bool {
return next?.alreadySent ?? false
}
//FIXME: what is the offset was at the edge of the split? We will have empty data
func splitToRight(atOffset offset: UInt) -> NetworkDataWrapper {
let splitPoint:Int = Int(offset - startOffset)
let leftData = data.subdata(in: 0..<splitPoint)
let rightData = data.subdata(in: splitPoint..<data.count)
data = leftData
let rightWrapper:NetworkDataWrapper = NetworkDataWrapper(startingOffset: offset, data: rightData)
rightWrapper.next = next
next = rightWrapper
return rightWrapper
}
override var description: String {
return "startOffset:\(startOffset), endOffset:\(endOffset), dataCount:\(data.count), sent:\(alreadySent), next:\(next != nil ?"hasNext":"noNext")"
}
}
//Init
let url: AudioURL
weak var delegate: AudioThrottleDelegate?
private var networkData: [NetworkDataWrapper] = []
var shouldThrottle = false
var byteOffsetBecauseOfSeek: UInt = 0
var totalBytesExpected: Int64? //this got sent up twice. Once at beginning of stream and second from network seek. We honor the first send
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())")
self.delegate?.didUpdate(networkStreamProgress: pto.getProgress())
if self.totalBytesExpected == nil, let totalBytesExpected = pto.getTotalBytesExpected() {
self.totalBytesExpected = totalBytesExpected
self.delegate?.didUpdate(totalBytesExpected: totalBytesExpected)
}
let lastItem = self.networkData.last
let startoffset = lastItem == nil ? self.byteOffsetBecauseOfSeek : lastItem!.endOffset + 1
let wrappedNetworkData = NetworkDataWrapper(startingOffset: startoffset, data: pto.getData())
lastItem?.next = wrappedNetworkData
self.networkData.append(wrappedNetworkData)
if !self.shouldThrottle {
Log.debug("sending up packet from stream untrottled at start: \(wrappedNetworkData.startOffset)")
//NOTE: the order here matters.
//We have to set to true before sending up to be processed because
//tellByteOffset() is ran in a separate thread than this one
//We got in a state where 10% of the time an episode will keep polling because
//the first 30 buffers have not been filled
wrappedNetworkData.alreadySent = true
delegate.shouldProcess(networkData: wrappedNetworkData.data)
}
}
}
func tellAudioFormatFound() {
shouldThrottle = true //the above layer has enough info that we can throttle
}
func tellByteOffset(offset: UInt64) {
Log.debug("offset \(offset)")
for wrappedNetworkData in networkData {
if wrappedNetworkData.containsOffset(UInt(offset)) {
Log.debug("offset within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
if wrappedNetworkData.alreadySent {
if !wrappedNetworkData.isNextSent() {
if let next = wrappedNetworkData.next {
Log.debug("Sending next network packet with range: \(next.startOffset) to \(next.endOffset)")
next.alreadySent = true
delegate?.shouldProcess(networkData: next.data)
}
}
return
}
Log.debug("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
wrappedNetworkData.alreadySent = true
return
}
}
}
func tellSeek(offset: UInt64) {
Log.info("seek with offset: \(offset)")
if networkData.count == 0 {
byteOffsetBecauseOfSeek = UInt(offset)
AudioDataManager.shared.seekStream(withRemoteURL: url, toByteOffset: offset)
networkData = []
return
}
if let finalOffset = networkData.last?.endOffset, let firstOffset = networkData.first?.startOffset {
if offset < firstOffset || offset > finalOffset {
byteOffsetBecauseOfSeek = UInt(offset)
AudioDataManager.shared.seekStream(withRemoteURL: url, toByteOffset: offset)
networkData = []
return
}
}
for (i, d) in networkData.enumerated() {
if offset > d.endOffset {
d.alreadySent = false
continue
}
if d.containsOffset(UInt(offset)) {
let wrappedData = d.splitToRight(atOffset: UInt(offset))
networkData.insert(wrappedData, at: i+1)
d.alreadySent = false
wrappedData.alreadySent = true
Log.info("\(d) ::: \(wrappedData)")
delegate?.shouldProcess(networkData: wrappedData.data)
return
}
}
}
func pollRangeOfBytesAvailable() -> (UInt64, UInt64) {
let start = networkData.first?.startOffset ?? 0
let end = networkData.last?.endOffset ?? 0
return (UInt64(start), UInt64(end))
}
func invalidate() {
AudioDataManager.shared.deleteStream(withRemoteURL: url)
}
}
@@ -0,0 +1,187 @@
//
// 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 he have 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) throws
func pullBuffer(withSize size: AVAudioFrameCount) 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
//Field
var converter: AudioConverterRef? //set by AudioConverterNew
var currentAudioPacketIndex: AVAudioPacketCount = 0
required init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat) throws {
self.engineAudioFormat = toEngineAudioFormat
do {
parser = try AudioParser(withRemoteUrl: url, 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(withSize size: AVAudioFrameCount) 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: size) else {
Log.monitor(ConverterError.failedToCreatePCMBuffer.errorDescription as Any)
throw ConverterError.failedToCreatePCMBuffer
}
pcmBuffer.frameLength = size
/**
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 = size / 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 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,114 @@
//
// 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 he have 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:
return "Failed to lock queue"
case .converterFailed(let status):
return localizedDescriptionFromConverterError(status)
case .failedToCreateDestinationFormat:
return "Failed to create a destination (processing) format"
case .failedToCreatePCMBuffer:
return "Failed to create PCM buffer for reading data"
case .notEnoughData:
return "Not enough data for read-conversion operation"
case .parserMissingDataFormat:
return "Parser is missing a valid data format"
case .reachedEndOfFile:
return "Reached the end of the file"
case .unableToCreateConverter(let status):
return localizedDescriptionFromConverterError(status)
case .superConcerningShouldNeverHappen:
return "Weird unexpected reader error. Should not have happened"
case .cannotCreatePCMBufferWithoutConverter:
return "Could not create a PCM Buffer because reader does not have a converter yet"
case .throttleParsingBuffersForEngine:
return "Preventing the reader from creating more PCM buffers since the player has more than 60 seconds of audio already to play"
case .failedToCreateParser:
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,95 @@
//
// 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 he have 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
}
// 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.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
memcpy((ioData.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self))!, bytes, packetByteCount)
})
ioData.pointee.mBuffers.mDataByteSize = UInt32(packetByteCount)
// 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
}
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
}
+56
View File
@@ -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 he have 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)
}
}
+302
View File
@@ -0,0 +1,302 @@
//
// 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 he have 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 {
//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 {
parsedFileAudioFormatCallback(format)
throttler.tellAudioFormatFound()
}
}
}
//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))
}
guard let sizeOfFileInBytes = expectedFileSizeInBytes, 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
numberOfPacketsParsed += 1
}
//TODO: duration will not work with WAV or AIFF
}
}
/**
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
}
init(withRemoteUrl url: AudioURL, parsedFileAudioFormatCallback: @escaping(AVAudioFormat) -> ()) throws {
self.url = url
self.parsedFileAudioFormatCallback = parsedFileAudioFormatCallback
self.throttler = AudioThrottler(withRemoteUrl: url, withDelegate: self)
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
}
}
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data) {
if let offset = getOffset(fromPacketIndex: index) {
throttler.tellByteOffset(offset: offset)
}
// 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
let isEndOfData = packetIndex >= audioPackets.count
if isEndOfData {
if isParsingComplete {
throw ParserError.readerAskingBeyondEndOfFile
} else {
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(audioPackets.count)")
throw ParserError.notEnoughDataForReader
}
}
return audioPackets[Int(packetIndex)]
}
func tellSeek(toIndex index: AVAudioPacketCount) {
//Already within the processed audio packets. Ignore
if indexSeekOffset <= index && index < audioPackets.count + Int(indexSeekOffset) {
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
audioPackets = []
throttler.tellSeek(offset: byteOffset)
}
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")
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 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
*/
}
}
//MARK:- AudioThrottleDelegate
extension AudioParser: AudioThrottleDelegate {
func didUpdate(totalBytesExpected bytes: Int64) {
expectedFileSizeInBytes = UInt64(bytes)
}
func didUpdate(networkStreamProgress progress: Double) {
networkProgress = progress
}
func shouldProcess(networkData data: Data) {
Log.debug("processing data count: \(data.count) :: already had \(audioPackets.count) audio packets")
self.shouldPreventPacketFromFillingUp = false
do {
let sID = self.streamID!
let dataSize = data.count
let _ = try data.withUnsafeBytes({ (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)
}
}
}
@@ -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 he have 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,71 @@
//
// AudioParserPacketListener.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 he have 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 ParserPacketListener(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ streamData: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {
let selfAudioParser = Unmanaged<AudioParser>.fromOpaque(context).takeUnretainedValue()
//bug in core audio where this could be nil
let packetDescriptionOrNil: UnsafeMutablePointer<AudioStreamPacketDescription>? = packetDescriptions
let isCompressed = packetDescriptionOrNil != nil
guard let fileAudioFormat = selfAudioParser.fileAudioFormat else {
Log.monitor("shouldnot 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 isCompressed {
for i in 0 ..< Int(packetCount) {
let audioPacketDescription = packetDescriptions[i]
let audioPacketStart = Int(audioPacketDescription.mStartOffset)
let audioPacketSize = Int(audioPacketDescription.mDataByteSize)
let audioPacketData = Data(bytes: streamData.advanced(by: audioPacketStart), count: audioPacketSize)
selfAudioParser.audioPackets.append((audioPacketDescription,audioPacketData))
}
} else {
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.audioPackets.append((nil, 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 he have 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,55 @@
//
// 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 isPlayable: Bool
public var startingBufferTimePositon: Double {
get {
return startingNeedle
}
}
public var totalDurationBuffered: Double {
get {
return durationLoadedByNetwork
}
}
public var isReadyForPlaying: Bool {
get {
return isPlayable
}
}
public func contains(_ needle: Double) -> Bool {
return needle >= startingNeedle && (needle - startingNeedle) < durationLoadedByNetwork
}
}
+140
View File
@@ -0,0 +1,140 @@
//
// 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 {
@available(iOS 10.0, *)
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo, duration: Duration) {
var nowPlayingInfo:[String : Any] = [:]
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))
nowPlayingInfo[MPMediaItemPropertyArtwork] =
MPMediaItemArtwork(boundsSize: info.artwork.size) { size in
return info.artwork
}
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)
}
}
+215
View File
@@ -0,0 +1,215 @@
//
// 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 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)
func deleteDownload(withRemoteURL url: AudioURL)
}
class AudioDataManager: AudioDataManagable {
var allowCellular: Bool = false
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 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) {
let key = url.key
if FileStorage.Audio.isStored(key) {
globalDownloadProgressCallback(key, 1.0)
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, withResumeData: nil)
}
func deleteDownload(withRemoteURL url: AudioURL) {
downloadWorker.stop(withID: url.key, callback: nil)
FileStorage.Audio.delete(url.key)
}
}
// 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,309 @@
//
// 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, withResumeData data: Data?)
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, withResumeData data: Data? = nil) {
Log.info("paramID: \(id) activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
let temp = activeDownloads.filter { $0.info.id == id }.count
guard temp == 0 else {
return
}
let rank = Date.getUTC()
guard numberOfActive < MAX_CONCURRENT_DOWNLOADS else {
queuedDownloads.update(with: DownloadInfo(id: id, remoteUrl: remoteUrl, rank: rank))
return
}
var task: URLSessionDownloadTask
if let resumeData = data {
task = session.downloadTask(withResumeData: resumeData)
} else {
task = session.downloadTask(with: remoteUrl)
}
task.taskDescription = id
let activeTask = ActiveDownload(info: DownloadInfo(id: id, remoteUrl: remoteUrl, rank: rank), 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
}
}
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)
activeDownloads = activeDownloads.filter { $0 != task }
if let queued = queuedDownloads.popHighestRanked() {
start(withID: queued.id, withRemoteUrl: queued.remoteUrl)
}
}
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 {
let id: ID
let remoteUrl: URL
let rank: Int
}
private class ActiveDownload: Hashable {
var hashValue: Int {
return info.id.hashValue ^ task.hashValue
}
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
}
}
}
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
}
}
extension String {
var pathExtension: String? {
let cleaned = self.replacingOccurrences(of: " ", with: "_")
let ext = URL(string: cleaned)?.pathExtension
return ext == "" ? nil : ext
}
}
+126
View File
@@ -0,0 +1,126 @@
//
// 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.
*/
private static func getUrl(givenAName name: NameFile, inDirectory dir: FileManager.SearchPathDirectory) -> URL {
let directoryPath = NSSearchPathForDirectoriesInDomains(dir, .userDomainMask, true)[0] as String
let url = URL(fileURLWithPath: directoryPath)
return url.appendingPathComponent(name)
}
private static func isStored(_ url: URL) -> Bool{
// https://stackoverflow.com/questions/42897844/swift-3-0-filemanager-fileexistsatpath-always-return-false
// When determining if a file exists, we must use .path not .absolute string!
return FileManager.default.fileExists(atPath: url.path)
}
private static func delete(_ url: URL) {
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 urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
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)"
}
}
}
+44
View File
@@ -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,331 @@
//
// 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") //Probably because of seek
return
}
guard let totalBytesExpected = totalBytesExpectedForCurrentStream, totalBytesExpected > 0 else {
Log.monitor("should not be called 223r2")
return
}
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")
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
}
guard self.task == task else {
Log.error("stream_error not the same task")
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?
}
+43
View File
@@ -0,0 +1,43 @@
//
// 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
public typealias UTC = Int
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
}
}
+198
View File
@@ -0,0 +1,198 @@
//
// 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 static let shared: SAPlayer = SAPlayer()
private var presenter: SAPlayerPresenter!
private var player: AudioEngine?
public var skipForwardSeconds: Double = 30
public var skipBackwardSeconds: Double = 15
public var rate: Double = 1.0 {
didSet {
presenter.handleSetSpeed(withMultiple: rate)
}
}
public var duration: Double {
get {
return presenter.duration ?? 0.0
}
}
public var prettyDuration: String {
get {
return SAPlayer.prettifyTimestamp(duration)
}
}
public var elapsedTime: Double {
get {
return presenter.needle ?? 0
}
}
public var prettyElapsedTime: String {
get {
return SAPlayer.prettifyTimestamp(elapsedTime)
}
}
public var mediaInfo: SALockScreenInfo? = nil {
didSet {
if let info = mediaInfo {
presenter.handleLockscreenInfo(info: info)
}
}
}
private init() {
presenter = SAPlayerPresenter(delegate: self)
}
public static func prettifyTimestamp(_ timestamp: Double) -> String {
let hours = Int(timestamp / 60 / 60)
let minutes = Int((timestamp - Double(hours * 60)) / 60)
let secondsLeft = Int(timestamp) - (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)
}
}
//MARK: - Player Controls
extension SAPlayer {
public func togglePlayAndPause() {
presenter.handleTogglePlayingAndPausing()
}
public func play() {
presenter.handlePlay()
}
public func pause() {
presenter.handlePause()
}
public func skipBackwards() {
presenter.handleSkipBackward()
}
public func skipForward() {
presenter.handleSkipForward()
}
public func seekTo(seconds: Double) {
presenter.handleSeek(toNeedle: seconds)
}
public func initializeAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlayAudio(withRemoteUrl: url)
}
}
extension SAPlayer {
public struct Downloader {
public static func downloadAudio(withRemoteUrl url: URL) {
SAPlayer.shared.addUrlToMapping(url: url)
AudioDataManager.shared.startDownload(withRemoteURL: url)
}
public static func cancelDownload(withRemoteUrl url: URL) {
AudioDataManager.shared.deleteDownload(withRemoteURL: url)
}
public static func deleteDownload(withRemoteUrl url: URL) {
AudioDataManager.shared.deleteDownload(withRemoteURL: url)
}
public static func isDownloaded(withRemoteUrl url: URL) -> Bool {
return AudioDataManager.shared.getPersistedUrl(withRemoteURL: url) != nil
}
public static func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
AudioDataManager.shared.setBackgroundCompletionHandler(completionHandler)
}
}
}
extension SAPlayer: SAPlayerDelegate {
func startAudioDownloaded(withSavedUrl url: AudioURL) {
player?.pause()
player?.invalidate()
player = AudioDiskEngine(withSavedUrl: url, delegate: presenter)
}
func startAudioStreamed(withRemoteUrl url: AudioURL) {
player?.pause()
player?.invalidate()
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter)
}
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: .longForm, options: [])
} else {
// Fallback on earlier versions
}
try AVAudioSession.sharedInstance().setActive(true, with: .notifyOthersOnDeactivation)
} catch {
Log.monitor("Problem setting up AVAudioSession to play in:: \(error.localizedDescription)")
}
}
func pauseEngine() {
player?.pause()
}
func seekEngine(toNeedle needle: Needle) {
player?.seek(toNeedle: needle)
}
func setSpeedEngine(withMultiple multiple: Double) {
player?.setSpeed(speed: multiple)
}
}
+39
View File
@@ -0,0 +1,39 @@
//
// 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 skipForwardSeconds: Double { get set }
var skipBackwardSeconds: Double { get set }
func startAudioDownloaded(withSavedUrl url: AudioURL)
func startAudioStreamed(withRemoteUrl url: AudioURL)
func playEngine()
func pauseEngine()
func seekEngine(toNeedle needle: Needle) //TODO ensure that engine cleans up out of bounds
func setSpeedEngine(withMultiple multiple: Double)
}
+185
View File
@@ -0,0 +1,185 @@
//
// 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 {
weak var delegate: SAPlayerDelegate?
var shouldPlayImmediately = false //for auto-play
var needle: Needle?
var duration: Duration?
private var key: String?
private var isPlaying = false
private var mediaInfo: SALockScreenInfo?
private var urlKeyMap: [Key: URL] = [:]
var durationRef:UInt = 0
var needleRef:UInt = 0
var playingStatusRef:UInt = 0
init(delegate: SAPlayerDelegate?) {
self.delegate = delegate
delegate?.setLockScreenControls(presenter: self)
prepareNextEpisodeToPlay()
}
func getUrl(forKey key: Key) -> URL? {
return urlKeyMap[key]
}
func addUrlToKeyMap(_ url: URL) {
urlKeyMap[url.key] = url
}
func handlePlayAudio(withRemoteUrl url: URL) {
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
self.key = url.key
if let savedUrl = AudioDataManager.shared.getPersistedUrl(withRemoteURL: url) {
self.key = savedUrl.key
urlKeyMap[savedUrl.key] = url
delegate?.startAudioDownloaded(withSavedUrl: savedUrl)
} else {
urlKeyMap[url.key] = url
delegate?.startAudioStreamed(withRemoteUrl: url)
}
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
if let info = self.mediaInfo {
self.delegate?.setLockScreenInfo(withMediaInfo: info, 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
})
}
@available(iOS 10.0, *)
func handleLockscreenInfo(info: SALockScreenInfo) {
self.mediaInfo = info
}
}
//MARK:- Used by outside world including:
// SPP, lock screen, directors
extension SAPlayerPresenter {
func handlePause() {
delegate?.pauseEngine()
}
func handlePlay() {
delegate?.playEngine()
}
func handleTogglePlayingAndPausing() {
if isPlaying {
handlePause()
} else {
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 handleSetSpeed(withMultiple: Double) {
delegate?.setSpeedEngine(withMultiple: withMultiple)
}
}
//MARK:- For lock screen
extension SAPlayerPresenter {
func getIsPlaying() -> Bool {
return isPlaying
}
}
//MARK:- AVAudioEngineDelegate
extension SAPlayerPresenter: AudioEngineDelegate {
func didError() {
Log.monitor("We should have handled engine error")
}
func didEndPlaying() {
// TODO
// playNextEpisode()
}
}
//MARK:- Autoplay
//FIXME: This needs to be refactored
extension SAPlayerPresenter {
func prepareNextEpisodeToPlay() {
// TODO
}
}
+196
View File
@@ -0,0 +1,196 @@
//
// 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 fuction
- 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.
*/
public struct Duration {
/**
Subscribe to updates to changes in duration of the current audio initialized.
Note: It's recommended to have a weak reference to a class that uses this fuction
- 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 fuction
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated duration.
- Parameter playingStatus: Whether the player is playing audio or paused.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
*/
public static func subscribe(_ closure: @escaping (_ url: URL, _ playingStatus: Bool) -> ()) -> UInt {
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: It's recommended to have a weak reference to a class that uses this fuction
- 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 fuction
- 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)
}
}
}
}
+37
View File
@@ -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
+45
View File
@@ -0,0 +1,45 @@
//
// 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."
return UInt(Date().timeIntervalSince1970.bitPattern)
}
/**
- Returns: UTC in seconds.
*/
static func getUTC() -> UTC {
return Int(Date().timeIntervalSince1970)
}
}
@@ -0,0 +1,97 @@
//
// 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] = [:]
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
}
}
}
+190
View File
@@ -0,0 +1,190 @@
//
// Log.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-01-29.
// Copyrights to ColorLog
// https://cocoapods.org/pods/ColorLog
import Foundation
import os.log
class Log {
private init() {}
// Possible levels of log messages to log
public enum LogLevel: Int {
case DEBUG = 1
case INFO = 2
case WARN = 3
case ERROR = 4
case MONITOR = 5
case TEST = 6
}
// 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.
public static var logLevel: LogLevel = LogLevel.ERROR
// 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)")
}
}
/**
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: "MONITOR 🔥🔥🔥🔥")
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)")
}
}
/**
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: " |^| ")
}
}
+30
View File
@@ -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
}
}
}
+2 -2
View File
@@ -28,9 +28,9 @@ SwiftAudioPlayer is a Swift based audio player that can handle streaming from a
s.source = { :git => 'https://github.com/tanhakabir/SwiftAudioPlayer.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '8.0'
s.ios.deployment_target = '10.0'
s.source_files = 'SwiftAudioPlayer/Classes/**/*'
s.source_files = 'Source/**/*'
s.swift_version = '4.0'
# s.resource_bundles = {
View File
View File