Compare commits

...

30 Commits

Author SHA1 Message Date
tanhakabir d78536033a Release 7.5.0 2021-09-27 17:34:13 -07:00
tanhakabir 73bc016d9d Shorten timeout to 30 seconds 2021-09-27 17:28:17 -07:00
tanhakabir 911f47e1e2 Push up error messages from download layer 2021-09-27 17:03:16 -07:00
tanhakabir 879a2816be Release 7.4.0 2021-09-10 11:09:37 -07:00
tanhakabir 2c3ebefd54 Merge pull request #149 from jiangdi0924/master
SAPlayer.shared.audioQueued  no content ,So i fixed it. ( Not sure if it is a good solution, Please review it)
2021-08-26 15:26:51 -07:00
Norton 862dd47509 I found out that SAPlayer.shared.audioQueued no content, and i think it's maybe a bug. 2021-08-26 15:17:55 +08:00
tanhakabir dee22d5193 Merge pull request #147 from cntrump/pr/improve_lockscreen_control
Lockscreen Media Player as public for other players.
2021-08-20 15:24:01 -07:00
Lvv.me 9dd479e377 Lockscreen Media Player as public for other players. 2021-08-19 20:08:20 +08:00
tanhakabir ddf26e206e Release 7.3.0 (adds Changelog) 2021-08-17 10:16:26 -07:00
tanhakabir e319134eb8 Merge pull request #144 from cntrump/pr/replace_deprecated_method
Replace deprecated subscribe method.
2021-08-17 10:12:51 -07:00
Lvv.me 9599f66a0f Replace deprecated subscribe method. 2021-08-17 19:58:44 +08:00
tanhakabir ef231a2570 Release 7.2.1 2021-08-17 03:51:21 -07:00
tanhakabir 350e6ec064 Fix directory bug 2021-08-17 03:51:01 -07:00
tanhakabir ad63b89ede Release 7.2.0 2021-08-17 03:38:52 -07:00
tanhakabir 43e887b823 Merge pull request #143 from tanhakabir/issue-138
Experimental set download location
2021-08-17 03:38:28 -07:00
tanhakabir 006b94ea10 experimental set download location 2021-08-17 03:38:00 -07:00
tanhakabir abb0a29fb4 Release 7.1.0 2021-08-17 03:13:19 -07:00
tanhakabir ed3ba9698d Merge pull request #132 from cntrump/pr_fix_seek_fail
Fix seek fail issue when data is not loaded for AudioStreamEngine.
2021-08-17 03:12:43 -07:00
tanhakabir 294902e3fe Release 7.0.1 2021-08-17 03:06:08 -07:00
tanhakabir 764f0b130b Merge pull request #141 from tanhakabir/issue-140
Refractor cache for broadcasting updates
2021-08-17 03:04:48 -07:00
tanhakabir 952dc1978f Update README.md 2021-08-17 03:04:11 -07:00
tanhakabir 1752572770 clean up example app 2021-08-17 02:59:42 -07:00
tanhakabir 223600f30c add deprecation warnings 2021-08-17 02:39:31 -07:00
tanhakabir 4d90c539cd update streaming director to new broadcasting 2021-08-17 02:38:16 -07:00
tanhakabir fd9b451f45 remove unnecessary key from queue director 2021-08-17 02:22:29 -07:00
tanhakabir fd3e78f13c refractor director broadcasting 2021-08-17 02:09:57 -07:00
tanhakabir af3d553011 Release 6.4.0 2021-08-16 21:09:48 -07:00
tanhakabir d0f9127c65 Merge pull request #139 from tanhakabir/issue-128
Fix runtime error on queuing audio.
2021-08-16 21:09:04 -07:00
tanhakabir c25071d83a Merge branch 'master' into pr_fix_seek_fail 2021-08-12 23:56:19 -07:00
Lvv.me 19db1fc74b Fix seek fail issue when data is not loaded for AudioStreamEngine. 2021-08-09 15:50:38 +08:00
23 changed files with 491 additions and 206 deletions
+10
View File
@@ -0,0 +1,10 @@
# Changelog
## 7.5.0
- Propagate up any errors from downloading audio. This will cause breaking changes to `SAPlayer.Downloader.downloadAudio(...)`
## 7.3.0
- Take in PR from @cntrump to use the non-deprecated subscription pattern in loop feature
+10 -4
View File
@@ -15,6 +15,7 @@
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 */; };
@@ -39,7 +40,7 @@
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 */; };
@@ -100,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>"; };
@@ -133,6 +135,7 @@
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>"; };
A4883AC926CC25DE0073B8B6 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerDownloader.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>"; };
@@ -253,6 +256,7 @@
children = (
15DF3E7F1B5E10B1BBE49D3E9A67C938 /* LICENSE */,
B8C829A46249957CD3056074B5CC0BBB /* README.md */,
A4883AC926CC25DE0073B8B6 /* CHANGELOG.md */,
6EC04ECC8F7CB2AF2E4E042A6A8ECFA1 /* SwiftAudioPlayer.podspec */,
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */,
);
@@ -264,10 +268,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>";
@@ -516,6 +521,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 */,
@@ -554,7 +560,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;
+18 -31
View File
@@ -114,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, albumTitle: nil, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
}
func checkIfAudioDownloaded() {
@@ -146,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)
@@ -177,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)
@@ -194,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
@@ -222,13 +203,14 @@ class ViewController: UIViewController {
}
}
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)"
}
}
@@ -298,14 +280,18 @@ class ViewController: UIViewController {
} else {
downloadButton.setTitle("Cancel 0%", for: .normal)
isDownloading = true
SAPlayer.Downloader.downloadAudio(withRemoteUrl: selectedAudio.url, completion: { [weak self] url in
SAPlayer.Downloader.downloadAudio(withRemoteUrl: selectedAudio.url, completion: { [weak self] (url, error) in
guard let self = self else { return }
guard error == nil else {
DispatchQueue.main.async {
self.currentUrlLocationLabel.text = "ERROR! \(error!.localizedDescription)"
}
return
}
DispatchQueue.main.async {
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
self.selectedAudio.addSavedUrl(url)
SAPlayer.shared.startSavedAudio(withSavedUrl: url, mediaInfo: self.selectedAudio.lockscreenInfo)
self.lastPlayedAudioIndex = self.selectedAudio.index
}
})
streamButton.isEnabled = false
@@ -320,6 +306,7 @@ class ViewController: UIViewController {
@IBAction func streamTouched(_ sender: Any) {
if !isStreaming {
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 {
+14 -8
View File
@@ -78,13 +78,10 @@ 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
}
@@ -168,11 +165,13 @@ 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.
### Lockscreen Media Player
### LockScreen Media Player
Update and set what displays on the lockscreen's media player when the player is active.
@@ -257,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)
@@ -302,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
+69 -5
View File
@@ -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)
}
}
+3 -3
View File
@@ -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
@@ -26,18 +26,25 @@ import Foundation
class StreamingDownloadDirector {
static let shared = StreamingDownloadDirector()
private var currentAudioKey: Key?
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
private init() {}
func create() {}
func setKey(_ key: Key) {
currentAudioKey = key
}
func resetCache() {
closures.resetCache()
}
func clear() {
closures.clear()
}
func attach(closure: @escaping (Key, Double) throws -> Void) -> UInt {
func attach(closure: @escaping (Double) throws -> Void) -> UInt {
return closures.attach(closure: closure)
}
@@ -48,6 +55,11 @@ class StreamingDownloadDirector {
extension StreamingDownloadDirector {
func didUpdate(_ key: Key, networkStreamProgress: Double) {
closures.broadcast(key: key, payload: networkStreamProgress)
guard key == currentAudioKey else {
Log.debug("silence old updates")
return
}
closures.broadcast(payload: networkStreamProgress)
}
}
+3 -2
View File
@@ -115,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
+13 -2
View File
@@ -157,9 +157,11 @@ class AudioStreamEngine: AudioEngine {
delegate?.didError()
}
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (key, progress) in
StreamingDownloadDirector.shared.setKey(key)
StreamingDownloadDirector.shared.resetCache()
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (progress) in
guard let self = self else { return }
guard key == url.key else { return }
// polling for buffers when we receive data. This won't be throttled on fresh new audio or seeked audio but in all other cases it most likely will be throttled
self.pollForNextBuffer() // no buffer updates because thread issues if I try to update buffer status in streaming listener
@@ -271,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
+1 -2
View File
@@ -155,9 +155,8 @@ class AudioParser: AudioParsable {
self.throttler = AudioThrottler(withRemoteUrl: url, withDelegate: self)
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (key, progress) in
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (progress) in
guard let self = self else { return }
guard key == url.key else { return }
self.networkProgress = progress
// initially parse a bunch of packets
+26 -11
View File
@@ -27,20 +27,35 @@ import Foundation
import MediaPlayer
import UIKit
public protocol LockScreenViewPresenter : AnyObject {
func getIsPlaying() -> Bool
func handlePlay()
func handlePause()
func handleSkipBackward()
func handleSkipForward()
func handleSeek(toNeedle needle: Double)
}
// MARK: - Set up lockscreen audio controls
// Documentation: https://developer.apple.com/documentation/avfoundation/media_assets_playback_and_editing/creating_a_basic_video_player_ios_and_tvos/controlling_background_audio
protocol LockScreenViewProtocol {
public protocol LockScreenViewProtocol {
var skipForwardSeconds: Double { get set }
var skipBackwardSeconds: Double { get set }
}
extension LockScreenViewProtocol {
public extension LockScreenViewProtocol {
func clearLockScreenInfo() {
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.removeTarget(nil)
commandCenter.pauseCommand.removeTarget(nil)
commandCenter.skipBackwardCommand.removeTarget(nil)
commandCenter.skipForwardCommand.removeTarget(nil)
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
}
@available(iOS 10.0, tvOS 10.0, *)
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo?, duration: Duration) {
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo?, duration: Double) {
var nowPlayingInfo:[String : Any] = [:]
guard let info = info else {
@@ -82,7 +97,7 @@ extension LockScreenViewProtocol {
}
// https://stackoverflow.com/questions/36754934/update-mpremotecommandcenter-play-pause-button
func setLockScreenControls(presenter: SAPlayerPresenter) { //FIXME: this is weird
func setLockScreenControls(presenter: LockScreenViewPresenter) {
// Get the shared MPRemoteCommandCenter
let commandCenter = MPRemoteCommandCenter.shared()
@@ -146,29 +161,29 @@ extension LockScreenViewProtocol {
}
}
func updateLockscreenElapsedTime(needle: Needle) {
func updateLockScreenElapsedTime(needle: Double) {
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: Double(needle))
}
func updateLockscreenPlaybackDuration(duration: Duration) {
func updateLockScreenPlaybackDuration(duration: Double) {
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: duration)
}
func updateLockscreenPaused(){
func updateLockScreenPaused(){
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
}
func updateLockscreenPlaying(){
func updateLockScreenPlaying(){
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
}
func updateLockscreenChangePlaybackRate(speed: Float){
func updateLockScreenChangePlaybackRate(speed: Float){
if speed > 0.0{
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = speed
}
}
func updateLockscreenSkipIntervals() {
func updateLockScreenSkipIntervals() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.skipBackwardCommand.isEnabled = skipBackwardSeconds > 0
commandCenter.skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
+10 -3
View File
@@ -30,10 +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()
@@ -47,13 +49,14 @@ protocol AudioDataManagable {
func deleteStream(withRemoteURL url: AudioURL)
func getPersistedUrl(withRemoteURL url: AudioURL) -> URL?
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL) -> ())
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL, Error?) -> ())
func cancelDownload(withRemoteURL url: AudioURL)
func deleteDownload(withLocalURL url: URL)
}
class AudioDataManager: AudioDataManagable {
var allowCellular: Bool = true
var downloadDirectory: FileManager.SearchPathDirectory = .documentDirectory
static let shared: AudioDataManagable = AudioDataManager()
@@ -110,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
}
@@ -164,12 +171,12 @@ extension AudioDataManager {
return FileStorage.Audio.locate(url.key)
}
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL) -> ()) {
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL, Error?) -> ()) {
let key = url.key
if let savedUrl = FileStorage.Audio.locate(key), FileStorage.Audio.isStored(key) {
globalDownloadProgressCallback(key, 1.0)
completion(savedUrl)
completion(savedUrl, nil)
return
}
@@ -35,7 +35,7 @@ protocol AudioDataDownloadable: AnyObject {
func getProgressOfDownload(withID id: ID) -> Double?
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ())
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL, Error?) -> ())
func stop(withID id: ID, callback: ((_ dataSoFar: Data?, _ totalBytesExpected: Int64?) -> ())?)
func pauseAllActive() //Because of streaming
func resumeAllActive() //Because of streaming
@@ -56,6 +56,7 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
config.isDiscretionary = !allowsCellularDownload
config.sessionSendsLaunchEvents = true
config.allowsCellularAccess = allowsCellularDownload
config.timeoutIntervalForRequest = 30
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
@@ -89,7 +90,7 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
return activeDownloads.filter { $0.info.id == id }.first?.progress
}
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ()) {
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL, Error?) -> ()) {
Log.info("startExternal paramID: \(id) activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
let temp = activeDownloads.filter { $0.info.id == id }.count
guard temp == 0 else {
@@ -204,7 +205,7 @@ extension AudioDownloadWorker: URLSessionDownloadDelegate {
completionHandler(task.info.id, nil)
for handler in task.info.completionHandlers {
handler(destinationUrl)
handler(destinationUrl, nil)
}
activeDownloads = activeDownloads.filter { $0 != task }
@@ -238,6 +239,9 @@ extension AudioDownloadWorker: URLSessionDownloadDelegate {
for download in activeDownloads {
if download.task == task {
for handler in download.info.completionHandlers {
handler(download.info.remoteUrl, e)
}
completionHandler(download.info.id, e)
activeDownloads = activeDownloads.filter { $0.task != task }
}
@@ -281,7 +285,7 @@ extension AudioDownloadWorker {
let id: ID
let remoteUrl: URL
let rank: Int
var completionHandlers: [(URL) -> ()]
var completionHandlers: [(URL, Error?) -> ()]
func hash(into hasher: inout Hasher) {
hasher.combine(id)
@@ -328,11 +332,11 @@ extension Set where Element == AudioDownloadWorker.DownloadInfo {
return ret
}
mutating func updatePreservingOldCompletionHandlers(withID id: ID, withRemoteUrl remoteUrl: URL, completion: ((URL) -> ())? = nil) -> AudioDownloadWorker.DownloadInfo {
mutating func updatePreservingOldCompletionHandlers(withID id: ID, withRemoteUrl remoteUrl: URL, completion: ((URL, Error?) -> ())? = nil) -> AudioDownloadWorker.DownloadInfo {
let rank = Date.getUTC()
let tempHandlers: [(URL) -> ()] = completion != nil ? [completion!] : []
let tempHandlers: [(URL, Error?) -> ()] = completion != nil ? [completion!] : []
var newInfo = AudioDownloadWorker.DownloadInfo.init(id: id, remoteUrl: remoteUrl, rank: rank, completionHandlers: tempHandlers)
+7 -2
View File
@@ -64,9 +64,14 @@ struct FileStorage {
// MARK:- Audio
extension FileStorage {
struct Audio {
private static let directory: FileManager.SearchPathDirectory = .documentDirectory
private init() {}
private static var directory: FileManager.SearchPathDirectory {
get {
return AudioDataManager.shared.downloadDirectory
}
}
static func isStored(_ id: ID) -> Bool {
guard let url = locate(id)?.path else {
return false
@@ -103,7 +108,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) {
+9 -3
View File
@@ -182,7 +182,14 @@ public class SAPlayer {
/**
List of queued audio for playback. You can edit this list as you wish to modify the queue.
*/
public var audioQueued: [SAAudioQueueItem] = []
public var audioQueued: [SAAudioQueueItem] {
get {
return presenter.audioQueue
}
set {
presenter.audioQueue = newValue
}
}
/**
Total duration of current audio initialized. Returns nil if no audio is initialized in player.
@@ -585,8 +592,7 @@ extension SAPlayer: SAPlayerDelegate {
}
internal func seekEngine(toNeedle needle: Needle) {
var seekToNeedle = needle < 0 ? 0 : needle
seekToNeedle = needle > Needle(duration ?? 0) ? Needle(duration ?? 0) : needle
let seekToNeedle = needle < 0 ? 0 : needle
player?.seek(toNeedle: seekToNeedle)
}
}
+10 -1
View File
@@ -47,7 +47,7 @@ extension SAPlayer {
- Parameter completion: Completion handler that will return once the download is successful and complete.
- Parameter savedUrl: The url of where the audio was saved locally on the device. Will receive once download has completed.
*/
public static func downloadAudio(withRemoteUrl url: URL, completion: @escaping (_ savedUrl: URL) -> ()) {
public static func downloadAudio(withRemoteUrl url: URL, completion: @escaping (_ savedUrl: URL, _ error: Error?) -> ()) {
SAPlayer.shared.addUrlToMapping(url: url)
AudioDataManager.shared.startDownload(withRemoteURL: url, completion: completion)
}
@@ -109,5 +109,14 @@ extension SAPlayer {
AudioDataManager.shared.setAllowCellularDownloadPreference(allowUsingCellularData)
}
}
/**
EXPERIMENTAL!
*/
public static var downloadDirectory: FileManager.SearchPathDirectory = .documentDirectory {
didSet {
AudioDataManager.shared.setDownloadDirectory(downloadDirectory)
}
}
}
}
+1 -1
View File
@@ -147,7 +147,7 @@ extension SAPlayer {
guard playingStatusId == nil else { return }
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe({ (url, status) in
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe({ (status) in
if status == .ended && enabled {
SAPlayer.shared.seekTo(seconds: 0.0)
SAPlayer.shared.play()
+83 -102
View File
@@ -39,15 +39,48 @@ class SAPlayerPresenter {
private var urlKeyMap: [Key: URL] = [:]
var durationRef:UInt = 0
var needleRef:UInt = 0
var playingStatusRef:UInt = 0
var durationRef: UInt = 0
var needleRef: UInt = 0
var playingStatusRef: UInt = 0
var audioQueue: [SAAudioQueueItem] = []
init(delegate: SAPlayerDelegate?) {
self.delegate = delegate
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)
})
delegate?.setLockScreenControls(presenter: self)
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? {
@@ -60,28 +93,35 @@ class SAPlayerPresenter {
func handleClear() {
delegate?.clearEngine()
AudioClockDirector.shared.resetCache()
needle = nil
duration = nil
key = nil
delegate?.mediaInfo = nil
delegate?.clearLockScreenInfo()
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
}
func handlePlaySavedAudio(withSavedUrl url: URL) {
attachForUpdates(url: url)
resetCacheForNewAudio(url: url)
delegate?.setLockScreenControls(presenter: self)
delegate?.startAudioDownloaded(withSavedUrl: url)
}
func handlePlayStreamedAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate) {
attachForUpdates(url: url)
resetCacheForNewAudio(url: url)
delegate?.setLockScreenControls(presenter: self)
delegate?.startAudioStreamed(withRemoteUrl: url, bitrate: bitrate)
}
private func resetCacheForNewAudio(url: URL) {
self.key = url.key
urlKeyMap[url.key] = url
AudioClockDirector.shared.setKey(url.key)
AudioClockDirector.shared.resetCache()
}
func handleQueueStreamedAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate) {
audioQueue.append(SAAudioQueueItem(loc: .remote, url: url, mediaInfo: mediaInfo, bitrate: bitrate))
}
@@ -107,84 +147,16 @@ class SAPlayerPresenter {
return urls
}
private func attachForUpdates(url: URL) {
detachFromUpdates()
self.key = url.key
urlKeyMap[url.key] = url
durationRef = AudioClockDirector.shared.attachToChangesInDuration(closure: { [weak self] (key, duration) in
guard let self = self else { throw DirectorError.closureIsDead }
guard key == self.key else {
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
return
}
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
self.duration = duration
self.delegate?.setLockScreenInfo(withMediaInfo: self.delegate?.mediaInfo, duration: duration)
})
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (key, needle) in
guard let self = self else { throw DirectorError.closureIsDead }
guard key == self.key else {
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
return
}
self.needle = needle
self.delegate?.updateLockscreenElapsedTime(needle: needle)
})
playingStatusRef = AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { [weak self] (key, isPlaying) in
guard let self = self else { throw DirectorError.closureIsDead }
guard key == self.key else {
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
return
}
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.
guard isPlaying != self.isPlaying else { return }
self.isPlaying = isPlaying
if(self.isPlaying == .ended) {
self.playNextAudioIfExists()
}
})
}
private func detachFromUpdates() {
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
}
func handleStopStreamingAudio() {
delegate?.clearEngine()
detachFromUpdates()
AudioClockDirector.shared.resetCache()
}
}
//MARK:- Used by outside world including:
// SPP, lock screen, directors
extension SAPlayerPresenter {
func handlePause() {
delegate?.pauseEngine()
self.delegate?.updateLockscreenPaused()
}
func handlePlay() {
delegate?.playEngine()
self.delegate?.updateLockscreenPlaying()
}
func handleTogglePlayingAndPausing() {
if isPlaying == .playing {
handlePause()
@@ -192,35 +164,46 @@ extension SAPlayerPresenter {
handlePlay()
}
}
func handleSkipForward() {
guard let forward = delegate?.skipForwardSeconds else { return }
handleSeek(toNeedle: (needle ?? 0) + forward)
func handleAudioRateChanged(rate: Float) {
delegate?.updateLockScreenChangePlaybackRate(speed: rate)
}
func handleScrubbingIntervalsChanged() {
delegate?.updateLockScreenSkipIntervals()
}
}
//MARK:- For lock screen
extension SAPlayerPresenter : LockScreenViewPresenter {
func getIsPlaying() -> Bool {
return isPlaying == .playing
}
func handlePlay() {
delegate?.playEngine()
self.delegate?.updateLockScreenPlaying()
}
func handlePause() {
delegate?.pauseEngine()
self.delegate?.updateLockScreenPaused()
}
func handleSkipBackward() {
guard let backward = delegate?.skipForwardSeconds else { return }
handleSeek(toNeedle: (needle ?? 0) - backward)
}
func handleSkipForward() {
guard let forward = delegate?.skipForwardSeconds else { return }
handleSeek(toNeedle: (needle ?? 0) + forward)
}
func handleSeek(toNeedle needle: Needle) {
delegate?.seekEngine(toNeedle: needle)
}
func handleAudioRateChanged(rate: Float) {
delegate?.updateLockscreenChangePlaybackRate(speed: rate)
}
func handleScrubbingIntervalsChanged() {
delegate?.updateLockscreenSkipIntervals()
}
}
//MARK:- For lock screen
extension SAPlayerPresenter {
func getIsPlaying() -> Bool {
return isPlaying == .playing
}
}
//MARK:- AVAudioEngineDelegate
@@ -239,11 +222,9 @@ extension SAPlayerPresenter {
return
}
let nextAudioURL = audioQueue.removeFirst()
let key = nextAudioURL.url.key
Log.info("getting ready to play \(nextAudioURL)")
AudioQueueDirector.shared.changeInQueue(key, url: nextAudioURL.url)
AudioQueueDirector.shared.changeInQueue(url: nextAudioURL.url)
handleClear()
+62 -4
View File
@@ -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)
}
/**
+12 -11
View File
@@ -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
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '6.3.1'
s.version = '7.5.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.