Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad63b89ede | |||
| 43e887b823 | |||
| 006b94ea10 | |||
| abb0a29fb4 | |||
| ed3ba9698d | |||
| 294902e3fe | |||
| 764f0b130b | |||
| 952dc1978f | |||
| 1752572770 | |||
| 223600f30c | |||
| 4d90c539cd | |||
| fd9b451f45 | |||
| fd3e78f13c | |||
| af3d553011 | |||
| d0f9127c65 | |||
| 27d5ce4f03 | |||
| 3f93bd1a86 | |||
| 20c0253f68 | |||
| 5e78c446a9 | |||
| 487b071490 | |||
| b79d16b409 | |||
| 4684a92380 | |||
| 2cff597e45 | |||
| c25071d83a | |||
| 98dc7cfa3c | |||
| 4f1242f56d | |||
| f3c91ccc34 | |||
| 2d88b69aa7 | |||
| f67b939ac4 | |||
| a0e9b973e0 | |||
| ef54080a68 | |||
| 2d35bbad59 | |||
| 13b68920d1 | |||
| 2e8f44c553 | |||
| 58ac9b5ae5 | |||
| 706ab5961c | |||
| 19db1fc74b | |||
| 50139ca8c5 | |||
| 6c3e52b66e | |||
| 6d955687a3 | |||
| 38d5740f4d | |||
| 4cbfb4b16b | |||
| 01668790f3 | |||
| 1cf8fb99ba | |||
| 2d3fe83a56 | |||
| 5f63b52592 | |||
| 9111ac6257 | |||
| bfbb979897 | |||
| 0b40a6f0b4 | |||
| f9465f54a0 | |||
| 8ce28db471 | |||
| a84f834f45 | |||
| e3e3af2b7a | |||
| 6987458f0a | |||
| c912d5f381 | |||
| c444ae4c9f | |||
| 6d3f3c6d6f | |||
| bb7f1d1d0a | |||
| 6c446f27e0 | |||
| c513c723ed | |||
| b34a264aec | |||
| a83c2f702f | |||
| 8644bf24fb | |||
| 69a979cb98 | |||
| 6ba43e70ea | |||
| 6f19009000 | |||
| 64677ad6ce | |||
| 3894309706 | |||
| e44f16258f | |||
| 1e3cf35b7b | |||
| 4bfb3f1774 | |||
| e056336955 | |||
| 64d2959a27 | |||
| eb1675d4fd | |||
| ca7e48cbe7 | |||
| 653f2817bc | |||
| edff806647 | |||
| c47d623118 | |||
| b270cf86ab | |||
| 5c2fd7dc97 | |||
| d21ef34392 | |||
| e6d54b0c33 | |||
| 7a1e5bca74 | |||
| 1996812c90 | |||
| 6e1f8f12d4 | |||
| 625e1ab169 | |||
| 52c33518ad | |||
| 3f6fc327ff | |||
| e3e4e4dd46 | |||
| b60e567a83 | |||
| 17e0ee5dd8 |
+16
-32
@@ -7,16 +7,15 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
27E3EC64A90305ACA68AE35A7DC597E0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
|
||||
2A421C2A94DF56A00FF73322C6B470C8 /* SwiftAudioPlayer-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E268C8D5FBBF7E0E790D3AA6A70FEC2 /* SwiftAudioPlayer-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
3A31FEF49CC8C3B757EEB4EBCC9BCCF4 /* Pods-SwiftAudioPlayer_Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 351771425C270B04BF2A07F0262DA192 /* Pods-SwiftAudioPlayer_Tests-dummy.m */; };
|
||||
418D41690EF20077112E2BE86E32FB6A /* Pods-SwiftAudioPlayer_Example-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = AB41D88A2C694FBDF26EA56381EED25F /* Pods-SwiftAudioPlayer_Example-dummy.m */; };
|
||||
79D8DF73FA7CDD6E266BAE71D46E035F /* Pods-SwiftAudioPlayer_Tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
|
||||
8F93DB166237195ED222EE55B6404625 /* Pods-SwiftAudioPlayer_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B0B76CB1439F4D361322144E5A65C3A /* Pods-SwiftAudioPlayer_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
A40DBE292391D9CA00F86146 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40DBE282391D9C900F86146 /* Data.swift */; };
|
||||
A411CE4625F9609D0039E1CD /* SAPlayerFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */; };
|
||||
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */; };
|
||||
A448635026CB783800CFDC29 /* DirectorThreadSafeClosures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A448634F26CB783800CFDC29 /* DirectorThreadSafeClosures.swift */; };
|
||||
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8D2200E00E0018AB51 /* SAPlayer.swift */; };
|
||||
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */; };
|
||||
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */; };
|
||||
@@ -41,18 +40,18 @@
|
||||
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 */; };
|
||||
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosuresDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F872200DAD50018AB51 /* DirectorThreadSafeClosuresDeprecated.swift */; };
|
||||
A4681FE0220113E40018AB51 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F802200D0500018AB51 /* Log.swift */; };
|
||||
A4681FE1220113E70018AB51 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8B2200DDD50018AB51 /* Constants.swift */; };
|
||||
A470FE0825F9ADF800F135FF /* AudioClockDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */; };
|
||||
A470FE0925F9ADF800F135FF /* DownloadProgressDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */; };
|
||||
A470FE1C25F9AEB900F135FF /* AudioQueueDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */; };
|
||||
A470FE2125F9AF1400F135FF /* AudioQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE2025F9AF1400F135FF /* AudioQueue.swift */; };
|
||||
A4827771262A216C00B6918A /* StreamingDownloadDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */; };
|
||||
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */; };
|
||||
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */; };
|
||||
A4FBA6B5221B74C900D5A353 /* SAPlayerHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SAPlayerHelpers.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 */
|
||||
|
||||
@@ -89,7 +88,6 @@
|
||||
509D93CD81F074F6E7C4B9DE13210ACF /* Pods_SwiftAudioPlayer_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-SwiftAudioPlayer_Tests-umbrella.h"; sourceTree = "<group>"; };
|
||||
55AB0CDF00C23619C7F54FE21D0C9534 /* Pods-SwiftAudioPlayer_Example-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Example-frameworks.sh"; sourceTree = "<group>"; };
|
||||
5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
69AF5444212FEC2674325627F26305AD /* Pods-SwiftAudioPlayer_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SwiftAudioPlayer_Example.release.xcconfig"; sourceTree = "<group>"; };
|
||||
6EC04ECC8F7CB2AF2E4E042A6A8ECFA1 /* SwiftAudioPlayer.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; path = SwiftAudioPlayer.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
70839C5AD428953FAF3091E814FF6E31 /* Pods-SwiftAudioPlayer_Example.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-SwiftAudioPlayer_Example.modulemap"; sourceTree = "<group>"; };
|
||||
@@ -103,10 +101,11 @@
|
||||
A40DBE282391D9C900F86146 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
||||
A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerFeatures.swift; sourceTree = "<group>"; };
|
||||
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayingStatus.swift; sourceTree = "<group>"; };
|
||||
A448634F26CB783800CFDC29 /* DirectorThreadSafeClosures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectorThreadSafeClosures.swift; sourceTree = "<group>"; };
|
||||
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = Credited_LICENSE; sourceTree = "<group>"; };
|
||||
A4681F802200D0500018AB51 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
|
||||
A4681F822200D9150018AB51 /* AudioEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEngine.swift; sourceTree = "<group>"; };
|
||||
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectorThreadSafeClosures.swift; sourceTree = "<group>"; };
|
||||
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosuresDeprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectorThreadSafeClosuresDeprecated.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>"; };
|
||||
@@ -135,8 +134,9 @@
|
||||
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadProgressDirector.swift; sourceTree = "<group>"; };
|
||||
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioQueueDirector.swift; sourceTree = "<group>"; };
|
||||
A470FE2025F9AF1400F135FF /* AudioQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioQueue.swift; sourceTree = "<group>"; };
|
||||
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingDownloadDirector.swift; sourceTree = "<group>"; };
|
||||
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerDownloader.swift; sourceTree = "<group>"; };
|
||||
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SALockScreenInfo.swift; sourceTree = "<group>"; };
|
||||
A4FBA6B3221B74C900D5A353 /* SAPlayerHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerHelpers.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>"; };
|
||||
@@ -154,7 +154,6 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B73D01578ABBDB6FF402D868A6C547FF /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -162,7 +161,6 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
27E3EC64A90305ACA68AE35A7DC597E0 /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -170,7 +168,6 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -216,14 +213,6 @@
|
||||
path = "Target Support Files/Pods-SwiftAudioPlayer_Tests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5E0D919E635D23B70123790B8308F8EF /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */,
|
||||
);
|
||||
name = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5F444B7A1C462A30A1CA4CCD3A7CF7B0 /* Targets Support Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -256,7 +245,6 @@
|
||||
children = (
|
||||
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */,
|
||||
D2A5FF8756A6E3EEEA69006E1A3C81F7 /* Development Pods */,
|
||||
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */,
|
||||
21D946895A4F57F51246F3EBCF330719 /* Products */,
|
||||
5F444B7A1C462A30A1CA4CCD3A7CF7B0 /* Targets Support Files */,
|
||||
);
|
||||
@@ -278,10 +266,11 @@
|
||||
children = (
|
||||
A4681F8B2200DDD50018AB51 /* Constants.swift */,
|
||||
A4681F802200D0500018AB51 /* Log.swift */,
|
||||
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */,
|
||||
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosuresDeprecated.swift */,
|
||||
A4681F892200DB3C0018AB51 /* Date.swift */,
|
||||
A4681F962200E2E20018AB51 /* URL.swift */,
|
||||
A40DBE282391D9C900F86146 /* Data.swift */,
|
||||
A448634F26CB783800CFDC29 /* DirectorThreadSafeClosures.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -356,7 +345,7 @@
|
||||
A4681FE2220117B50018AB51 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */,
|
||||
A4FBA6B3221B74C900D5A353 /* SAPlayerHelpers.swift */,
|
||||
A4681F8D2200E00E0018AB51 /* SAPlayer.swift */,
|
||||
A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */,
|
||||
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */,
|
||||
@@ -378,18 +367,11 @@
|
||||
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */,
|
||||
A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */,
|
||||
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */,
|
||||
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */,
|
||||
);
|
||||
path = Directors;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5E0D919E635D23B70123790B8308F8EF /* iOS */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D2A5FF8756A6E3EEEA69006E1A3C81F7 /* Development Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -537,6 +519,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A448635026CB783800CFDC29 /* DirectorThreadSafeClosures.swift in Sources */,
|
||||
A470FE0925F9ADF800F135FF /* DownloadProgressDirector.swift in Sources */,
|
||||
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */,
|
||||
A470FE1C25F9AEB900F135FF /* AudioQueueDirector.swift in Sources */,
|
||||
@@ -547,12 +530,13 @@
|
||||
A4681FCF220113A40018AB51 /* AudioConverterListener.swift in Sources */,
|
||||
A4681FE1220113E70018AB51 /* Constants.swift in Sources */,
|
||||
A40DBE292391D9CA00F86146 /* Data.swift in Sources */,
|
||||
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */,
|
||||
A4FBA6B5221B74C900D5A353 /* SAPlayerHelpers.swift in Sources */,
|
||||
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */,
|
||||
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */,
|
||||
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */,
|
||||
A4681FD5220113BD0018AB51 /* AudioParserErrors.swift in Sources */,
|
||||
A4681FC9220113920018AB51 /* LockScreenViewProtocol.swift in Sources */,
|
||||
A4827771262A216C00B6918A /* StreamingDownloadDirector.swift in Sources */,
|
||||
A4681FD6220113BF0018AB51 /* AudioThrottler.swift in Sources */,
|
||||
A4681FCC2201139B0018AB51 /* AudioDiskEngine.swift in Sources */,
|
||||
A4681FDE220113DE0018AB51 /* Date.swift in Sources */,
|
||||
@@ -574,7 +558,7 @@
|
||||
A4681FCD2201139E0018AB51 /* AudioStreamEngine.swift in Sources */,
|
||||
A411CE4625F9609D0039E1CD /* SAPlayerFeatures.swift in Sources */,
|
||||
A4681FD9220113CD0018AB51 /* AudioStreamWorker.swift in Sources */,
|
||||
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */,
|
||||
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosuresDeprecated.swift in Sources */,
|
||||
A4681FCB220113980018AB51 /* AudioEngine.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
41B4A1BE666DAEDD342DBACF /* Pods_SwiftAudioPlayer_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */; };
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
|
||||
@@ -15,7 +14,6 @@
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
|
||||
607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; };
|
||||
A470FEE2260303DA00F135FF /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FEE1260303DA00F135FF /* Model.swift */; };
|
||||
E5808EC0557FB2395AA56468 /* Pods_SwiftAudioPlayer_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -30,9 +28,7 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0B7D1E6C00E83B4AF8AA1781 /* Pods-SwiftAudioPlayer_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Tests/Pods-SwiftAudioPlayer_Tests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4B5DD2AE0B23A759D18926DC /* Pods-SwiftAudioPlayer_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Example/Pods-SwiftAudioPlayer_Example.release.xcconfig"; sourceTree = "<group>"; };
|
||||
4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACD01AFB9204008FA782 /* SwiftAudioPlayer_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudioPlayer_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -56,7 +52,6 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E5808EC0557FB2395AA56468 /* Pods_SwiftAudioPlayer_Example.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -64,22 +59,12 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
41B4A1BE666DAEDD342DBACF /* Pods_SwiftAudioPlayer_Tests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
408E805A4561B2F63083E539 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */,
|
||||
4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4246ED1215E81CA7B8F0AB36 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -99,7 +84,6 @@
|
||||
607FACE81AFB9204008FA782 /* Tests */,
|
||||
607FACD11AFB9204008FA782 /* Products */,
|
||||
4246ED1215E81CA7B8F0AB36 /* Pods */,
|
||||
408E805A4561B2F63083E539 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -72,8 +72,8 @@
|
||||
<rect key="frame" x="16" y="60" width="343" height="32"/>
|
||||
<segments>
|
||||
<segment title="Soundbite"/>
|
||||
<segment title="Acquired"/>
|
||||
<segment title="Y Combinator"/>
|
||||
<segment title="Podcast"/>
|
||||
<segment title="Radio"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="audioSelected:" destination="vXZ-lx-hvc" eventType="valueChanged" id="oYE-yq-348"/>
|
||||
@@ -116,30 +116,32 @@
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Skip Silences" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="M2y-FP-H1D">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Skip Silences" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="M2y-FP-H1D">
|
||||
<rect key="frame" x="89" y="504" width="101" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="2cn-E5-TeQ">
|
||||
<rect key="frame" x="226" y="499" width="49" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="2cn-E5-TeQ">
|
||||
<rect key="frame" x="226" y="499" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="skipSilencesSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="p7X-Y8-7hO"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="IGe-aU-Y6D">
|
||||
<rect key="frame" x="226" y="540" width="49" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="IGe-aU-Y6D">
|
||||
<rect key="frame" x="226" y="540" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="sleepSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="noa-m8-VHy"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Sleep After 5 s" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vf6-kr-yWa">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sleep After 5 s" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vf6-kr-yWa">
|
||||
<rect key="frame" x="83" y="545" width="112" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Loop" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JOr-pf-CKN">
|
||||
<rect key="frame" x="152" y="588" width="38" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -158,14 +160,22 @@
|
||||
<action selector="streamTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="AXY-N7-87Y"/>
|
||||
</connections>
|
||||
</button>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="cfU-Rp-Kqf">
|
||||
<rect key="frame" x="226" y="583" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="loopSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="psj-Vs-9BI"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="nsl-df-P21" firstAttribute="top" secondItem="y5i-MZ-Qat" secondAttribute="bottom" constant="8" id="0aM-Sz-J9k"/>
|
||||
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="16" id="1wb-IW-jYz"/>
|
||||
<constraint firstItem="j3w-gr-HzF" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="26c-ZJ-768"/>
|
||||
<constraint firstItem="JOr-pf-CKN" firstAttribute="top" secondItem="vf6-kr-yWa" secondAttribute="bottom" constant="22" id="4UI-XL-M9D"/>
|
||||
<constraint firstItem="jUc-tP-CC5" firstAttribute="top" secondItem="KDu-ea-kF8" secondAttribute="bottom" constant="80" id="5sT-An-9vw"/>
|
||||
<constraint firstItem="6d9-Bc-hIz" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="KDu-ea-kF8" secondAttribute="trailing" constant="8" symbolic="YES" id="60t-zV-EiY"/>
|
||||
<constraint firstItem="2cn-E5-TeQ" firstAttribute="centerY" secondItem="M2y-FP-H1D" secondAttribute="centerY" id="6QX-Ru-ZbO"/>
|
||||
<constraint firstItem="joK-xi-MCo" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="7KA-Mg-HFD"/>
|
||||
<constraint firstItem="vfk-OJ-S3T" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="8PP-Pp-1Hc"/>
|
||||
<constraint firstItem="joK-xi-MCo" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="AH1-Uu-eLB"/>
|
||||
@@ -174,19 +184,27 @@
|
||||
<constraint firstItem="Urj-Dv-41y" firstAttribute="centerY" secondItem="j3w-gr-HzF" secondAttribute="centerY" id="Fvd-7V-Rr8"/>
|
||||
<constraint firstItem="1IX-z5-wWx" firstAttribute="leading" secondItem="joK-xi-MCo" secondAttribute="leading" id="GeX-7f-jzu"/>
|
||||
<constraint firstItem="0QE-3F-a4G" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="jUc-tP-CC5" secondAttribute="trailing" constant="8" symbolic="YES" id="JP5-yW-eVB"/>
|
||||
<constraint firstItem="cfU-Rp-Kqf" firstAttribute="leading" secondItem="JOr-pf-CKN" secondAttribute="trailing" constant="36" id="JxU-kl-pkL"/>
|
||||
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="w2a-RA-zmI" secondAttribute="bottom" constant="100" id="K1K-8N-SpD"/>
|
||||
<constraint firstItem="IGe-aU-Y6D" firstAttribute="centerY" secondItem="vf6-kr-yWa" secondAttribute="centerY" id="K1s-td-R7b"/>
|
||||
<constraint firstItem="vf6-kr-yWa" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="83" id="M0b-b2-UnQ"/>
|
||||
<constraint firstItem="vfk-OJ-S3T" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="NOY-IO-NIJ"/>
|
||||
<constraint firstItem="tFH-sY-Xu9" firstAttribute="centerY" secondItem="jUc-tP-CC5" secondAttribute="centerY" id="Rre-EY-kVY"/>
|
||||
<constraint firstItem="KDu-ea-kF8" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="43" id="SRU-sX-z5b"/>
|
||||
<constraint firstItem="cfU-Rp-Kqf" firstAttribute="centerY" secondItem="JOr-pf-CKN" secondAttribute="centerY" id="Tox-y4-XVg"/>
|
||||
<constraint firstItem="w2a-RA-zmI" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="Vki-IZ-AdN"/>
|
||||
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="top" secondItem="j3w-gr-HzF" secondAttribute="bottom" constant="8" id="Wwx-Uo-yIC"/>
|
||||
<constraint firstItem="IGe-aU-Y6D" firstAttribute="leading" secondItem="vf6-kr-yWa" secondAttribute="trailing" constant="31" id="XpW-wP-Iyh"/>
|
||||
<constraint firstItem="vf6-kr-yWa" firstAttribute="top" secondItem="M2y-FP-H1D" secondAttribute="bottom" constant="20" id="Y8L-El-ycq"/>
|
||||
<constraint firstItem="nsl-df-P21" firstAttribute="leading" secondItem="vfk-OJ-S3T" secondAttribute="leading" id="a5C-nZ-8Jc"/>
|
||||
<constraint firstItem="yUQ-mI-ozK" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="a66-h4-WVf"/>
|
||||
<constraint firstItem="Urj-Dv-41y" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="aKt-EV-Bwd"/>
|
||||
<constraint firstItem="tFH-sY-Xu9" firstAttribute="top" secondItem="1IX-z5-wWx" secondAttribute="bottom" constant="27" id="bIq-V0-Sac"/>
|
||||
<constraint firstItem="M2y-FP-H1D" firstAttribute="top" secondItem="vfk-OJ-S3T" secondAttribute="bottom" constant="26" id="bsl-hj-xUt"/>
|
||||
<constraint firstItem="tFH-sY-Xu9" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="62.5" id="cH6-q6-Lel"/>
|
||||
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="nsl-df-P21" secondAttribute="bottom" constant="8" id="cKV-wk-6P9"/>
|
||||
<constraint firstItem="jUc-tP-CC5" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cgM-Nj-yit"/>
|
||||
<constraint firstItem="JOr-pf-CKN" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="152" id="cgd-E2-XpJ"/>
|
||||
<constraint firstItem="KDu-ea-kF8" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="dLw-rF-Pfb"/>
|
||||
<constraint firstItem="w2a-RA-zmI" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="daz-b0-eCC"/>
|
||||
<constraint firstItem="jUc-tP-CC5" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="tFH-sY-Xu9" secondAttribute="trailing" constant="8" symbolic="YES" id="fS9-Ce-4ph"/>
|
||||
@@ -194,11 +212,13 @@
|
||||
<constraint firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" constant="16" id="gdg-7Y-7la"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1IX-z5-wWx" secondAttribute="trailing" constant="16" id="hHM-jO-RZd"/>
|
||||
<constraint firstItem="pVf-cJ-9ca" firstAttribute="centerX" secondItem="joK-xi-MCo" secondAttribute="centerX" id="lOM-Fa-KdR"/>
|
||||
<constraint firstItem="2cn-E5-TeQ" firstAttribute="leading" secondItem="M2y-FP-H1D" secondAttribute="trailing" constant="36" id="laG-3h-LI7"/>
|
||||
<constraint firstItem="6d9-Bc-hIz" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="m9s-An-IWV"/>
|
||||
<constraint firstItem="vfk-OJ-S3T" firstAttribute="top" secondItem="yUQ-mI-ozK" secondAttribute="bottom" constant="8" id="oaW-rr-UVN"/>
|
||||
<constraint firstItem="nsl-df-P21" firstAttribute="trailing" secondItem="vfk-OJ-S3T" secondAttribute="trailing" id="r5e-Wq-dqV"/>
|
||||
<constraint firstItem="y5i-MZ-Qat" firstAttribute="centerX" secondItem="nsl-df-P21" secondAttribute="centerX" id="reC-GA-ZgT"/>
|
||||
<constraint firstAttribute="trailing" secondItem="0QE-3F-a4G" secondAttribute="trailing" constant="62.5" id="tg1-gr-hdd"/>
|
||||
<constraint firstItem="M2y-FP-H1D" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="89" id="vcF-gP-oe0"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6d9-Bc-hIz" secondAttribute="trailing" constant="44" id="vtN-y4-iqp"/>
|
||||
<constraint firstItem="0QE-3F-a4G" firstAttribute="centerY" secondItem="jUc-tP-CC5" secondAttribute="centerY" id="xDi-tj-bBF"/>
|
||||
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="top" secondItem="jUc-tP-CC5" secondAttribute="bottom" constant="40" id="ytQ-s4-kJm"/>
|
||||
@@ -212,6 +232,7 @@
|
||||
<outlet property="currentUrlLocationLabel" destination="1IX-z5-wWx" id="MuO-fF-ZxL"/>
|
||||
<outlet property="downloadButton" destination="KDu-ea-kF8" id="5o4-1h-y06"/>
|
||||
<outlet property="durationLabel" destination="Urj-Dv-41y" id="mIq-eh-int"/>
|
||||
<outlet property="loopSwitch" destination="cfU-Rp-Kqf" id="wTZ-Sr-mV4"/>
|
||||
<outlet property="playPauseButton" destination="jUc-tP-CC5" id="e9C-zV-A1B"/>
|
||||
<outlet property="rateLabel" destination="yUQ-mI-ozK" id="Dx4-lO-A1B"/>
|
||||
<outlet property="rateSlider" destination="vfk-OJ-S3T" id="mNc-ET-aNM"/>
|
||||
|
||||
@@ -7,24 +7,25 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudioPlayer
|
||||
|
||||
struct AudioInfo: Hashable {
|
||||
var index: Int = 0
|
||||
|
||||
var urls: [URL] = [URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!,
|
||||
URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!,
|
||||
URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&sd=1&u=1549423185")!]
|
||||
URL(string: "https://ice6.somafm.com/groovesalad-256-mp3")!]
|
||||
|
||||
var url: URL {
|
||||
switch index {
|
||||
case 0:
|
||||
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
|
||||
return urls[0]
|
||||
case 1:
|
||||
return URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!
|
||||
return urls[1]
|
||||
case 2:
|
||||
return URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&sd=1&u=1549423185")!
|
||||
return urls[2]
|
||||
default:
|
||||
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
|
||||
return urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +34,9 @@ struct AudioInfo: Hashable {
|
||||
case 0:
|
||||
return "Soundbite"
|
||||
case 1:
|
||||
return "Acquired"
|
||||
return "Podcast"
|
||||
case 2:
|
||||
return "Y Combinator"
|
||||
return "Radio"
|
||||
default:
|
||||
return "Soundbite"
|
||||
}
|
||||
@@ -44,6 +45,12 @@ struct AudioInfo: Hashable {
|
||||
let artist: String = "SwiftAudioPlayer Sample App"
|
||||
let releaseDate: Int = 1550790640
|
||||
|
||||
var lockscreenInfo: SALockScreenInfo {
|
||||
get {
|
||||
return SALockScreenInfo(title: self.title, artist: self.artist, albumTitle: nil, artwork: nil, releaseDate: self.releaseDate)
|
||||
}
|
||||
}
|
||||
|
||||
var savedUrl: URL? {
|
||||
get {
|
||||
return savedUrls[index]
|
||||
|
||||
@@ -37,6 +37,7 @@ class ViewController: UIViewController {
|
||||
var isDownloading: Bool = false
|
||||
var isStreaming: Bool = false
|
||||
var beingSeeked: Bool = false
|
||||
var loopEnabled = false
|
||||
|
||||
|
||||
var downloadId: UInt?
|
||||
@@ -69,6 +70,7 @@ class ViewController: UIViewController {
|
||||
super.viewDidLoad()
|
||||
|
||||
SAPlayer.Downloader.allowUsingCellularData = true
|
||||
SAPlayer.shared.HTTPHeaderFields = ["User-Agent": "foobar"]
|
||||
|
||||
// SAPlayer.shared.DEBUG_MODE = true
|
||||
|
||||
@@ -112,27 +114,14 @@ class ViewController: UIViewController {
|
||||
selectedAudio.setIndex(i)
|
||||
|
||||
if selectedAudio.savedUrl != nil {
|
||||
downloadButton.isEnabled = true
|
||||
downloadButton.setTitle("Delete downloaded", for: .normal)
|
||||
streamButton.isEnabled = false
|
||||
} else {
|
||||
downloadButton.isEnabled = true
|
||||
downloadButton.setTitle("Download", for: .normal)
|
||||
streamButton.isEnabled = true
|
||||
}
|
||||
|
||||
if let savedUrl = selectedAudio.savedUrl {
|
||||
self.currentUrlLocationLabel.text = "saved url: \(savedUrl.absoluteString)"
|
||||
} else {
|
||||
self.currentUrlLocationLabel.text = "remote url: \(selectedAudio.url.absoluteString)"
|
||||
}
|
||||
|
||||
// if let savedUrl = savedUrls[selectedAudio] {}
|
||||
scrubberSlider.value = 0
|
||||
bufferProgress.progress = 0
|
||||
|
||||
// unsubscribeFromChanges()
|
||||
// subscribeToChanges()
|
||||
|
||||
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
|
||||
}
|
||||
|
||||
func checkIfAudioDownloaded() {
|
||||
@@ -144,16 +133,14 @@ class ViewController: UIViewController {
|
||||
}
|
||||
|
||||
func subscribeToChanges() {
|
||||
durationId = SAPlayer.Updates.Duration.subscribe { [weak self] (url, duration) in
|
||||
durationId = SAPlayer.Updates.Duration.subscribe { [weak self] (duration) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
|
||||
self.durationLabel.text = SAPlayer.prettifyTimestamp(duration)
|
||||
self.duration = duration
|
||||
}
|
||||
|
||||
elapsedId = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
|
||||
elapsedId = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (position) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
|
||||
|
||||
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
|
||||
|
||||
@@ -175,11 +162,8 @@ class ViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
bufferId = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
|
||||
bufferId = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (buffer) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
|
||||
|
||||
if self.duration == 0.0 { return }
|
||||
|
||||
self.bufferProgress.progress = Float(buffer.bufferingProgress)
|
||||
|
||||
@@ -192,9 +176,8 @@ class ViewController: UIViewController {
|
||||
self.isPlayable = buffer.isReadyForPlaying
|
||||
}
|
||||
|
||||
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (url, playing) in
|
||||
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (playing) in
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
|
||||
|
||||
self.playbackStatus = playing
|
||||
|
||||
@@ -212,19 +195,22 @@ class ViewController: UIViewController {
|
||||
self.playPauseButton.setTitle("Loading", for: .normal)
|
||||
return
|
||||
case .ended:
|
||||
self.isPlayable = false
|
||||
self.playPauseButton.setTitle("Done", for: .normal)
|
||||
if !self.loopEnabled {
|
||||
self.isPlayable = false
|
||||
self.playPauseButton.setTitle("Done", for: .normal)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
queueId = SAPlayer.Updates.AudioQueue.subscribe { [weak self] key, forthcomingPlaybackUrl in
|
||||
queueId = SAPlayer.Updates.AudioQueue.subscribe { [weak self] forthcomingPlaybackUrl in
|
||||
guard let self = self else { return }
|
||||
/// we update the selected audio. this is a little contrived, but allows us to update outlets
|
||||
if let indexFound = self.selectedAudio.getIndex(forURL: forthcomingPlaybackUrl) {
|
||||
self.selectAudio(atIndex: indexFound)
|
||||
}
|
||||
print("💥 Received queue update 💥")
|
||||
|
||||
self.currentUrlLocationLabel.text = "\(forthcomingPlaybackUrl.absoluteString)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +245,12 @@ class ViewController: UIViewController {
|
||||
@IBAction func rateChanged(_ sender: Any) {
|
||||
let speed = rateSlider.value
|
||||
rateLabel.text = "rate: \(speed)x"
|
||||
SAPlayer.shared.rate = speed
|
||||
|
||||
if skipSilencesSwitch.isOn {
|
||||
SAPlayer.Features.SkipSilences.setRateSafely(speed) // if using Skip Silences, we need use this version of setting rate to safely change the rate with the feature enabled.
|
||||
} else {
|
||||
SAPlayer.shared.rate = speed
|
||||
}
|
||||
}
|
||||
@IBAction func reverbChanged(_ sender: Any) {
|
||||
let reverb = reverbSlider.value
|
||||
@@ -294,9 +285,6 @@ class ViewController: UIViewController {
|
||||
DispatchQueue.main.async {
|
||||
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
|
||||
self.selectedAudio.addSavedUrl(url)
|
||||
|
||||
SAPlayer.shared.startSavedAudio(withSavedUrl: url)
|
||||
self.lastPlayedAudioIndex = self.selectedAudio.index
|
||||
}
|
||||
})
|
||||
streamButton.isEnabled = false
|
||||
@@ -311,7 +299,13 @@ class ViewController: UIViewController {
|
||||
|
||||
@IBAction func streamTouched(_ sender: Any) {
|
||||
if !isStreaming {
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
|
||||
self.currentUrlLocationLabel.text = "remote url: \(selectedAudio.url.absoluteString)"
|
||||
if selectedAudio.index == 2 { // radio
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, bitrate: .low, mediaInfo: selectedAudio.lockscreenInfo)
|
||||
} else {
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, mediaInfo: selectedAudio.lockscreenInfo)
|
||||
}
|
||||
|
||||
lastPlayedAudioIndex = selectedAudio.index
|
||||
streamButton.setTitle("Cancel streaming", for: .normal)
|
||||
downloadButton.isEnabled = false
|
||||
@@ -325,16 +319,6 @@ class ViewController: UIViewController {
|
||||
}
|
||||
|
||||
@IBAction func playPauseTouched(_ sender: Any) {
|
||||
// if lastPlayedAudioIndex != selectedAudio.index {
|
||||
// if let savedUrl = selectedAudio.savedUrl {
|
||||
// SAPlayer.shared.startSavedAudio(withSavedUrl: savedUrl)
|
||||
// } else {
|
||||
// SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
|
||||
// }
|
||||
//
|
||||
// return
|
||||
// }
|
||||
|
||||
SAPlayer.shared.togglePlayAndPause()
|
||||
}
|
||||
|
||||
@@ -377,5 +361,18 @@ class ViewController: UIViewController {
|
||||
_ = SAPlayer.Features.SleepTimer.disable()
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet weak var loopSwitch: UISwitch!
|
||||
|
||||
@IBAction func loopSwitched(_ sender: Any) {
|
||||
loopEnabled = loopSwitch.isOn
|
||||
|
||||
if loopSwitch.isOn {
|
||||
SAPlayer.Features.Loop.enable()
|
||||
} else {
|
||||
SAPlayer.Features.Loop.disable()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// swift-tools-version:5.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftAudioPlayer",
|
||||
platforms: [
|
||||
.iOS(.v10), .tvOS(.v10)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "SwiftAudioPlayer",
|
||||
targets: ["SwiftAudioPlayer"])
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "SwiftAudioPlayer",
|
||||
path: "Source"
|
||||
)
|
||||
],
|
||||
swiftLanguageVersions: [.v5]
|
||||
)
|
||||
@@ -14,6 +14,7 @@ Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbo
|
||||
|
||||
1. Realtime audio manipulation that includes going up to 10x speed, using [equalizers and other manipulations](https://developer.apple.com/documentation/avfaudio/avaudiouniteq)
|
||||
1. Stream online audio using AVAudioEngine
|
||||
1. Stream radio
|
||||
1. Play locally saved audio with the same API
|
||||
1. Download audio
|
||||
1. Queue up downloaded and streamed audio for autoplay
|
||||
@@ -24,6 +25,7 @@ Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbo
|
||||
These are community supported audio manipulation features using this audio engine. You can implement your own version of these features and you can look at [SAPlayerFeatures](https://github.com/tanhakabir/SwiftAudioPlayer/blob/master/Source/SAPlayerFeatures.swift) to learn how they were implemented using the library.
|
||||
1. Skip silences in audio
|
||||
1. Sleep timer to stop playing audio after a delay
|
||||
1. Loop audio playback for both streamed and saved audio
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -76,19 +78,16 @@ To receive streaming progress (for buffer progress %):
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
|
||||
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] 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.bufferProgress.progress = Float(buffer.bufferingProgress)
|
||||
|
||||
self.isPlayable = buffer.isReadyForPlaying
|
||||
}
|
||||
}
|
||||
```
|
||||
Look at the [Updates](#SAPlayer.Updates) section to see usage details and other updates to follow.
|
||||
Look at the [Updates](#saplayerupdates) section to see usage details and other updates to follow.
|
||||
|
||||
|
||||
For realtime audio manipulations, [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes are used. For example to adjust the reverb through a slider in the UI:
|
||||
@@ -113,18 +112,12 @@ For a more detailed explanation on usage, look at the [Realtime Audio Manipulati
|
||||
|
||||
For more details and specifics look at the [API documentation](#api-in-detail) below.
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
### Issues
|
||||
### Issues or questions
|
||||
|
||||
Submit any issues or requests [on the Github repo](https://github.com/tanhakabir/SwiftAudioPlayer/issues).
|
||||
|
||||
### Any questions?
|
||||
|
||||
Feel free to reach out to either of us:
|
||||
|
||||
[tanhakabir](https://github.com/tanhakabir), tanhakabir.ca@gmail.com
|
||||
[JonMercer](https://github.com/JonMercer), mercer.jon@gmail.com
|
||||
Submit any issues, requests, and questions [on the Github repo](https://github.com/tanhakabir/SwiftAudioPlayer/issues).
|
||||
|
||||
### License
|
||||
|
||||
@@ -146,9 +139,9 @@ Known supported file types are `.mp3` and `.wav`.
|
||||
|
||||
To set up player with audio to play, use either:
|
||||
* `startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
|
||||
* `startRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
|
||||
* `startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
|
||||
|
||||
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen.
|
||||
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen. For streamed audio you can optionally set the bitrate to be `.high` or `.low`. High is more performant but won't work well for radio streams; for radio streams you should use low. The default bitrate if you don't set it is `.high`.
|
||||
|
||||
For streaming remote audio, subscribe to `SAPlayer.Updates.StreamingBuffer` for updates on streaming progress.
|
||||
|
||||
@@ -172,6 +165,8 @@ SAPlayer.shared.queueSavedAudio(withSavedUrl: C://random_folder/audio.mp3) // or
|
||||
SAPlayer.shared.queueRemoteAudio(withRemoteUrl: https://randomwebsite.com/audio.mp3)
|
||||
```
|
||||
|
||||
You can also directly access and modify the queue from `SAPlayer.shared.audioQueued`.
|
||||
|
||||
#### Important
|
||||
|
||||
The engine can handle audio manipulations like speed, pitch, effects, etc. To do this, nodes for effects must be finalized before initialize is called. Look at [audio manipulation documentation](#realtime-audio-manipulation) for more information.
|
||||
@@ -261,14 +256,16 @@ Receive updates for changing values from the player, such as the duration, elaps
|
||||
|
||||
All subscription functions for updates take the form of:
|
||||
```swift
|
||||
func subscribe(_ closure: @escaping (_ url: URL, _ payload: <Payload>) -> ()) -> UInt
|
||||
func subscribe(_ closure: @escaping (_ payload: <Payload>) -> ()) -> UInt
|
||||
```
|
||||
|
||||
- `closure`: The closure that will receive the updates. It's recommended to have a weak reference to a class that uses these functions.
|
||||
- `url`: The corresponding remote URL for the update. In the case there might be multiple files observed, such as downloading many files at once or switching over from playing one audio to another and the updates corresponding to the previous aren't silenced on switch-over.
|
||||
- `payload`: The updated value.
|
||||
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
|
||||
|
||||
Sometimes there is:
|
||||
- `url`: The corresponding remote URL for the update. In the case there might be multiple files observed, such as downloading many files at once.
|
||||
|
||||
Similarily unsubscribe takes the form of:
|
||||
```swift
|
||||
func unsubscribe(_ id: UInt)
|
||||
@@ -306,6 +303,11 @@ Payload = `Double`
|
||||
|
||||
Changes in the progress of downloading audio in the background. This does not correspond to progress in streaming downloads, look at StreamingBuffer for streaming progress.
|
||||
|
||||
### AudioQueue
|
||||
Payload = `URL`
|
||||
|
||||
Notification of the URL of the upcoming audio to be played. This URL may be remote or locally saved.
|
||||
|
||||
## Audio Effects
|
||||
|
||||
### Realtime Audio Manipulation
|
||||
|
||||
@@ -28,6 +28,12 @@ import CoreMedia
|
||||
|
||||
class AudioClockDirector {
|
||||
static let shared = AudioClockDirector()
|
||||
private var currentAudioKey: Key?
|
||||
|
||||
private var depNeedleClosures: DirectorThreadSafeClosuresDeprecated<Needle> = DirectorThreadSafeClosuresDeprecated()
|
||||
private var depDurationClosures: DirectorThreadSafeClosuresDeprecated<Duration> = DirectorThreadSafeClosuresDeprecated()
|
||||
private var depPlayingStatusClosures: DirectorThreadSafeClosuresDeprecated<SAPlayingStatus> = DirectorThreadSafeClosuresDeprecated()
|
||||
private var depBufferClosures: DirectorThreadSafeClosuresDeprecated<SAAudioAvailabilityRange> = DirectorThreadSafeClosuresDeprecated()
|
||||
|
||||
private var needleClosures: DirectorThreadSafeClosures<Needle> = DirectorThreadSafeClosures()
|
||||
private var durationClosures: DirectorThreadSafeClosures<Duration> = DirectorThreadSafeClosures()
|
||||
@@ -36,9 +42,23 @@ class AudioClockDirector {
|
||||
|
||||
private init() {}
|
||||
|
||||
func create() {}
|
||||
func setKey(_ key: Key) {
|
||||
currentAudioKey = key
|
||||
}
|
||||
|
||||
func resetCache() {
|
||||
needleClosures.resetCache()
|
||||
durationClosures.resetCache()
|
||||
playingStatusClosures.resetCache()
|
||||
bufferClosures.resetCache()
|
||||
}
|
||||
|
||||
func clear() {
|
||||
depNeedleClosures.clear()
|
||||
depDurationClosures.clear()
|
||||
depPlayingStatusClosures.clear()
|
||||
depBufferClosures.clear()
|
||||
|
||||
needleClosures.clear()
|
||||
durationClosures.clear()
|
||||
playingStatusClosures.clear()
|
||||
@@ -48,43 +68,67 @@ class AudioClockDirector {
|
||||
// MARK: - Attaches
|
||||
|
||||
// Needle
|
||||
@available(*, deprecated, message: "Use subscribe without key in the closure for current audio updates")
|
||||
func attachToChangesInNeedle(closure: @escaping (Key, Needle) throws -> Void) -> UInt {
|
||||
return depNeedleClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func attachToChangesInNeedle(closure: @escaping (Needle) throws -> Void) -> UInt {
|
||||
return needleClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
|
||||
// Duration
|
||||
@available(*, deprecated, message: "Use subscribe without key in the closure for current audio updates")
|
||||
func attachToChangesInDuration(closure: @escaping (Key, Duration) throws -> Void) -> UInt {
|
||||
return depDurationClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func attachToChangesInDuration(closure: @escaping (Duration) throws -> Void) -> UInt {
|
||||
return durationClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
|
||||
// Playing status
|
||||
@available(*, deprecated, message: "Use subscribe without key in the closure for current audio updates")
|
||||
func attachToChangesInPlayingStatus(closure: @escaping (Key, SAPlayingStatus) throws -> Void) -> UInt{
|
||||
return depPlayingStatusClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func attachToChangesInPlayingStatus(closure: @escaping (SAPlayingStatus) throws -> Void) -> UInt{
|
||||
return playingStatusClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
|
||||
// Buffer
|
||||
@available(*, deprecated, message: "Use subscribe without key in the closure for current audio updates")
|
||||
func attachToChangesInBufferedRange(closure: @escaping (Key, SAAudioAvailabilityRange) throws -> Void) -> UInt{
|
||||
return depBufferClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func attachToChangesInBufferedRange(closure: @escaping (SAAudioAvailabilityRange) throws -> Void) -> UInt{
|
||||
return bufferClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Detaches
|
||||
func detachFromChangesInNeedle(withID id: UInt) {
|
||||
depNeedleClosures.detach(id: id)
|
||||
needleClosures.detach(id: id)
|
||||
}
|
||||
|
||||
func detachFromChangesInDuration(withID id: UInt) {
|
||||
depDurationClosures.detach(id: id)
|
||||
durationClosures.detach(id: id)
|
||||
}
|
||||
|
||||
func detachFromChangesInPlayingStatus(withID id: UInt) {
|
||||
depPlayingStatusClosures.detach(id: id)
|
||||
playingStatusClosures.detach(id: id)
|
||||
}
|
||||
|
||||
func detachFromChangesInBufferedRange(withID id: UInt) {
|
||||
depBufferClosures.detach(id: id)
|
||||
bufferClosures.detach(id: id)
|
||||
}
|
||||
}
|
||||
@@ -92,24 +136,44 @@ class AudioClockDirector {
|
||||
// MARK: - Receives notifications from AudioEngine on ticks
|
||||
extension AudioClockDirector {
|
||||
func needleTick(_ key: Key, needle: Needle) {
|
||||
needleClosures.broadcast(key: key, payload: needle)
|
||||
guard key == currentAudioKey else {
|
||||
Log.debug("silence old updates")
|
||||
return
|
||||
}
|
||||
depNeedleClosures.broadcast(key: key, payload: needle)
|
||||
needleClosures.broadcast(payload: needle)
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioClockDirector {
|
||||
func durationWasChanged(_ key: Key, duration: Duration) {
|
||||
durationClosures.broadcast(key: key, payload: duration)
|
||||
guard key == currentAudioKey else {
|
||||
Log.debug("silence old updates")
|
||||
return
|
||||
}
|
||||
depDurationClosures.broadcast(key: key, payload: duration)
|
||||
durationClosures.broadcast(payload: duration)
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioClockDirector {
|
||||
func audioPlayingStatusWasChanged(_ key: Key, status: SAPlayingStatus) {
|
||||
playingStatusClosures.broadcast(key: key, payload: status)
|
||||
guard key == currentAudioKey else {
|
||||
Log.debug("silence old updates")
|
||||
return
|
||||
}
|
||||
depPlayingStatusClosures.broadcast(key: key, payload: status)
|
||||
playingStatusClosures.broadcast(payload: status)
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioClockDirector {
|
||||
func changeInAudioBuffered(_ key: Key, buffered: SAAudioAvailabilityRange) {
|
||||
bufferClosures.broadcast(key: key, payload: buffered)
|
||||
guard key == currentAudioKey else {
|
||||
Log.debug("silence old updates")
|
||||
return
|
||||
}
|
||||
depBufferClosures.broadcast(key: key, payload: buffered)
|
||||
bufferClosures.broadcast(payload: buffered)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class AudioQueueDirector {
|
||||
closures.clear()
|
||||
}
|
||||
|
||||
func attach(closure: @escaping (Key, URL) throws -> Void) -> UInt {
|
||||
func attach(closure: @escaping (URL) throws -> Void) -> UInt {
|
||||
return closures.attach(closure: closure)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class AudioQueueDirector {
|
||||
closures.detach(id: id)
|
||||
}
|
||||
|
||||
func changeInQueue(_ key: Key, url: URL) {
|
||||
closures.broadcast(key: key, payload: url)
|
||||
func changeInQueue(url: URL) {
|
||||
closures.broadcast(payload: url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import Foundation
|
||||
class DownloadProgressDirector {
|
||||
static let shared = DownloadProgressDirector()
|
||||
|
||||
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
|
||||
var closures: DirectorThreadSafeClosuresDeprecated<Double> = DirectorThreadSafeClosuresDeprecated()
|
||||
|
||||
private init() {
|
||||
AudioDataManager.shared.attach { [weak self] (key, progress) in
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// StreamingDownloadDirector.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 4/16/21.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
class StreamingDownloadDirector {
|
||||
static let shared = StreamingDownloadDirector()
|
||||
private var currentAudioKey: Key?
|
||||
|
||||
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
|
||||
|
||||
private init() {}
|
||||
|
||||
func setKey(_ key: Key) {
|
||||
currentAudioKey = key
|
||||
}
|
||||
|
||||
func resetCache() {
|
||||
closures.resetCache()
|
||||
}
|
||||
|
||||
func clear() {
|
||||
closures.clear()
|
||||
}
|
||||
|
||||
func attach(closure: @escaping (Double) throws -> Void) -> UInt {
|
||||
return closures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func detach(withID id: UInt) {
|
||||
closures.detach(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension StreamingDownloadDirector {
|
||||
func didUpdate(_ key: Key, networkStreamProgress: Double) {
|
||||
guard key == currentAudioKey else {
|
||||
Log.debug("silence old updates")
|
||||
return
|
||||
}
|
||||
|
||||
closures.broadcast(payload: networkStreamProgress)
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,6 @@ class AudioDiskEngine: AudioEngine {
|
||||
|
||||
doRepeatedly(timeInterval: 0.2) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard self.playingStatus != .ended else { return }
|
||||
|
||||
self.updateIsPlaying()
|
||||
self.updateNeedle()
|
||||
@@ -116,10 +115,11 @@ class AudioDiskEngine: AudioEngine {
|
||||
}
|
||||
|
||||
let playing = playerNode.isPlaying
|
||||
let seekToNeedle = needle > Needle(duration) ? Needle(duration) : needle
|
||||
|
||||
self.needle = needle // to tick while paused
|
||||
self.needle = seekToNeedle // to tick while paused
|
||||
|
||||
seekFrame = AVAudioFramePosition(Float(needle) * audioSampleRate)
|
||||
seekFrame = AVAudioFramePosition(Float(seekToNeedle) * audioSampleRate)
|
||||
seekFrame = max(seekFrame, 0)
|
||||
seekFrame = min(seekFrame, audioLengthSamples)
|
||||
currentPosition = seekFrame
|
||||
|
||||
@@ -45,6 +45,7 @@ class AudioEngine: AudioEngineProtocol {
|
||||
|
||||
var engine: AVAudioEngine!
|
||||
var playerNode: AVAudioPlayerNode!
|
||||
private var engineInvalidated: Bool = false
|
||||
|
||||
static let defaultEngineAudioFormat: AVAudioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
|
||||
|
||||
@@ -102,6 +103,8 @@ class AudioEngine: AudioEngineProtocol {
|
||||
AudioClockDirector.shared.changeInAudioBuffered(key, buffered: bufferedSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
private var audioModifiers: [AVAudioUnit]?
|
||||
|
||||
init(url: AudioURL, delegate:AudioEngineDelegate?, engineAudioFormat: AVAudioFormat) {
|
||||
self.key = url.key
|
||||
@@ -115,39 +118,40 @@ class AudioEngine: AudioEngineProtocol {
|
||||
|
||||
func initHelper(_ engineAudioFormat: AVAudioFormat) {
|
||||
engine.attach(playerNode)
|
||||
|
||||
for node in SAPlayer.shared.audioModifiers {
|
||||
engine.attach(node)
|
||||
}
|
||||
|
||||
if SAPlayer.shared.audioModifiers.count > 0 {
|
||||
var i = 0
|
||||
|
||||
let node = SAPlayer.shared.audioModifiers[i]
|
||||
engine.connect(playerNode, to: node, format: engineAudioFormat)
|
||||
|
||||
i += 1
|
||||
|
||||
while i < SAPlayer.shared.audioModifiers.count {
|
||||
let lastNode = SAPlayer.shared.audioModifiers[i - 1]
|
||||
let currNode = SAPlayer.shared.audioModifiers[i]
|
||||
|
||||
engine.connect(lastNode, to: currNode, format: engineAudioFormat)
|
||||
i += 1
|
||||
}
|
||||
|
||||
let finalNode = SAPlayer.shared.audioModifiers[SAPlayer.shared.audioModifiers.count - 1]
|
||||
|
||||
engine.connect(finalNode, to: engine.mainMixerNode, format: engineAudioFormat)
|
||||
} else {
|
||||
audioModifiers = SAPlayer.shared.audioModifiers
|
||||
|
||||
defer { engine.prepare() }
|
||||
|
||||
guard let audioModifiers = audioModifiers, audioModifiers.count > 0 else {
|
||||
engine.connect(playerNode, to: engine.mainMixerNode, format: engineAudioFormat)
|
||||
return
|
||||
}
|
||||
|
||||
engine.prepare()
|
||||
audioModifiers.forEach { engine.attach($0) }
|
||||
|
||||
var i = 0
|
||||
|
||||
let node = audioModifiers[i]
|
||||
engine.connect(playerNode, to: node, format: engineAudioFormat)
|
||||
|
||||
i += 1
|
||||
|
||||
while i < audioModifiers.count {
|
||||
let lastNode = audioModifiers[i - 1]
|
||||
let currNode = audioModifiers[i]
|
||||
|
||||
engine.connect(lastNode, to: currNode, format: engineAudioFormat)
|
||||
i += 1
|
||||
}
|
||||
|
||||
let finalNode = audioModifiers[audioModifiers.count - 1]
|
||||
|
||||
engine.connect(finalNode, to: engine.mainMixerNode, format: engineAudioFormat)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if state == .resumed {
|
||||
playerNode.stop()
|
||||
engine.stop()
|
||||
}
|
||||
|
||||
@@ -172,7 +176,7 @@ class AudioEngine: AudioEngineProtocol {
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] (timer: Timer) in
|
||||
guard let self = self else { return }
|
||||
guard self.playingStatus != .ended else {
|
||||
guard !self.engineInvalidated else {
|
||||
self.delegate = nil
|
||||
return
|
||||
}
|
||||
@@ -193,8 +197,6 @@ class AudioEngine: AudioEngineProtocol {
|
||||
|
||||
let isPlaying = engine.isRunning && playerNode.isPlaying
|
||||
playingStatus = isPlaying ? .playing : .paused
|
||||
|
||||
// playingStatus = .paused
|
||||
}
|
||||
|
||||
func play() {
|
||||
@@ -230,6 +232,13 @@ class AudioEngine: AudioEngineProtocol {
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
|
||||
engineInvalidated = true
|
||||
playerNode.stop()
|
||||
engine.stop()
|
||||
|
||||
if let audioModifiers = audioModifiers, audioModifiers.count > 0 {
|
||||
audioModifiers.forEach { engine.detach($0) }
|
||||
}
|
||||
Log.info("invalidated engine for key \(key)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class AudioStreamEngine: AudioEngine {
|
||||
//Constants
|
||||
private let MAX_POLL_BUFFER_COUNT = 300 //Having one buffer in engine at a time is choppy.
|
||||
private let MIN_BUFFERS_TO_BE_PLAYABLE = 1
|
||||
private let PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
|
||||
private var PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
|
||||
|
||||
private let queue = DispatchQueue(label: "SwiftAudioPlayer.StreamEngine", qos: .userInitiated)
|
||||
|
||||
@@ -68,17 +68,24 @@ class AudioStreamEngine: AudioEngine {
|
||||
|
||||
//Fields
|
||||
private var currentTimeOffset: TimeInterval = 0
|
||||
private var streamChangeListenerId: UInt?
|
||||
|
||||
private var numberOfBuffersScheduledInTotal = 0 {
|
||||
didSet {
|
||||
Log.debug("number of buffers scheduled in total: \(numberOfBuffersScheduledInTotal)")
|
||||
if numberOfBuffersScheduledInTotal == 0 {
|
||||
if playingStatus == .playing { wasPlaying = true }
|
||||
pause()
|
||||
// delegate?.didError()
|
||||
// TODO: we should not have an error here. We should instead have the throttler
|
||||
// propegate when it doesn't enough buffers while they were playing
|
||||
// TODO: "Make this a legitimate warning to user about needing more data from stream"
|
||||
}
|
||||
|
||||
if numberOfBuffersScheduledInTotal > MIN_BUFFERS_TO_BE_PLAYABLE && wasPlaying {
|
||||
wasPlaying = false
|
||||
play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,38 +140,69 @@ class AudioStreamEngine: AudioEngine {
|
||||
}
|
||||
}
|
||||
|
||||
init(withRemoteUrl url: AudioURL, delegate:AudioEngineDelegate?) {
|
||||
init(withRemoteUrl url: AudioURL, delegate:AudioEngineDelegate?, bitrate: SAPlayerBitrate) {
|
||||
Log.info(url)
|
||||
super.init(url: url, delegate: delegate, engineAudioFormat: AudioEngine.defaultEngineAudioFormat)
|
||||
|
||||
switch bitrate {
|
||||
case .high:
|
||||
PCM_BUFFER_SIZE = 8192
|
||||
case .low:
|
||||
PCM_BUFFER_SIZE = 4096
|
||||
}
|
||||
|
||||
do {
|
||||
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat, withPCMBufferSize: PCM_BUFFER_SIZE)
|
||||
} catch {
|
||||
delegate?.didError()
|
||||
}
|
||||
|
||||
StreamingDownloadDirector.shared.setKey(key)
|
||||
StreamingDownloadDirector.shared.resetCache()
|
||||
|
||||
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (progress) in
|
||||
guard let self = self else { return }
|
||||
|
||||
// polling for buffers when we receive data. This won't be throttled on fresh new audio or seeked audio but in all other cases it most likely will be throttled
|
||||
self.pollForNextBuffer() // no buffer updates because thread issues if I try to update buffer status in streaming listener
|
||||
}
|
||||
|
||||
|
||||
let timeInterval = 1 / (converter.engineAudioFormat.sampleRate / Double(PCM_BUFFER_SIZE))
|
||||
|
||||
doRepeatedly(timeInterval: timeInterval) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard self.playingStatus != .ended else { return }
|
||||
|
||||
self.pollForNextBufferRecursive()
|
||||
self.updateNetworkBufferRange()
|
||||
self.updateNeedle()
|
||||
self.updateIsPlaying()
|
||||
self.updateDuration()
|
||||
self.repeatedUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let id = streamChangeListenerId {
|
||||
StreamingDownloadDirector.shared.detach(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
private func repeatedUpdates() {
|
||||
self.pollForNextBuffer()
|
||||
self.updateNetworkBufferRange() // thread issues if I try to update buffer status in streaming listener
|
||||
self.updateNeedle()
|
||||
self.updateIsPlaying()
|
||||
self.updateDuration()
|
||||
}
|
||||
|
||||
//MARK:- Timer loop
|
||||
|
||||
//Called when
|
||||
//1. First time audio is finally parsed
|
||||
//2. When we run to the end of the network buffer and we're waiting again
|
||||
private func pollForNextBufferRecursive() {
|
||||
private func pollForNextBuffer() {
|
||||
guard shouldPollForNextBuffer else { return }
|
||||
|
||||
pollForNextBufferRecursive()
|
||||
}
|
||||
|
||||
private func pollForNextBufferRecursive() {
|
||||
do {
|
||||
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer()
|
||||
numberOfBuffersScheduledFromPoll += 1
|
||||
@@ -172,9 +210,9 @@ class AudioStreamEngine: AudioEngine {
|
||||
|
||||
Log.debug("processed buffer for engine of frame length \(nextScheduledBuffer.frameLength)")
|
||||
queue.async { [weak self] in
|
||||
if #available(iOS 11.0, *) {
|
||||
if #available(iOS 11.0, tvOS 11.0, *) {
|
||||
// to make sure the pcm buffers are properly free'd from memory we need to nil them after the player has used them
|
||||
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataRendered, completionHandler: { (_) in
|
||||
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataConsumed, completionHandler: { (_) in
|
||||
nextScheduledBuffer = nil
|
||||
self?.numberOfBuffersScheduledInTotal -= 1
|
||||
self?.pollForNextBufferRecursive()
|
||||
@@ -235,6 +273,15 @@ class AudioStreamEngine: AudioEngine {
|
||||
//MARK:- Overriden From Parent
|
||||
override func seek(toNeedle needle: Needle) {
|
||||
Log.info("didSeek to needle: \(needle)")
|
||||
|
||||
// if not playable (data not loaded etc), duration could be zero.
|
||||
guard isPlayable else {
|
||||
if predictedStreamDuration == 0 {
|
||||
seekNeedleCommandBeforeEngineWasReady = needle
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard needle < (ceil(predictedStreamDuration)) else {
|
||||
if !isPlayable {
|
||||
seekNeedleCommandBeforeEngineWasReady = needle
|
||||
@@ -286,8 +333,24 @@ class AudioStreamEngine: AudioEngine {
|
||||
super.pause()
|
||||
}
|
||||
|
||||
override func play() {
|
||||
queue.async { [weak self] in
|
||||
self?.playHelperDispatchQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private func playHelperDispatchQueue() {
|
||||
super.play()
|
||||
}
|
||||
|
||||
override func invalidate() {
|
||||
queue.sync { [weak self] in
|
||||
self?.invalidateHelperDispatchQueue()
|
||||
self?.converter.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private func invalidateHelperDispatchQueue() {
|
||||
super.invalidate()
|
||||
converter.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,74 +27,30 @@ 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 pullNextDataPacket(_ callback: @escaping (Data?) -> ())
|
||||
func tellSeek(offset: UInt64)
|
||||
func tellBytesPerAudioPacket(count: 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 byteCount: UInt {
|
||||
return UInt(data.count)
|
||||
}
|
||||
|
||||
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")"
|
||||
}
|
||||
}
|
||||
private let queue = DispatchQueue(label: "SwiftAudioPlayer.Throttler", qos: .userInitiated)
|
||||
|
||||
//Init
|
||||
let url: AudioURL
|
||||
weak var delegate: AudioThrottleDelegate?
|
||||
|
||||
private var networkData: [NetworkDataWrapper] = []
|
||||
private var networkData: [Data] = [] {
|
||||
didSet {
|
||||
// Log.test("NETWORK DATA \(networkData.count)")
|
||||
}
|
||||
}
|
||||
private var lastSentDataPacketIndex = -1
|
||||
|
||||
var shouldThrottle = false
|
||||
var byteOffsetBecauseOfSeek: UInt = 0
|
||||
|
||||
@@ -116,131 +72,104 @@ class AudioThrottler: AudioThrottleable {
|
||||
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 let totalBytesExpected = pto.getTotalBytesExpected() {
|
||||
self.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 tellBytesPerAudioPacket(count: UInt64) {
|
||||
if count > largestPollingOffsetDifference {
|
||||
largestPollingOffsetDifference = count
|
||||
}
|
||||
}
|
||||
|
||||
func tellByteOffset(offset: UInt64) {
|
||||
Log.debug("offset \(offset)")
|
||||
|
||||
for wrappedNetworkData in networkData {
|
||||
if wrappedNetworkData.containsOffset(UInt(offset)) {
|
||||
Log.debug("offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset) is next sent: \(wrappedNetworkData.isNextSent())")
|
||||
|
||||
if wrappedNetworkData.alreadySent {
|
||||
Log.debug("already sent offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
|
||||
|
||||
var bytesSent: UInt = 0
|
||||
var current = wrappedNetworkData
|
||||
|
||||
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
|
||||
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
|
||||
while bytesSent < largestPollingOffsetDifference {
|
||||
if let next = current.next {
|
||||
if !next.alreadySent {
|
||||
Log.info("Sending next network packet with range: \(next.startOffset) to \(next.endOffset), have sent \(bytesSent) bytes so far from \(largestPollingOffsetDifference) bytes")
|
||||
next.alreadySent = true
|
||||
delegate?.shouldProcess(networkData: next.data)
|
||||
}
|
||||
bytesSent += next.byteCount
|
||||
current = next
|
||||
} else {
|
||||
Log.debug("next package doesn't exist, bytes sent so far: \(bytesSent)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
|
||||
wrappedNetworkData.alreadySent = true
|
||||
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
|
||||
return
|
||||
self.queue.async { [weak self] in
|
||||
self?.networkData.append(pto.getData())
|
||||
StreamingDownloadDirector.shared.didUpdate(url.key, networkStreamProgress: pto.getProgress())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func tellSeek(offset: UInt64) {
|
||||
Log.info("seek with offset: \(offset)")
|
||||
|
||||
self.queue.async { [weak self] in
|
||||
self?.seekQueueHelper(offset)
|
||||
}
|
||||
}
|
||||
|
||||
func seekQueueHelper(_ offset: UInt64) {
|
||||
let offsetToFind = Int(offset) - Int(byteOffsetBecauseOfSeek)
|
||||
|
||||
var shouldStartNewStream: Bool = false
|
||||
|
||||
// if we have no data start a new stream after seek
|
||||
if networkData.count == 0 {
|
||||
shouldStartNewStream = true
|
||||
}
|
||||
|
||||
// if what we're looking for is outside of available data, start a new stream
|
||||
if offset < byteOffsetBecauseOfSeek || offsetToFind > networkData.sum {
|
||||
shouldStartNewStream = true
|
||||
}
|
||||
|
||||
// we should have the data within our cache. find it and save the index for the next pull
|
||||
if let indexOfDataContainingOffset = networkData.getIndexContainingByteOffset(offsetToFind) {
|
||||
lastSentDataPacketIndex = indexOfDataContainingOffset - 1
|
||||
}
|
||||
|
||||
if shouldStartNewStream {
|
||||
byteOffsetBecauseOfSeek = UInt(offset)
|
||||
lastSentDataPacketIndex = -1
|
||||
AudioDataManager.shared.seekStream(withRemoteURL: url, toByteOffset: offset)
|
||||
|
||||
networkData = []
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Log.error("83672 Should not get here")
|
||||
}
|
||||
|
||||
func pollRangeOfBytesAvailable() -> (UInt64, UInt64) {
|
||||
let start = networkData.first?.startOffset ?? 0
|
||||
let end = networkData.last?.endOffset ?? 0
|
||||
let start = byteOffsetBecauseOfSeek
|
||||
let end = networkData.sum + Int(byteOffsetBecauseOfSeek)
|
||||
|
||||
return (UInt64(start), UInt64(end))
|
||||
}
|
||||
|
||||
func pullNextDataPacket(_ callback: @escaping (Data?) -> ()) {
|
||||
queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard self.lastSentDataPacketIndex < self.networkData.count - 1 else {
|
||||
callback(nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.lastSentDataPacketIndex += 1
|
||||
|
||||
callback(self.networkData[self.lastSentDataPacketIndex])
|
||||
}
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
AudioDataManager.shared.deleteStream(withRemoteURL: url)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Data {
|
||||
var sum: Int {
|
||||
get {
|
||||
guard count > 0 else { return 0 }
|
||||
return self.reduce(0) { $0 + $1.count }
|
||||
}
|
||||
}
|
||||
|
||||
func getIndexContainingByteOffset(_ offset: Int) -> Int? {
|
||||
var dataCount = 0
|
||||
|
||||
for (i, data) in self.enumerated() {
|
||||
if offset >= dataCount && offset <= dataCount + data.count {
|
||||
return i
|
||||
}
|
||||
|
||||
dataCount += data.count
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class AudioConverter: AudioConvertable {
|
||||
self.pcmBufferSize = size
|
||||
|
||||
do {
|
||||
parser = try AudioParser(withRemoteUrl: url, parsedFileAudioFormatCallback: {
|
||||
parser = try AudioParser(withRemoteUrl: url, bufferSize: Int(size), parsedFileAudioFormatCallback: {
|
||||
[weak self] (fileAudioFormat: AVAudioFormat) in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@ import AVFoundation
|
||||
//TODO: what if user seeks beyond the data we have? What if we're done but user seeks even further than what we have
|
||||
|
||||
class AudioParser: AudioParsable {
|
||||
private var MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING = 8192 // this will be modified when we know the file format to be just enough packets to fill up 1 pcm buffer
|
||||
private var framesPerBuffer: Int = 1
|
||||
|
||||
//MARK:- For OS parser class
|
||||
var parsedAudioHeaderPacketCount: UInt64 = 0
|
||||
var parsedAudioPacketDataSize: UInt64 = 0
|
||||
@@ -61,8 +64,8 @@ class AudioParser: AudioParsable {
|
||||
public var fileAudioFormat: AVAudioFormat? {
|
||||
didSet {
|
||||
if let format = fileAudioFormat, oldValue == nil {
|
||||
MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING = framesPerBuffer/Int(format.streamDescription.pointee.mFramesPerPacket)
|
||||
parsedFileAudioFormatCallback(format)
|
||||
throttler.tellAudioFormatFound()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,16 +103,9 @@ class AudioParser: AudioParsable {
|
||||
return predictedCount
|
||||
}
|
||||
|
||||
var sumOfParsedAudioBytes:UInt32 = 0 {
|
||||
didSet {
|
||||
if let byteCount = averageBytesPerPacket {
|
||||
throttler.tellBytesPerAudioPacket(count: UInt64(byteCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sumOfParsedAudioBytes:UInt32 = 0
|
||||
var numberOfPacketsParsed:UInt32 = 0
|
||||
var audioPackets: [(AudioStreamPacketDescription?,Data)] = [] {
|
||||
var audioPackets: [(AudioStreamPacketDescription?,Data)] = [] {
|
||||
didSet {
|
||||
if let audioPacketByteSize = audioPackets.last?.0?.mDataByteSize {
|
||||
sumOfParsedAudioBytes += audioPacketByteSize
|
||||
@@ -122,6 +118,8 @@ class AudioParser: AudioParsable {
|
||||
//TODO: duration will not be accurate with WAV or AIFF
|
||||
}
|
||||
}
|
||||
private let lockQueue = DispatchQueue(label: "SwiftAudioPlayer.Parser.packets.lock")
|
||||
var lastSentAudioPacketIndex = -1
|
||||
|
||||
/**
|
||||
Audio packets varry in size. The first one parsed in a batch of audio
|
||||
@@ -148,12 +146,29 @@ class AudioParser: AudioParsable {
|
||||
return audioPackets.count == totalPredictedPacketCount
|
||||
}
|
||||
|
||||
var streamChangeListenerId: UInt?
|
||||
|
||||
init(withRemoteUrl url: AudioURL, parsedFileAudioFormatCallback: @escaping(AVAudioFormat) -> ()) throws {
|
||||
init(withRemoteUrl url: AudioURL, bufferSize: Int, parsedFileAudioFormatCallback: @escaping(AVAudioFormat) -> ()) throws {
|
||||
self.url = url
|
||||
self.framesPerBuffer = bufferSize
|
||||
self.parsedFileAudioFormatCallback = parsedFileAudioFormatCallback
|
||||
|
||||
self.throttler = AudioThrottler(withRemoteUrl: url, withDelegate: self)
|
||||
|
||||
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (progress) in
|
||||
guard let self = self else { return }
|
||||
self.networkProgress = progress
|
||||
|
||||
// initially parse a bunch of packets
|
||||
self.lockQueue.sync {
|
||||
if self.fileAudioFormat == nil {
|
||||
self.processNextDataPacket()
|
||||
} else if self.audioPackets.count - self.lastSentAudioPacketIndex < self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
|
||||
//Open the stream and when we call parse data is fed into this stream
|
||||
guard AudioFileStreamOpen(context, ParserPropertyListener, ParserPacketListener, kAudioFileMP3Type, &streamID) == noErr else {
|
||||
@@ -161,33 +176,61 @@ class AudioParser: AudioParsable {
|
||||
}
|
||||
}
|
||||
|
||||
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data) {
|
||||
if let offset = getOffset(fromPacketIndex: index) {
|
||||
throttler.tellByteOffset(offset: offset)
|
||||
deinit {
|
||||
if let id = streamChangeListenerId {
|
||||
StreamingDownloadDirector.shared.detach(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data) {
|
||||
determineIfMoreDataNeedsToBeParsed(index: index)
|
||||
|
||||
// Check if we've reached the end of the packets. We have two scenarios:
|
||||
// 1. We've reached the end of the packet data and the file has been completely parsed
|
||||
// 2. We've reached the end of the data we currently have downloaded, but not the file
|
||||
let packetIndex = index - indexSeekOffset
|
||||
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), we predict \(totalPredictedPacketCount) in total")
|
||||
throw ParserError.notEnoughDataForReader
|
||||
|
||||
var exception: ParserError? = nil
|
||||
var packet: (AudioStreamPacketDescription?, Data) = (nil, Data())
|
||||
lockQueue.sync {
|
||||
if packetIndex >= self.audioPackets.count {
|
||||
if isParsingComplete {
|
||||
exception = ParserError.readerAskingBeyondEndOfFile
|
||||
return
|
||||
} else {
|
||||
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(self.audioPackets.count), we predict \(self.totalPredictedPacketCount) in total")
|
||||
exception = ParserError.notEnoughDataForReader
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lastSentAudioPacketIndex = Int(packetIndex)
|
||||
packet = audioPackets[Int(packetIndex)]
|
||||
}
|
||||
if let exception = exception {
|
||||
throw exception
|
||||
} else {
|
||||
return packet
|
||||
}
|
||||
}
|
||||
|
||||
private func determineIfMoreDataNeedsToBeParsed(index: AVAudioPacketCount) {
|
||||
lockQueue.sync {
|
||||
if index > self.audioPackets.count - self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
var isIndexValid: Bool = true
|
||||
lockQueue.sync {
|
||||
if self.indexSeekOffset <= index && index < self.audioPackets.count + Int(self.indexSeekOffset) {
|
||||
isIndexValid = false
|
||||
}
|
||||
}
|
||||
guard isIndexValid else { return }
|
||||
|
||||
guard let byteOffset = getOffset(fromPacketIndex: index) else {
|
||||
return
|
||||
@@ -199,9 +242,12 @@ class AudioParser: AudioParsable {
|
||||
// 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 = []
|
||||
lockQueue.sync {
|
||||
self.audioPackets = []
|
||||
}
|
||||
|
||||
throttler.tellSeek(offset: byteOffset)
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
|
||||
private func getOffset(fromPacketIndex index: AVAudioPacketCount) -> UInt64? {
|
||||
@@ -254,6 +300,12 @@ class AudioParser: AudioParsable {
|
||||
return Needle(TimeInterval(frame)/TimeInterval(frameCount)*duration)
|
||||
}
|
||||
|
||||
func append(description: AudioStreamPacketDescription?, data: Data) {
|
||||
lockQueue.sync {
|
||||
self.audioPackets.append((description, data))
|
||||
}
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
throttler.invalidate()
|
||||
|
||||
@@ -281,6 +333,32 @@ class AudioParser: AudioParsable {
|
||||
|
||||
}
|
||||
|
||||
private func processNextDataPacket() {
|
||||
throttler.pullNextDataPacket { [weak self] (d) in
|
||||
guard let self = self else { return }
|
||||
guard let data = d else { return }
|
||||
|
||||
self.lockQueue.sync {
|
||||
Log.debug("processing data count: \(data.count) :: already had \(self.audioPackets.count) audio packets")
|
||||
}
|
||||
self.shouldPreventPacketFromFillingUp = false
|
||||
do {
|
||||
let sID = self.streamID!
|
||||
let dataSize = data.count
|
||||
|
||||
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
|
||||
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
|
||||
guard result == noErr else {
|
||||
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
|
||||
throw ParserError.failedToParseBytes(result)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
Log.monitor(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK:- AudioThrottleDelegate
|
||||
@@ -288,27 +366,4 @@ 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
|
||||
|
||||
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
|
||||
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
|
||||
guard result == noErr else {
|
||||
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
|
||||
throw ParserError.failedToParseBytes(result)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
Log.monitor(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func parserPacket(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ pac
|
||||
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))
|
||||
selfAudioParser.append(description: audioPacketDescription, data: audioPacketData)
|
||||
}
|
||||
} else { // not compressed audio (.wav)
|
||||
Log.debug("uncompressed audio")
|
||||
@@ -75,7 +75,7 @@ func parserPacket(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ pac
|
||||
let audioPacketStart = i * bytesPerAudioPacket
|
||||
let audioPacketSize = bytesPerAudioPacket
|
||||
let audioPacketData = Data(bytes: streamData.advanced(by: audioPacketStart), count: audioPacketSize)
|
||||
selfAudioParser.audioPackets.append((nil, audioPacketData))
|
||||
selfAudioParser.append(description: nil, data: audioPacketData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ public struct SAAudioAvailabilityRange {
|
||||
var needleAtEnd = false
|
||||
|
||||
if(totalDurationBuffered > 0 && needle > 0) {
|
||||
needleAtEnd = needle >= totalDurationBuffered - 1
|
||||
needleAtEnd = needle >= totalDurationBuffered - 5
|
||||
}
|
||||
|
||||
// if most of the audio is buffered for long audio or in short audio there isn't many seconds left to buffer it means wwe've reached the end of the audio
|
||||
|
||||
@@ -39,7 +39,7 @@ extension LockScreenViewProtocol {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@available(iOS 10.0, tvOS 10.0, *)
|
||||
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo?, duration: Duration) {
|
||||
var nowPlayingInfo:[String : Any] = [:]
|
||||
|
||||
@@ -50,6 +50,7 @@ extension LockScreenViewProtocol {
|
||||
|
||||
let title = info.title
|
||||
let artist = info.artist
|
||||
let albumTitle = info.albumTitle ?? artist
|
||||
let releaseDate = info.releaseDate
|
||||
|
||||
// For some reason we need to set a duration here for the needle?
|
||||
@@ -57,7 +58,7 @@ extension LockScreenViewProtocol {
|
||||
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = title
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = artist
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = albumTitle
|
||||
//nowPlayingInfo[MPMediaItemPropertyGenre] = //maybe later when we have it
|
||||
//nowPlayingInfo[MPMediaItemPropertyIsExplicit] = //maybe later when we have it
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = artist
|
||||
@@ -168,7 +169,10 @@ extension LockScreenViewProtocol {
|
||||
}
|
||||
|
||||
func updateLockscreenSkipIntervals() {
|
||||
MPRemoteCommandCenter.shared().skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
|
||||
MPRemoteCommandCenter.shared().skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
commandCenter.skipBackwardCommand.isEnabled = skipBackwardSeconds > 0
|
||||
commandCenter.skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
|
||||
commandCenter.skipForwardCommand.isEnabled = skipForwardSeconds > 0
|
||||
commandCenter.skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,12 @@ protocol AudioDataManagable {
|
||||
var numberOfActive: Int { get }
|
||||
|
||||
var allowCellular: Bool { get set }
|
||||
var downloadDirectory: FileManager.SearchPathDirectory { get }
|
||||
|
||||
func setHTTPHeaderFields(_ fields: [String: String]?)
|
||||
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ())
|
||||
func setAllowCellularDownloadPreference(_ preference: Bool)
|
||||
func setDownloadDirectory(_ dir: FileManager.SearchPathDirectory)
|
||||
|
||||
func clear()
|
||||
|
||||
@@ -53,6 +56,7 @@ protocol AudioDataManagable {
|
||||
|
||||
class AudioDataManager: AudioDataManagable {
|
||||
var allowCellular: Bool = true
|
||||
var downloadDirectory: FileManager.SearchPathDirectory = .documentDirectory
|
||||
|
||||
static let shared: AudioDataManagable = AudioDataManager()
|
||||
|
||||
@@ -96,6 +100,11 @@ class AudioDataManager: AudioDataManagable {
|
||||
streamingCallbacks = []
|
||||
}
|
||||
|
||||
func setHTTPHeaderFields(_ fields: [String: String]?) {
|
||||
streamWorker.HTTPHeaderFields = fields
|
||||
downloadWorker.HTTPHeaderFields = fields
|
||||
}
|
||||
|
||||
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
|
||||
backgroundCompletion = completionHandler
|
||||
}
|
||||
@@ -104,6 +113,10 @@ class AudioDataManager: AudioDataManagable {
|
||||
allowCellular = preference
|
||||
}
|
||||
|
||||
func setDownloadDirectory(_ dir: FileManager.SearchPathDirectory) {
|
||||
downloadDirectory = dir
|
||||
}
|
||||
|
||||
func attach(callback: @escaping (_ id: ID, _ progress: Double)->()) {
|
||||
globalDownloadProgressCallback = callback
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ protocol AudioDataDownloadable: AnyObject {
|
||||
var numberOfActive: Int { get }
|
||||
var numberOfQueued: Int { get }
|
||||
|
||||
var HTTPHeaderFields: [String: String]? { get set }
|
||||
|
||||
func getProgressOfDownload(withID id: ID) -> Double?
|
||||
|
||||
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ())
|
||||
@@ -57,6 +59,8 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
|
||||
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
||||
}()
|
||||
|
||||
var HTTPHeaderFields: [String: String]?
|
||||
|
||||
private var activeDownloads: [ActiveDownload] = []
|
||||
private var queuedDownloads = Set<DownloadInfo>()
|
||||
|
||||
@@ -111,7 +115,10 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
|
||||
|
||||
queuedDownloads.remove(info)
|
||||
|
||||
let task: URLSessionDownloadTask = session.downloadTask(with: info.remoteUrl)
|
||||
var request = URLRequest(url: info.remoteUrl)
|
||||
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
|
||||
let task: URLSessionDownloadTask = session.downloadTask(with: request)
|
||||
task.taskDescription = info.id
|
||||
|
||||
let activeTask = ActiveDownload(info: info, task: task)
|
||||
|
||||
@@ -64,7 +64,7 @@ struct FileStorage {
|
||||
// MARK:- Audio
|
||||
extension FileStorage {
|
||||
struct Audio {
|
||||
private static let directory: FileManager.SearchPathDirectory = .documentDirectory
|
||||
private static let directory: FileManager.SearchPathDirectory = AudioDataManager.shared.downloadDirectory
|
||||
private init() {}
|
||||
|
||||
static func isStored(_ id: ID) -> Bool {
|
||||
@@ -103,7 +103,7 @@ extension FileStorage {
|
||||
}
|
||||
|
||||
static func locate(_ id: ID) -> URL? {
|
||||
let folderUrls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
||||
let folderUrls = FileManager.default.urls(for: directory, in: .userDomainMask)
|
||||
guard folderUrls.count != 0 else { return nil }
|
||||
|
||||
if let urls = try? FileManager.default.contentsOfDirectory(at: folderUrls[0], includingPropertiesForKeys: nil) {
|
||||
|
||||
@@ -44,6 +44,9 @@ import Foundation
|
||||
protocol AudioDataStreamable {
|
||||
//if user taps download then starts to stream
|
||||
init(progressCallback: @escaping (_ id: ID, _ dto: StreamProgressDTO) -> (), doneCallback: @escaping (_ id: ID, _ error: Error?)->Bool) //Bool is should save or not
|
||||
|
||||
var HTTPHeaderFields: [String: String]? { get set }
|
||||
|
||||
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data?, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64?)
|
||||
func pause(withId id: ID)
|
||||
func resume(withId id: ID)
|
||||
@@ -66,6 +69,8 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
|
||||
fileprivate let doneCallback: (_ id: ID, _ error: Error?) -> Bool
|
||||
private var session: URLSession!
|
||||
|
||||
var HTTPHeaderFields: [String: String]?
|
||||
|
||||
private var id: ID?
|
||||
private var url: URL?
|
||||
private var task: URLSessionDataTask?
|
||||
@@ -89,7 +94,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
|
||||
|
||||
let config = URLSessionConfiguration.background(withIdentifier: "SwiftAudioPlayer.stream")
|
||||
// Specifies that the phone should keep trying till it receives connection instead of dropping immediately
|
||||
if #available(iOS 11.0, *) {
|
||||
if #available(iOS 11.0, tvOS 11.0, *) {
|
||||
config.waitsForConnectivity = true
|
||||
}
|
||||
self.session = URLSession(configuration: config, delegate: self, delegateQueue: nil) //TODO: should we use ephemeral
|
||||
@@ -105,6 +110,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
|
||||
|
||||
if let data = data {
|
||||
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: TIMEOUT)
|
||||
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
request.addValue("bytes=\(data.count)-", forHTTPHeaderField: "Range")
|
||||
task = session.dataTask(with: request)
|
||||
task?.taskDescription = id
|
||||
@@ -121,10 +127,11 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
|
||||
|
||||
task?.resume()
|
||||
} else {
|
||||
task = session.dataTask(with: url)
|
||||
task?.resume()
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
task = session.dataTask(with: request)
|
||||
task?.taskDescription = id
|
||||
task?.resume()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +224,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
|
||||
self.progressCallback(id, StreamProgressDTO(progress: 0, data: Data(), totalBytesExpected: totalBytesExpectedForWholeFile))
|
||||
|
||||
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: TIMEOUT)
|
||||
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||
request.addValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
task = session.dataTask(with: request)
|
||||
task?.resume()
|
||||
@@ -314,6 +322,7 @@ extension AudioStreamWorker: URLSessionDataDelegate {
|
||||
Log.monitor("\(task.currentRequest?.url?.absoluteString ?? "nil url") error: \(err.localizedDescription)")
|
||||
|
||||
let _ = doneCallback(id, err)
|
||||
return
|
||||
}
|
||||
|
||||
let shouldSave = doneCallback(id, nil)
|
||||
|
||||
+110
-58
@@ -45,6 +45,15 @@ public class SAPlayer {
|
||||
private var presenter: SAPlayerPresenter!
|
||||
private var player: AudioEngine?
|
||||
|
||||
/**
|
||||
Any necessary header fields for streaming and downloading requests can be set here.
|
||||
*/
|
||||
public var HTTPHeaderFields: [String: String]? {
|
||||
didSet {
|
||||
AudioDataManager.shared.setHTTPHeaderFields(HTTPHeaderFields)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Access the engine of the player. Engine is nil if player has not been initialized with audio.
|
||||
|
||||
@@ -81,14 +90,14 @@ public class SAPlayer {
|
||||
*/
|
||||
public var volume: Float? {
|
||||
get {
|
||||
return player?.engine.mainMixerNode.volume
|
||||
return player?.playerNode.volume
|
||||
}
|
||||
|
||||
set {
|
||||
guard let value = newValue else { return }
|
||||
guard value >= 0.0 && value <= 1.0 else { return }
|
||||
|
||||
player?.engine.mainMixerNode.volume = value
|
||||
player?.playerNode.volume = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,13 +132,6 @@ public class SAPlayer {
|
||||
|
||||
node.rate = value
|
||||
playbackRateOfAudioChanged(rate: value)
|
||||
|
||||
// if skip silences was on, reset it to have the new rate
|
||||
// TODO fix this to rate being broadcasted and handled in only Features.SkipSilences https://github.com/tanhakabir/SwiftAudioPlayer/issues/77
|
||||
// if Features.SkipSilences.enabled && !(value == rate ?? 1.0 - 0.5 || value == rate ?? 1.0 + 0.5) {
|
||||
// _ = Features.SkipSilences.disable()
|
||||
// _ = Features.SkipSilences.enable()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,15 +180,9 @@ public class SAPlayer {
|
||||
public var audioModifiers: [AVAudioUnit] = []
|
||||
|
||||
/**
|
||||
List of audio URLs queued for playback.
|
||||
List of queued audio for playback. You can edit this list as you wish to modify the queue.
|
||||
*/
|
||||
public var audioQueued: [URL] {
|
||||
get {
|
||||
return presenter.audioQueue.map { (queued) -> URL in
|
||||
return queued.1
|
||||
}
|
||||
}
|
||||
}
|
||||
public var audioQueued: [SAAudioQueueItem] = []
|
||||
|
||||
/**
|
||||
Total duration of current audio initialized. Returns nil if no audio is initialized in player.
|
||||
@@ -233,11 +229,7 @@ public class SAPlayer {
|
||||
|
||||
- Note: Setting this to nil clears the information displayed on the lockscreen media player.
|
||||
*/
|
||||
public var mediaInfo: SALockScreenInfo? = nil {
|
||||
didSet {
|
||||
presenter.handleLockscreenInfo(info: mediaInfo)
|
||||
}
|
||||
}
|
||||
public var mediaInfo: SALockScreenInfo? = nil
|
||||
|
||||
private init() {
|
||||
presenter = SAPlayerPresenter(delegate: self)
|
||||
@@ -255,6 +247,7 @@ public class SAPlayer {
|
||||
}
|
||||
|
||||
audioModifiers.append(AVAudioUnitTimePitch(audioComponentDescription: componentDescription))
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption), name: AVAudioSession.interruptionNotification, object: nil)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,9 +274,8 @@ public class SAPlayer {
|
||||
*/
|
||||
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)
|
||||
let minutes = Int((timestamp - Double(hours * 60 * 60)) / 60)
|
||||
let secondsLeft = Int(timestamp - Double(hours * 60 * 60) - Double(minutes * 60))
|
||||
|
||||
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", secondsLeft))"
|
||||
}
|
||||
@@ -295,6 +287,44 @@ public class SAPlayer {
|
||||
func addUrlToMapping(url: URL) {
|
||||
presenter.addUrlToKeyMap(url)
|
||||
}
|
||||
|
||||
@objc func handleInterruption(notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Switch over the interruption type.
|
||||
switch type {
|
||||
|
||||
case .began:
|
||||
// An interruption began. Update the UI as necessary.
|
||||
pause()
|
||||
|
||||
case .ended:
|
||||
// An interruption ended. Resume playback, if appropriate.
|
||||
|
||||
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
||||
if options.contains(.shouldResume) {
|
||||
// An interruption ended. Resume playback.
|
||||
play()
|
||||
} else {
|
||||
// An interruption ended. Don't resume playback.
|
||||
}
|
||||
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SAPlayerBitrate {
|
||||
/// This bitrate is good for radio streams that are passing ittle amounts of audio data at a time. This will allow the player to process the audio data in a fast enough rate to not pause or get stuck playing. This rate however ends up using more CPU and is worse for your battery-life and performance of your app.
|
||||
case low
|
||||
|
||||
/// This bitrate is good for streaming saved audio files like podcasts where most of the audio data will be received from the remote server at the beginning in a short time. This rate is more performant by using much less CPU and being better for your battery-life and app performance.
|
||||
case high // go for audio files being streamed. This is uses less CPU and
|
||||
}
|
||||
|
||||
//MARK: - External Player Controls
|
||||
@@ -407,12 +437,13 @@ extension SAPlayer {
|
||||
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
|
||||
*/
|
||||
public func startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlaySavedAudio(withSavedUrl: url)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "startSavedAudio")
|
||||
public func initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
|
||||
// Because we support queueing, we want to clear off any existing players.
|
||||
// Therefore, instantiate new player every time, destroy any existing ones.
|
||||
// This prevents a crash where an owning engine already exists.
|
||||
presenter.handleClear()
|
||||
|
||||
// order here matters, need to set media info before trying to play audio
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlaySavedAudio(withSavedUrl: url)
|
||||
}
|
||||
@@ -442,17 +473,19 @@ extension SAPlayer {
|
||||
- Note: Subscribe to `SAPlayer.Updates.StreamingBuffer` to see updates in streaming progress.
|
||||
|
||||
- Parameter withRemoteUrl: The URL of the remote audio.
|
||||
- Parameter bitrate: The bitrate of the streamed audio. By default the bitrate is set to high for streaming saved audio files. If you want to stream radios then you should use the `low` bitrate option.
|
||||
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
|
||||
*/
|
||||
public func startRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
public func startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate = .high, mediaInfo: SALockScreenInfo? = nil) {
|
||||
|
||||
// Because we support queueing, we want to clear off any existing players.
|
||||
// Therefore, instantiate new player every time, destroy any existing ones.
|
||||
// This prevents a crash where an owning engine already exists.
|
||||
presenter.handleClear()
|
||||
|
||||
// order here matters, need to set media info before trying to play audio
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "startRemoteAudio")
|
||||
public func initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: bitrate)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,18 +499,40 @@ extension SAPlayer {
|
||||
Queues remote audio to be played next. The URLs in the queue can be both remote or on disk but once the queued audio starts playing it will start buffering and loading then. This means no guarantee for a 'gapless' playback where there might be several moments in between one audio ending and another starting due to buffering remote audio.
|
||||
|
||||
- Parameter withRemoteUrl: The URL of the remote audio.
|
||||
- Parameter bitrate: The bitrate of the streamed audio. By default the bitrate is set to high for streaming saved audio files. If you want to stream radios then you should use the `low` bitrate option.
|
||||
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
|
||||
*/
|
||||
public func queueRemoteAudio(withRemoteUrl url: URL) {
|
||||
presenter.handleQueueStreamedAudio(withRemoteUrl: url)
|
||||
public func queueRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate = .high, mediaInfo: SALockScreenInfo? = nil) {
|
||||
presenter.handleQueueStreamedAudio(withRemoteUrl: url, mediaInfo: mediaInfo, bitrate: bitrate)
|
||||
}
|
||||
|
||||
/**
|
||||
Queues saved audio to be played next. The URLs in the queuecan be both remote or on disk but once the queued audio starts playing it will start buffering and loading then. This means no guarantee for a 'gapless' playback where there might be several moments in between one audio ending and another starting due to buffering remote audio.
|
||||
Queues saved audio to be played next. The URLs in the queue can be both remote or on disk but once the queued audio starts playing it will start buffering and loading then. This means no guarantee for a 'gapless' playback where there might be several moments in between one audio ending and another starting due to buffering remote audio.
|
||||
|
||||
- Parameter withSavedUrl: The URL of the audio saved on the device.
|
||||
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
|
||||
*/
|
||||
public func queueSavedAudio(withSavedUrl url: URL) {
|
||||
presenter.handleQueueSavedAudio(withSavedUrl: url)
|
||||
public func queueSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
presenter.handleQueueSavedAudio(withSavedUrl: url, mediaInfo: mediaInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove the first queued audio if one exists. Receive the first URL removed back.
|
||||
|
||||
- Returns the URL of the removed audio.
|
||||
*/
|
||||
public func removeFirstQueuedAudio() -> URL? {
|
||||
guard audioQueued.count != 0 else { return nil }
|
||||
return presenter.handleRemoveFirstQueuedItem()
|
||||
}
|
||||
|
||||
/**
|
||||
Clear the list of queued audio.
|
||||
|
||||
- Returns the list of removed audio URLs
|
||||
*/
|
||||
public func clearAllQueuedAudio() -> [URL] {
|
||||
return presenter.handleClearQueued()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -491,22 +546,22 @@ extension SAPlayer {
|
||||
|
||||
//MARK: - Internal implementation of delegate
|
||||
extension SAPlayer: SAPlayerDelegate {
|
||||
func startAudioDownloaded(withSavedUrl url: AudioURL) {
|
||||
internal func startAudioDownloaded(withSavedUrl url: AudioURL) {
|
||||
player = AudioDiskEngine(withSavedUrl: url, delegate: presenter)
|
||||
}
|
||||
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL) {
|
||||
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter)
|
||||
internal func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate) {
|
||||
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter, bitrate: bitrate)
|
||||
}
|
||||
|
||||
func clearEngine() {
|
||||
internal func clearEngine() {
|
||||
player?.pause()
|
||||
player?.invalidate()
|
||||
player = nil
|
||||
Log.info("cleared engine")
|
||||
}
|
||||
|
||||
func playEngine() {
|
||||
internal func playEngine() {
|
||||
becomeDeviceAudioPlayer()
|
||||
player?.play()
|
||||
}
|
||||
@@ -514,26 +569,23 @@ extension SAPlayer: SAPlayerDelegate {
|
||||
//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: [])
|
||||
if #available(iOS 11.0, tvOS 11.0, *) {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, policy: .longFormAudio, options: [])
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
|
||||
}
|
||||
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
|
||||
|
||||
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
|
||||
} catch {
|
||||
Log.monitor("Problem setting up AVAudioSession to play in:: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func pauseEngine() {
|
||||
internal func pauseEngine() {
|
||||
player?.pause()
|
||||
}
|
||||
|
||||
func seekEngine(toNeedle needle: Needle) {
|
||||
var seekToNeedle = needle < 0 ? 0 : needle
|
||||
seekToNeedle = needle > Needle(duration ?? 0) ? Needle(duration ?? 0) : needle
|
||||
internal func seekEngine(toNeedle needle: Needle) {
|
||||
let seekToNeedle = needle < 0 ? 0 : needle
|
||||
player?.seek(toNeedle: seekToNeedle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,12 @@ import Foundation
|
||||
import CoreMedia
|
||||
|
||||
protocol SAPlayerDelegate: AnyObject, LockScreenViewProtocol {
|
||||
var mediaInfo: SALockScreenInfo? { get set }
|
||||
var skipForwardSeconds: Double { get set }
|
||||
var skipBackwardSeconds: Double { get set }
|
||||
|
||||
func startAudioDownloaded(withSavedUrl url: AudioURL)
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL)
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate)
|
||||
func clearEngine()
|
||||
func playEngine()
|
||||
func pauseEngine()
|
||||
|
||||
@@ -109,5 +109,14 @@ extension SAPlayer {
|
||||
AudioDataManager.shared.setAllowCellularDownloadPreference(allowUsingCellularData)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
EXPERIMENTAL!
|
||||
*/
|
||||
public static var downloadDirectory: FileManager.SearchPathDirectory = .documentDirectory {
|
||||
didSet {
|
||||
AudioDataManager.shared.setDownloadDirectory(downloadDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,18 +24,20 @@ extension SAPlayer {
|
||||
public struct SkipSilences {
|
||||
|
||||
static var enabled: Bool = false
|
||||
static var originalRate: Float = 1.0
|
||||
|
||||
/**
|
||||
Enable feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected. This can be called at any point of audio playback.
|
||||
|
||||
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
|
||||
- Precondition: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
|
||||
- Important: If you want to change the rate of the overall player while having skip silences on, please use `SAPlayer.Features.SkipSilences.setRateSafely()` to properly set the rate of the player. Any rate changes to the player will be ignored while using Skip Silences otherwise.
|
||||
*/
|
||||
public static func enable() -> Bool {
|
||||
guard let engine = SAPlayer.shared.engine else { return false }
|
||||
|
||||
Log.info("enabling skip silences feature")
|
||||
enabled = true
|
||||
let originalRate = SAPlayer.shared.rate ?? 1.0
|
||||
originalRate = SAPlayer.shared.rate ?? 1.0
|
||||
let format = engine.mainMixerNode.outputFormat(forBus: 0)
|
||||
|
||||
|
||||
@@ -58,10 +60,10 @@ extension SAPlayer {
|
||||
Log.debug("meterLevel: \(meterLevel)")
|
||||
if meterLevel < 0.6 { // below 0.6 decibels is below audible audio
|
||||
SAPlayer.shared.rate = originalRate + 0.5
|
||||
Log.test("speed up rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
Log.debug("speed up rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
} else {
|
||||
SAPlayer.shared.rate = originalRate
|
||||
Log.test("slow down rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
Log.debug("slow down rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,17 +73,27 @@ extension SAPlayer {
|
||||
/**
|
||||
Disable feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected. This can be called at any point of audio playback.
|
||||
|
||||
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
|
||||
- Precondition: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
|
||||
*/
|
||||
public static func disable() -> Bool {
|
||||
// TODO fix disabling on speed up portion and being stuck at faster speed https://github.com/tanhakabir/SwiftAudioPlayer/issues/76
|
||||
guard let engine = SAPlayer.shared.engine else { return false }
|
||||
Log.info("disabling skip silences feature")
|
||||
engine.mainMixerNode.removeTap(onBus: 0)
|
||||
SAPlayer.shared.rate = originalRate
|
||||
enabled = false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
Use this function to set the overall rate of the player for when skip silences is on. This ensures that the overall rate will be what is set through this function even as skip silences is on; if this function is not used then any changes asked of from the overall player while skip silences is on won't be recorded!
|
||||
|
||||
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
|
||||
*/
|
||||
public static func setRateSafely(_ rate: Float) {
|
||||
originalRate = rate
|
||||
SAPlayer.shared.rate = rate
|
||||
}
|
||||
|
||||
private static func scaledPower(power: Float) -> Float {
|
||||
guard power.isFinite else { return 0.0 }
|
||||
let minDb: Float = -80.0
|
||||
@@ -119,5 +131,37 @@ extension SAPlayer {
|
||||
timer?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Feature to play the current playing audio on repeat until feature is disabled.
|
||||
*/
|
||||
public struct Loop {
|
||||
static var enabled: Bool = false
|
||||
static var playingStatusId: UInt?
|
||||
|
||||
/**
|
||||
Enable feature to play the current playing audio on loop. This will continue until the feature is disabled. And this feature works for both remote and saved audio.
|
||||
*/
|
||||
public static func enable() {
|
||||
enabled = true
|
||||
|
||||
guard playingStatusId == nil else { return }
|
||||
|
||||
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe({ (url, status) in
|
||||
if status == .ended && enabled {
|
||||
SAPlayer.shared.seekTo(seconds: 0.0)
|
||||
SAPlayer.shared.play()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Disable feature playing audio on loop.
|
||||
*/
|
||||
public static func disable() {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,49 @@ public typealias UTC = Int
|
||||
public struct SALockScreenInfo {
|
||||
var title: String
|
||||
var artist: String
|
||||
var albumTitle: String?
|
||||
var artwork: UIImage?
|
||||
var releaseDate: UTC
|
||||
|
||||
public init(title: String, artist: String, artwork: UIImage?, releaseDate: UTC) {
|
||||
public init(title: String, artist: String, albumTitle: String?, artwork: UIImage?, releaseDate: UTC) {
|
||||
self.title = title
|
||||
self.artist = artist
|
||||
self.albumTitle = albumTitle
|
||||
self.artwork = artwork
|
||||
self.releaseDate = releaseDate
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Use to add audio to be queued for playback.
|
||||
*/
|
||||
public struct SAAudioQueueItem {
|
||||
public var loc: Location
|
||||
public var url: URL
|
||||
public var mediaInfo: SALockScreenInfo?
|
||||
public var bitrate: SAPlayerBitrate
|
||||
|
||||
/**
|
||||
Use to add audio to be queued for playback.
|
||||
|
||||
- Parameter loc: If the URL for the file is remote or saved on device.
|
||||
- Parameter url: URL of audio to be queued
|
||||
- Parameter mediaInfo: Relevant lockscreen media info for the queued audio.
|
||||
- Parameter bitrate: For streamed remote audio specifiy a bitrate if different from high. Use low bitrate for radio streams.
|
||||
*/
|
||||
public init(loc: Location, url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate = .high) {
|
||||
self.loc = loc
|
||||
self.url = url
|
||||
self.mediaInfo = mediaInfo
|
||||
self.bitrate = bitrate
|
||||
}
|
||||
|
||||
/**
|
||||
Where the queued audio is sourced. Remote to be streamed or locally saved on device.
|
||||
*/
|
||||
public enum Location {
|
||||
case remote
|
||||
case saved
|
||||
}
|
||||
}
|
||||
+84
-106
@@ -28,11 +28,6 @@ import AVFoundation
|
||||
import MediaPlayer
|
||||
|
||||
class SAPlayerPresenter {
|
||||
enum Location {
|
||||
case remote
|
||||
case disk
|
||||
}
|
||||
|
||||
weak var delegate: SAPlayerDelegate?
|
||||
var shouldPlayImmediately = false //for auto-play
|
||||
|
||||
@@ -41,19 +36,53 @@ class SAPlayerPresenter {
|
||||
|
||||
private var key: String?
|
||||
private var isPlaying: SAPlayingStatus = .buffering
|
||||
private var mediaInfo: SALockScreenInfo?
|
||||
|
||||
private var urlKeyMap: [Key: URL] = [:]
|
||||
|
||||
var durationRef:UInt = 0
|
||||
var needleRef:UInt = 0
|
||||
var playingStatusRef:UInt = 0
|
||||
var audioQueue: [(Location, URL)] = []
|
||||
var durationRef: UInt = 0
|
||||
var needleRef: UInt = 0
|
||||
var playingStatusRef: UInt = 0
|
||||
var audioQueue: [SAAudioQueueItem] = []
|
||||
|
||||
init(delegate: SAPlayerDelegate?) {
|
||||
self.delegate = delegate
|
||||
|
||||
delegate?.setLockScreenControls(presenter: self)
|
||||
|
||||
durationRef = AudioClockDirector.shared.attachToChangesInDuration(closure: { [weak self] (duration) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
|
||||
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
|
||||
self.duration = duration
|
||||
|
||||
self.delegate?.setLockScreenInfo(withMediaInfo: self.delegate?.mediaInfo, duration: duration)
|
||||
})
|
||||
|
||||
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (needle) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
|
||||
self.needle = needle
|
||||
self.delegate?.updateLockscreenElapsedTime(needle: needle)
|
||||
})
|
||||
|
||||
playingStatusRef = AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { [weak self] (isPlaying) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
|
||||
if(isPlaying == .paused && self.shouldPlayImmediately) {
|
||||
self.shouldPlayImmediately = false
|
||||
self.handlePlay()
|
||||
}
|
||||
|
||||
// solves bug nil == owningEngine || GetEngine() == owningEngine where too many
|
||||
// ended statuses were notified to cause 2 engines to be initialized and causes an error.
|
||||
// TODO don't need guard
|
||||
guard isPlaying != self.isPlaying else { return }
|
||||
self.isPlaying = isPlaying
|
||||
|
||||
if(self.isPlaying == .ended) {
|
||||
self.playNextAudioIfExists()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func getUrl(forKey key: Key) -> URL? {
|
||||
@@ -66,108 +95,61 @@ class SAPlayerPresenter {
|
||||
|
||||
func handleClear() {
|
||||
delegate?.clearEngine()
|
||||
AudioClockDirector.shared.resetCache()
|
||||
|
||||
needle = nil
|
||||
duration = nil
|
||||
key = nil
|
||||
mediaInfo = nil
|
||||
delegate?.mediaInfo = nil
|
||||
delegate?.clearLockScreenInfo()
|
||||
|
||||
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
|
||||
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
|
||||
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
|
||||
}
|
||||
|
||||
func handlePlaySavedAudio(withSavedUrl url: URL) {
|
||||
// Because we support queueing, we want to clear off any existing players.
|
||||
// Therefore, instantiate new player every time, destroy any existing ones.
|
||||
// This prevents a crash where an owning engine already exists.
|
||||
handleClear()
|
||||
attachForUpdates(url: url)
|
||||
resetCacheForNewAudio(url: url)
|
||||
delegate?.startAudioDownloaded(withSavedUrl: url)
|
||||
}
|
||||
|
||||
func handlePlayStreamedAudio(withRemoteUrl url: URL) {
|
||||
// Because we support queueing, we want to clear off any existing players.
|
||||
// Therefore, instantiate new player every time, destroy any existing ones.
|
||||
// This prevents a crash where an owning engine already exists.
|
||||
handleClear()
|
||||
attachForUpdates(url: url)
|
||||
delegate?.startAudioStreamed(withRemoteUrl: url)
|
||||
func handlePlayStreamedAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate) {
|
||||
resetCacheForNewAudio(url: url)
|
||||
delegate?.startAudioStreamed(withRemoteUrl: url, bitrate: bitrate)
|
||||
}
|
||||
|
||||
func handleQueueStreamedAudio(withRemoteUrl url: URL) {
|
||||
audioQueue.append((.remote, url))
|
||||
}
|
||||
|
||||
func handleQueueSavedAudio(withSavedUrl url: URL) {
|
||||
audioQueue.append((.disk, url))
|
||||
}
|
||||
|
||||
private func attachForUpdates(url: URL) {
|
||||
detachFromUpdates()
|
||||
|
||||
private func resetCacheForNewAudio(url: URL) {
|
||||
self.key = url.key
|
||||
urlKeyMap[url.key] = url
|
||||
|
||||
durationRef = AudioClockDirector.shared.attachToChangesInDuration(closure: { [weak self] (key, duration) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
guard key == self.key else {
|
||||
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
|
||||
self.duration = duration
|
||||
|
||||
self.delegate?.setLockScreenInfo(withMediaInfo: self.mediaInfo, duration: duration)
|
||||
})
|
||||
|
||||
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (key, needle) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
guard key == self.key else {
|
||||
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
self.needle = needle
|
||||
self.delegate?.updateLockscreenElapsedTime(needle: needle)
|
||||
})
|
||||
|
||||
playingStatusRef = AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { [weak self] (key, isPlaying) in
|
||||
guard let self = self else { throw DirectorError.closureIsDead }
|
||||
guard key == self.key else {
|
||||
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
self.isPlaying = isPlaying
|
||||
|
||||
if(self.isPlaying == .paused && self.shouldPlayImmediately) {
|
||||
self.shouldPlayImmediately = false
|
||||
self.handlePlay()
|
||||
}
|
||||
|
||||
if(self.isPlaying == .ended) {
|
||||
self.playNextAudioIfExists()
|
||||
}
|
||||
})
|
||||
AudioClockDirector.shared.setKey(url.key)
|
||||
AudioClockDirector.shared.resetCache()
|
||||
}
|
||||
|
||||
private func detachFromUpdates() {
|
||||
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
|
||||
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
|
||||
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
|
||||
func handleQueueStreamedAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate) {
|
||||
audioQueue.append(SAAudioQueueItem(loc: .remote, url: url, mediaInfo: mediaInfo, bitrate: bitrate))
|
||||
}
|
||||
|
||||
func handleQueueSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?) {
|
||||
audioQueue.append(SAAudioQueueItem(loc: .saved, url: url, mediaInfo: mediaInfo))
|
||||
}
|
||||
|
||||
func handleRemoveFirstQueuedItem() -> URL? {
|
||||
guard audioQueue.count != 0 else { return nil }
|
||||
|
||||
return audioQueue.remove(at: 0).url
|
||||
}
|
||||
|
||||
func handleClearQueued() -> [URL] {
|
||||
guard audioQueue.count != 0 else { return [] }
|
||||
|
||||
let urls = audioQueue.map { item in
|
||||
return item.url
|
||||
}
|
||||
|
||||
audioQueue = []
|
||||
return urls
|
||||
}
|
||||
|
||||
func handleStopStreamingAudio() {
|
||||
delegate?.clearEngine()
|
||||
detachFromUpdates()
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
func handleLockscreenInfo(info: SALockScreenInfo?) {
|
||||
self.mediaInfo = info
|
||||
AudioClockDirector.shared.resetCache()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,27 +220,23 @@ extension SAPlayerPresenter {
|
||||
return
|
||||
}
|
||||
let nextAudioURL = audioQueue.removeFirst()
|
||||
let key = nextAudioURL.1.key
|
||||
|
||||
|
||||
Log.info("getting ready to play \(nextAudioURL)")
|
||||
AudioQueueDirector.shared.changeInQueue(key, url: nextAudioURL.1)
|
||||
AudioQueueDirector.shared.changeInQueue(url: nextAudioURL.url)
|
||||
|
||||
handleClear()
|
||||
|
||||
// We need to give a second to clean up the previous engine properly. Deinit takes some time.
|
||||
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch nextAudioURL.0 {
|
||||
case .remote:
|
||||
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.1)
|
||||
break
|
||||
case .disk:
|
||||
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.1)
|
||||
}
|
||||
|
||||
self.shouldPlayImmediately = true
|
||||
delegate?.mediaInfo = nextAudioURL.mediaInfo
|
||||
|
||||
switch nextAudioURL.loc {
|
||||
case .remote:
|
||||
handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.url, bitrate: nextAudioURL.bitrate)
|
||||
break
|
||||
case .saved:
|
||||
handlePlaySavedAudio(withSavedUrl: nextAudioURL.url)
|
||||
break
|
||||
}
|
||||
|
||||
shouldPlayImmediately = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ extension SAPlayer {
|
||||
- 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.
|
||||
*/
|
||||
@available(*, deprecated, message: "Use subscribe without the url in the closure for current audio updates")
|
||||
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 }
|
||||
@@ -54,6 +55,19 @@ extension SAPlayer {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Subscribe to updates in elapsed time of the playing audio. Aka, the current timestamp of the audio.
|
||||
|
||||
- Note: It's recommended to have a weak reference to a class that uses this function
|
||||
|
||||
- Parameter closure: The closure that will receive the updates of the changes in 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 (_ timePosition: Double) -> ()) -> UInt {
|
||||
AudioClockDirector.shared.attachToChangesInNeedle(closure: closure)
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in elapsed time of audio.
|
||||
|
||||
@@ -83,6 +97,7 @@ extension SAPlayer {
|
||||
- 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.
|
||||
*/
|
||||
@available(*, deprecated, message: "Use subscribe without the url in the closure for current audio updates")
|
||||
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 }
|
||||
@@ -90,6 +105,21 @@ extension SAPlayer {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Subscribe to updates to changes in duration of the current audio initialized.
|
||||
|
||||
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
|
||||
|
||||
- Note: It's recommended to have a weak reference to a class that uses this function
|
||||
|
||||
- Parameter closure: The closure that will receive the updates of the changes in duration.
|
||||
- Parameter 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 (_ duration: Double) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInDuration(closure: closure)
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in duration of the current initialized audio.
|
||||
|
||||
@@ -115,6 +145,7 @@ extension SAPlayer {
|
||||
- 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.
|
||||
*/
|
||||
@available(*, deprecated, message: "Use subscribe without the url in the closure for current audio updates")
|
||||
public static func subscribe(_ closure: @escaping (_ url: URL, _ playingStatus: SAPlayingStatus) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { (key, isPlaying) in
|
||||
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
|
||||
@@ -122,6 +153,19 @@ extension SAPlayer {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Subscribe to updates to changes in the playing/paused status of audio.
|
||||
|
||||
- Note: It's recommended to have a weak reference to a class that uses this function
|
||||
|
||||
- Parameter closure: The closure that will receive the updates of the changes in 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 (_ playingStatus: SAPlayingStatus) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: closure)
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in the playing/paused status of audio.
|
||||
|
||||
@@ -149,6 +193,7 @@ extension SAPlayer {
|
||||
- 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.
|
||||
*/
|
||||
@available(*, deprecated, message: "Use subscribe without the url in the closure for current audio updates")
|
||||
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 }
|
||||
@@ -156,6 +201,21 @@ extension SAPlayer {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Subscribe to updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information. For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
|
||||
|
||||
- Note: For live streams that don't have an expected audio length from the beginning of the stream; the duration is constantly changing and equal to the total seconds buffered from the SAAudioAvailabilityRange.
|
||||
|
||||
- Note: It's recommended to have a weak reference to a class that uses this function
|
||||
|
||||
- Parameter closure: The closure that will receive the updates of the changes in duration.
|
||||
- Parameter 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 (_ buffer: SAAudioAvailabilityRange) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInBufferedRange(closure: closure)
|
||||
}
|
||||
|
||||
/**
|
||||
Stop recieving updates of changes in streaming progress.
|
||||
|
||||
@@ -207,10 +267,8 @@ extension SAPlayer {
|
||||
- Parameter url: The corresponding remote URL for the forthcoming audio file.
|
||||
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
|
||||
*/
|
||||
public static func subscribe(_ closure: @escaping (_ key: String, _ newUrl: URL) -> ()) -> UInt {
|
||||
return AudioQueueDirector.shared.attach(closure: { (key, url) in
|
||||
closure(key, url)
|
||||
})
|
||||
public static func subscribe(_ closure: @escaping (_ newUrl: URL) -> ()) -> UInt {
|
||||
return AudioQueueDirector.shared.attach(closure: closure)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,18 +25,15 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DirectorError: Error {
|
||||
case closureIsDead
|
||||
}
|
||||
|
||||
/**
|
||||
P for payload
|
||||
*/
|
||||
class DirectorThreadSafeClosures<P> {
|
||||
typealias TypeClosure = (Key, P) throws -> Void
|
||||
typealias TypeClosure = (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] = [:]
|
||||
private var cache: P? = nil
|
||||
|
||||
var count: Int {
|
||||
get {
|
||||
@@ -44,13 +41,17 @@ class DirectorThreadSafeClosures<P> {
|
||||
}
|
||||
}
|
||||
|
||||
func broadcast(key: Key, payload: P) {
|
||||
func resetCache() {
|
||||
cache = nil
|
||||
}
|
||||
|
||||
func broadcast(payload: P) {
|
||||
queue.sync {
|
||||
self.cache[key] = payload
|
||||
self.cache = payload
|
||||
var iterator = self.closures.makeIterator()
|
||||
while let element = iterator.next() {
|
||||
do {
|
||||
try element.value(key, payload)
|
||||
try element.value(payload)
|
||||
} catch {
|
||||
helperRemove(withKey: element.key)
|
||||
}
|
||||
@@ -64,9 +65,9 @@ class DirectorThreadSafeClosures<P> {
|
||||
|
||||
//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 {
|
||||
if let val = cache {
|
||||
do {
|
||||
try closure(key, val)
|
||||
try closure(val)
|
||||
} catch {
|
||||
return id
|
||||
}
|
||||
@@ -85,7 +86,7 @@ class DirectorThreadSafeClosures<P> {
|
||||
func clear() {
|
||||
queue.async(flags: .barrier) {
|
||||
self.closures.removeAll()
|
||||
self.cache.removeAll()
|
||||
self.cache = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// DirectorThreadSafeClosures.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-01-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DirectorError: Error {
|
||||
case closureIsDead
|
||||
}
|
||||
|
||||
/**
|
||||
P for payload
|
||||
*/
|
||||
class DirectorThreadSafeClosuresDeprecated<P> {
|
||||
typealias TypeClosure = (Key, P) throws -> Void
|
||||
private var queue: DispatchQueue = DispatchQueue(label: "SwiftAudioPlayer.thread_safe_map", attributes: .concurrent)
|
||||
private var closures: [UInt: TypeClosure] = [:]
|
||||
private var cache: [Key: P] = [:]
|
||||
|
||||
var count: Int {
|
||||
get {
|
||||
return closures.count
|
||||
}
|
||||
}
|
||||
|
||||
func broadcast(key: Key, payload: P) {
|
||||
queue.sync {
|
||||
self.cache[key] = payload
|
||||
var iterator = self.closures.makeIterator()
|
||||
while let element = iterator.next() {
|
||||
do {
|
||||
try element.value(key, payload)
|
||||
} catch {
|
||||
helperRemove(withKey: element.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//UInt is actually 64-bits on modern devices
|
||||
func attach(closure: @escaping TypeClosure) -> UInt {
|
||||
let id: UInt = Date.getUTC64()
|
||||
|
||||
//The director may not yet have the status yet. We should only call the closure if we have it
|
||||
//Let the caller know the immediate value. If it's dead already then stop
|
||||
for (key, val) in cache {
|
||||
do {
|
||||
try closure(key, val)
|
||||
} catch {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
//Replace what's in the map with the new closure
|
||||
helperInsert(withKey: id, closure: closure)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func detach(id: UInt) {
|
||||
helperRemove(withKey: id)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
queue.async(flags: .barrier) {
|
||||
self.closures.removeAll()
|
||||
self.cache.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
private func helperRemove(withKey key: UInt) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.closures[key] = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func helperInsert(withKey key: UInt, closure: @escaping TypeClosure) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.closures[key] = closure
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioPlayer'
|
||||
s.version = '4.1.0'
|
||||
s.version = '7.2.0'
|
||||
s.summary = 'SwiftAudioPlayer is a Swift based audio player that can handle streaming from a remote location and audio manipulation.'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
@@ -26,9 +26,9 @@ SwiftAudioPlayer is a Swift based audio player that can handle streaming from a
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'tanhakabir' => 'tanhakabir.ca@gmail.com', 'JonMercer' => 'mercer.jon@gmail.com' }
|
||||
s.source = { :git => 'https://github.com/tanhakabir/SwiftAudioPlayer.git', :tag => s.version.to_s }
|
||||
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
|
||||
s.social_media_url = 'https://twitter.com/_tanhakabir'
|
||||
|
||||
s.ios.deployment_target = '10.0'
|
||||
s.platforms = { :ios => '10.0', :tvos => '10.0' }
|
||||
|
||||
s.source_files = 'Source/**/*'
|
||||
s.swift_version = '5.0'
|
||||
|
||||
Reference in New Issue
Block a user