Compare commits

...

30 Commits

Author SHA1 Message Date
Tanha 8bf6cbb56e Release 1.0.3 2019-11-18 13:46:50 -08:00
Tanha b97f97ca5e Fix fatal error on iOS 10.0
close #3
2019-11-18 13:39:06 -08:00
Tanha 0c7bcdcf90 Fix issue on example app that prevented downloaded audio being playable 2019-11-18 11:42:02 -08:00
Tanha 840122e603 remove build badge from README 2019-04-27 23:00:46 -07:00
Tanha 8518d10c6d v1.0.2 2019-04-27 22:56:57 -07:00
Tanha f214be28a9 nit 2019-04-27 22:37:17 -07:00
Tanha f219d9d1a0 nit 2019-04-27 22:36:37 -07:00
Tanha 8797c0d917 add API documentation for Downloader 2019-04-27 22:35:21 -07:00
Tanha 0121d05dff refractor deletion of downloaded files 2019-04-27 21:54:42 -07:00
Tanha 26faf62657 documentation for downloading 2019-04-27 21:42:12 -07:00
Tanha 61e79d067a ensure cancelling download also removed from queued downloads 2019-04-27 20:46:53 -07:00
Tanha 103838d1b8 add UI to see where file is saved on device 2019-04-27 19:43:43 -07:00
Tanha 47de2a5251 fix double download bug 2019-04-27 19:40:48 -07:00
Tanha d4d8f767e3 document downloading audio 2019-04-27 18:59:14 -07:00
tanhakabir c75da619cf Merge pull request #2 from tanhakabir/refractor_downloaded_audio
Refractor downloaded audio
2019-04-22 15:30:51 -07:00
Tanha aea6f5efaa add completion handler for individual entities to receive when download complete upon calling start 2019-04-22 15:30:11 -07:00
Tanha 2625b8f4db remove unused resume data in download worker 2019-04-10 14:45:56 -07:00
Tanha e6460513ea start piping for passing completion handlers for downloads 2019-02-28 14:45:50 -08:00
tanhakabir a2504f2726 Update README.md 2019-02-25 15:56:10 -08:00
Tanha 23f445ce4d seperate downloader from rest of SAPlayer implementation 2019-02-25 01:33:18 -08:00
Tanha 61fe0c6ebb nit 2019-02-24 23:24:06 -08:00
Tanha 72c4335386 nit 2019-02-24 23:23:57 -08:00
Tanha 640f0b92f0 make lockscreen artwork optional 2019-02-24 23:03:43 -08:00
Tanha c0f8db29c0 nit spelling 2019-02-24 21:36:46 -08:00
Tanha 285cd92514 nit 2019-02-24 21:35:10 -08:00
Tanha a5293a5b39 nit 2019-02-24 21:31:59 -08:00
Tanha 8430a7e8ce nit 2019-02-24 21:31:04 -08:00
Tanha 34e430713b nit 2019-02-24 21:25:10 -08:00
Tanha d23a5f8d62 Update README with Updates API 2019-02-24 21:24:04 -08:00
Tanha 9f89944bc5 nit 2019-02-24 20:40:32 -08:00
23 changed files with 392 additions and 81 deletions
+4
View File
@@ -42,6 +42,7 @@
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */; };
A4681FE0220113E40018AB51 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F802200D0500018AB51 /* Log.swift */; };
A4681FE1220113E70018AB51 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8B2200DDD50018AB51 /* Constants.swift */; };
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */; };
A4FBA6B2221B538E00D5A353 /* DownloadProgressDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */; };
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */; };
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */; };
@@ -124,6 +125,7 @@
A4681FBC220100AB0018AB51 /* AudioStreamEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamEngine.swift; sourceTree = "<group>"; };
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenViewProtocol.swift; sourceTree = "<group>"; };
A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgressDirector.swift; sourceTree = "<group>"; };
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerDownloader.swift; sourceTree = "<group>"; };
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SALockScreenInfo.swift; sourceTree = "<group>"; };
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerUpdateSubscription.swift; sourceTree = "<group>"; };
A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAAudioAvailabilityRange.swift; sourceTree = "<group>"; };
@@ -344,6 +346,7 @@
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */,
A4681F8D2200E00E0018AB51 /* SAPlayer.swift */,
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */,
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */,
A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */,
A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */,
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */,
@@ -530,6 +533,7 @@
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */,
A4681FD3220113B60018AB51 /* AudioParserPropertyListener.swift in Sources */,
A4681FCA220113940018AB51 /* AudioClockDirector.swift in Sources */,
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */,
A4681FD0220113A70018AB51 /* AudioConverterErrors.swift in Sources */,
A4FBA6B2221B538E00D5A353 /* DownloadProgressDirector.swift in Sources */,
A4681FD7220113C30018AB51 /* StreamProgressPTO.swift in Sources */,
@@ -105,6 +105,12 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="remote url: " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1IX-z5-wWx">
<rect key="frame" x="16" y="207" width="343" height="16"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
@@ -117,6 +123,7 @@
<constraint firstItem="joK-xi-MCo" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="AH1-Uu-eLB"/>
<constraint firstItem="joK-xi-MCo" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" constant="60" id="Ba7-nd-oCD"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="centerY" secondItem="j3w-gr-HzF" secondAttribute="centerY" id="Fvd-7V-Rr8"/>
<constraint firstItem="1IX-z5-wWx" firstAttribute="leading" secondItem="joK-xi-MCo" secondAttribute="leading" id="GeX-7f-jzu"/>
<constraint firstItem="0QE-3F-a4G" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="jUc-tP-CC5" secondAttribute="trailing" constant="8" symbolic="YES" id="JP5-yW-eVB"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="w2a-RA-zmI" secondAttribute="bottom" constant="200" id="K1K-8N-SpD"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="NOY-IO-NIJ"/>
@@ -126,12 +133,14 @@
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="top" secondItem="j3w-gr-HzF" secondAttribute="bottom" constant="8" id="Wwx-Uo-yIC"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="a66-h4-WVf"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="aKt-EV-Bwd"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="top" secondItem="1IX-z5-wWx" secondAttribute="bottom" constant="27" id="bIq-V0-Sac"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="62.5" id="cH6-q6-Lel"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cgM-Nj-yit"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="dLw-rF-Pfb"/>
<constraint firstItem="w2a-RA-zmI" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="daz-b0-eCC"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="tFH-sY-Xu9" secondAttribute="trailing" constant="8" symbolic="YES" id="fS9-Ce-4ph"/>
<constraint firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" constant="16" id="gdg-7Y-7la"/>
<constraint firstAttribute="trailing" secondItem="1IX-z5-wWx" secondAttribute="trailing" constant="16" id="hHM-jO-RZd"/>
<constraint firstItem="6d9-Bc-hIz" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="m9s-An-IWV"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="top" secondItem="yUQ-mI-ozK" secondAttribute="bottom" constant="8" id="oaW-rr-UVN"/>
<constraint firstAttribute="trailing" secondItem="0QE-3F-a4G" secondAttribute="trailing" constant="62.5" id="tg1-gr-hdd"/>
@@ -145,6 +154,7 @@
<outlet property="audioSelector" destination="joK-xi-MCo" id="GmY-Xg-be0"/>
<outlet property="bufferProgress" destination="lTK-Hd-Tl2" id="54k-by-qb2"/>
<outlet property="currentTimestampLabel" destination="j3w-gr-HzF" id="5Lh-aS-pat"/>
<outlet property="currentUrlLocationLabel" destination="1IX-z5-wWx" id="MuO-fF-ZxL"/>
<outlet property="downloadButton" destination="KDu-ea-kF8" id="5o4-1h-y06"/>
<outlet property="durationLabel" destination="Urj-Dv-41y" id="mIq-eh-int"/>
<outlet property="playPauseButton" destination="jUc-tP-CC5" id="e9C-zV-A1B"/>
+23 -5
View File
@@ -48,10 +48,16 @@ class ViewController: UIViewController {
if SAPlayer.Downloader.isDownloaded(withRemoteUrl: selectedAudio.url) {
downloadButton.setTitle("Delete downloaded", for: .normal)
streamButton.isEnabled = false
} else {
downloadButton.setTitle("Download", for: .normal)
streamButton.isEnabled = true
}
self.currentUrlLocationLabel.text = "remote url: \(selectedAudio.url.absoluteString)"
}
}
@IBOutlet weak var currentUrlLocationLabel: UILabel!
@IBOutlet weak var bufferProgress: UIProgressView!
@IBOutlet weak var scrubberSlider: UISlider!
@@ -94,6 +100,7 @@ class ViewController: UIViewController {
adjustSpeed()
isPlayable = false
selectedAudio = AudioInfo(index: 0)
_ = SAPlayer.Updates.Duration.subscribe { [weak self] (url, duration) in
guard let self = self else { return }
@@ -113,12 +120,15 @@ class ViewController: UIViewController {
}
_ = SAPlayer.Updates.AudioDownloading.subscribe { [weak self] (url, progress) in
print(progress)
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
if self.isDownloading {
self.downloadButton.setTitle("Cancel \(String(format: "%02d", (progress * 100)))%", for: .normal)
DispatchQueue.main.async {
UIView.performWithoutAnimation {
self.downloadButton.setTitle("Cancel \(String(format: "%.2f", (progress * 100)))%", for: .normal)
}
}
}
}
@@ -175,15 +185,23 @@ class ViewController: UIViewController {
@IBAction func downloadTouched(_ sender: Any) {
if !isDownloading {
if SAPlayer.Downloader.isDownloaded(withRemoteUrl: selectedAudio.url) {
SAPlayer.Downloader.deleteDownload(withRemoteUrl: selectedAudio.url)
if let savedUrl = SAPlayer.Downloader.getSavedUrl(forRemoteUrl: selectedAudio.url) {
SAPlayer.Downloader.deleteDownloaded(withSavedUrl: savedUrl)
downloadButton.setTitle("Download", for: .normal)
streamButton.isEnabled = true
isDownloading = false
} else {
downloadButton.setTitle("Cancel 0%", for: .normal)
isDownloading = true
SAPlayer.Downloader.downloadAudio(withRemoteUrl: selectedAudio.url)
SAPlayer.Downloader.downloadAudio(withRemoteUrl: selectedAudio.url, completion: { [weak self] url in
DispatchQueue.main.async {
self?.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
if let selectedUrl = self?.selectedAudio.url {
SAPlayer.shared.initializeAudio(withRemoteUrl: selectedUrl)
}
}
})
streamButton.isEnabled = false
}
} else {
+116 -5
View File
@@ -1,6 +1,5 @@
# SwiftAudioPlayer
[![CI Status](https://img.shields.io/travis/tanhakabir/SwiftAudioPlayer.svg?style=flat)](https://travis-ci.org/tanhakabir/SwiftAudioPlayer)
[![Version](https://img.shields.io/cocoapods/v/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
[![License](https://img.shields.io/cocoapods/l/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
@@ -33,20 +32,20 @@ pod 'SwiftAudioPlayer'
### Usage
To play remote audio:
```
```swift
let url = URL(string: "https://randomwebsite.com/audio.mp3")!
SAPlayer.shared.initializeAudio(withRemoteUrl: url)
SAPlayer.shared.play()
```
To set the display information for the lockscreen:
```
```swift
let info = SALockScreenInfo(title: "Random audio", artist: "Foo", artwork: UIImage(), releaseDate: 123456789)
SAPlayer.shared.mediaInfo = info
```
To receive streaming progress:
```
```swift
@IBOutlet weak var bufferProgress: UIProgressView!
override func viewDidLoad() {
@@ -64,7 +63,11 @@ override func viewDidLoad() {
}
}
```
Look at the [Updates](#SAPlayer.Updates) section to see usage details and other updates to follow.
**Important:** For app in background downloading please refer to [note](#important-step-for-background-downloads).
For more details and specifics look at the [API documentation](#api-in-detail) below.
## Contact
@@ -79,6 +82,114 @@ Feel free to reach out to either of us:
[tanhakabir](https://github.com/tanhakabir), tanhakabir.ca@gmail.com
[JonMercer](https://github.com/JonMercer), mercer.jon@gmail.com
## License
### License
SwiftAudioPlayer is available under the MIT license. See the LICENSE file for more info.
---
# API in detail
## SAPlayer.Downloader
Use functionaity from Downloader to save audio files from remote locations for future offline playback.
Audio files are saved under custom naming scheme on device and are recoverable with original remote URL for file.
#### Important step for background downloads
To ensure that your app will keep downloading audio in the background be sure to add the following to `AppDelegate.swift`:
```swift
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
SAPlayer.Downloader.setBackgroundCompletionHandler(completionHandler)
}
```
### Downloading
Downloads will be held on pause when active stream is started, and will resume downloads when streaming is done.
Use the following to start downloading audio in the background:
```swift
func downloadAudio(withRemoteUrl url: URL, completion: @escaping (_ savedUrl: URL) -> ())
```
It will call the completion handler you pass after successful download with the location of the downloaded file on the device.
And use the following to stop any active or prevent future downloads of the corresponding remote URL:
```swift
func cancelDownload(withRemoteUrl url: URL)
```
### Manage Downloaded
Use the following to manage downloaded audio files.
Checks if downloaded already:
```swift
func isDownloaded(withRemoteUrl url: URL) -> Bool
```
Get URL of audio file saved on device corresponding to remote location:
```swift
func getSavedUrl(forRemoteUrl url: URL) -> URL?
```
Delete downloaded audio if it exists:
```swift
func deleteDownloaded(withSavedUrl url: URL)
```
## SAPlayer.Updates
Receive updates for changing values from the player, such as the duration, elapsed time of playing audio, download progress, and etc.
All subscription functions for updates take the form of:
```swift
func subscribe(_ closure: @escaping (_ url: URL, _ payload: <Payload>) -> ()) -> UInt
```
- `closure`: The closure that will receive the updates. It's recommended to have a weak reference to a class that uses these functions.
- `url`: The corresponding remote URL for the update. In the case there might be multiple files observed, such as downloading many files at once or switching over from playing one audio to another and the updates corresponding to the previous aren't silenced on switch-over.
- `payload`: The updated value.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
Similarily unsubscribe takes the form of:
```swift
func unsubscribe(_ id: UInt)
```
- `id`: The closure with this id will stop receiving updates.
### ElapsedTime
Payload = `Double`
Changes in the timestamp/elapsed time of the current initialized audio. Aka, where the scrubber's pointer of the audio should be at.
Subscribe to this to update views on changes in position of which part of audio is being played.
### Duration
Payload = `Double`
Changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data.
### PlayingStatus
Payload = `Bool`
Changes in the playing/paused status of the player.
### StreamingBuffer
Payload = `SAAudioAvailabilityRange`
Changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information.
For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
### AudioDownloading
Payload = `Double`
Changes in the progress of downloading audio in the background. This does not correspond to progress in streaming downloads, look at StreamingBuffer for streaming progress.
+1 -1
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
+1 -1
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
+1 -1
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
+1 -1
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
+1 -1
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
+13 -3
View File
@@ -56,12 +56,22 @@ extension LockScreenViewProtocol {
nowPlayingInfo[MPMediaItemPropertyPodcastTitle] = title
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 //because default is 1.0. If we pause audio then it keeps ticking
nowPlayingInfo[MPMediaItemPropertyReleaseDate] = Date(timeIntervalSince1970: TimeInterval(releaseDate))
nowPlayingInfo[MPMediaItemPropertyArtwork] =
MPMediaItemArtwork(boundsSize: info.artwork.size) { size in
return info.artwork
if let artwork = info.artwork {
nowPlayingInfo[MPMediaItemPropertyArtwork] =
MPMediaItemArtwork(boundsSize: artwork.size) { size in
return artwork
}
} else {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: UIImage().size) { size in
return UIImage()
}
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
+12 -6
View File
@@ -45,8 +45,9 @@ protocol AudioDataManagable {
func deleteStream(withRemoteURL url: AudioURL)
func getPersistedUrl(withRemoteURL url: AudioURL) -> URL?
func startDownload(withRemoteURL url: AudioURL)
func deleteDownload(withRemoteURL url: AudioURL)
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL) -> ())
func cancelDownload(withRemoteURL url: AudioURL)
func deleteDownload(withLocalURL url: URL)
}
class AudioDataManager: AudioDataManagable {
@@ -152,11 +153,12 @@ extension AudioDataManager {
return FileStorage.Audio.locate(url.key)
}
func startDownload(withRemoteURL url: AudioURL) {
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL) -> ()) {
let key = url.key
if FileStorage.Audio.isStored(key) {
if let savedUrl = FileStorage.Audio.locate(key), FileStorage.Audio.isStored(key) {
globalDownloadProgressCallback(key, 1.0)
completion(savedUrl)
return
}
@@ -171,13 +173,17 @@ extension AudioDataManager {
return
}
downloadWorker.start(withID: key, withRemoteUrl: url, withResumeData: nil)
downloadWorker.start(withID: key, withRemoteUrl: url, completion: completion)
}
func deleteDownload(withRemoteURL url: AudioURL) {
func cancelDownload(withRemoteURL url: AudioURL) {
downloadWorker.stop(withID: url.key, callback: nil)
FileStorage.Audio.delete(url.key)
}
func deleteDownload(withLocalURL url: URL) {
FileStorage.delete(url)
}
}
// MARK:- Listeners
@@ -33,7 +33,7 @@ protocol AudioDataDownloadable: AnyObject {
func getProgressOfDownload(withID id: ID) -> Double?
func start(withID id: ID, withRemoteUrl remoteUrl: URL, withResumeData data: Data?)
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ())
func stop(withID id: ID, callback: ((_ dataSoFar: Data?, _ totalBytesExpected: Int64?) -> ())?)
func pauseAllActive() //Because of streaming
func resumeAllActive() //Because of streaming
@@ -85,31 +85,36 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
return activeDownloads.filter { $0.info.id == id }.first?.progress
}
func start(withID id: ID, withRemoteUrl remoteUrl: URL, withResumeData data: Data? = nil) {
Log.info("paramID: \(id) activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ()) {
Log.info("startExternal paramID: \(id) activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
let temp = activeDownloads.filter { $0.info.id == id }.count
guard temp == 0 else {
return
}
let rank = Date.getUTC()
let info = queuedDownloads.updatePreservingOldCompletionHandlers(withID: id, withRemoteUrl: remoteUrl, completion: completion)
guard numberOfActive < MAX_CONCURRENT_DOWNLOADS else {
queuedDownloads.update(with: DownloadInfo(id: id, remoteUrl: remoteUrl, rank: rank))
start(withInfo: info)
}
fileprivate func start(withInfo info: DownloadInfo) {
Log.info("paramID: \(info.id) activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
let temp = activeDownloads.filter { $0.info.id == info.id }.count
guard temp == 0 else {
return
}
var task: URLSessionDownloadTask
if let resumeData = data {
task = session.downloadTask(withResumeData: resumeData)
} else {
task = session.downloadTask(with: remoteUrl)
guard numberOfActive < MAX_CONCURRENT_DOWNLOADS else {
queuedDownloads.updatePreservingOldCompletionHandlers(withID: info.id, withRemoteUrl: info.remoteUrl)
return
}
task.taskDescription = id
queuedDownloads.remove(info)
let activeTask = ActiveDownload(info: DownloadInfo(id: id, remoteUrl: remoteUrl, rank: rank), task: task)
let task: URLSessionDownloadTask = session.downloadTask(with: info.remoteUrl)
task.taskDescription = info.id
let activeTask = ActiveDownload(info: info, task: task)
activeDownloads.append(activeTask)
activeTask.task.resume()
@@ -145,6 +150,7 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
}
}
queuedDownloads.remove(withMatchingId: id)
callback?(nil, nil)
}
}
@@ -189,10 +195,15 @@ extension AudioDownloadWorker: URLSessionDownloadDelegate {
}
completionHandler(task.info.id, nil)
for handler in task.info.completionHandlers {
handler(destinationUrl)
}
activeDownloads = activeDownloads.filter { $0 != task }
if let queued = queuedDownloads.popHighestRanked() {
start(withID: queued.id, withRemoteUrl: queued.remoteUrl)
start(withInfo: queued)
}
}
@@ -256,9 +267,18 @@ extension AudioDownloadWorker {
// MARK:- Helper Classes
extension AudioDownloadWorker {
fileprivate struct DownloadInfo: Hashable {
static func == (lhs: AudioDownloadWorker.DownloadInfo, rhs: AudioDownloadWorker.DownloadInfo) -> Bool {
return lhs.id == rhs.id && lhs.remoteUrl == rhs.remoteUrl
}
var hashValue: Int {
return id.hashValue ^ remoteUrl.hashValue
}
let id: ID
let remoteUrl: URL
let rank: Int
var completionHandlers: [(URL) -> ()]
}
private class ActiveDownload: Hashable {
@@ -298,6 +318,47 @@ extension Set where Element == AudioDownloadWorker.DownloadInfo {
return ret
}
mutating func updatePreservingOldCompletionHandlers(withID id: ID, withRemoteUrl remoteUrl: URL, completion: ((URL) -> ())? = nil) -> AudioDownloadWorker.DownloadInfo {
let rank = Date.getUTC()
let tempHandlers: [(URL) -> ()] = completion != nil ? [completion!] : []
var newInfo = AudioDownloadWorker.DownloadInfo.init(id: id, remoteUrl: remoteUrl, rank: rank, completionHandlers: tempHandlers)
if let previous = self.update(with: newInfo) {
let prevHandlers = previous.completionHandlers
let newHandlers = prevHandlers + tempHandlers
newInfo = AudioDownloadWorker.DownloadInfo.init(id: id, remoteUrl: remoteUrl, rank: rank, completionHandlers: newHandlers)
self.update(with: newInfo)
}
return newInfo
}
mutating func remove(withMatchingId id: ID) {
var toRemove: AudioDownloadWorker.DownloadInfo? = nil
var matchCount = 0
for item in self.enumerated() {
if item.element.id == id {
toRemove = item.element
matchCount += 1
}
}
guard matchCount <= 1 else {
Log.error("Found \(matchCount) matches of queued info with the same id of: \(id), this should have never happened.")
return
}
if let removeInfo = toRemove {
self.remove(removeInfo)
}
}
}
extension String {
+3 -3
View File
@@ -36,19 +36,19 @@ struct FileStorage {
Note: It is not guaranteed that the file actually exists.
*/
private static func getUrl(givenAName name: NameFile, inDirectory dir: FileManager.SearchPathDirectory) -> URL {
static func getUrl(givenAName name: NameFile, inDirectory dir: FileManager.SearchPathDirectory) -> URL {
let directoryPath = NSSearchPathForDirectoriesInDomains(dir, .userDomainMask, true)[0] as String
let url = URL(fileURLWithPath: directoryPath)
return url.appendingPathComponent(name)
}
private static func isStored(_ url: URL) -> Bool{
static func isStored(_ url: URL) -> Bool{
// https://stackoverflow.com/questions/42897844/swift-3-0-filemanager-fileexistsatpath-always-return-false
// When determining if a file exists, we must use .path not .absolute string!
return FileManager.default.fileExists(atPath: url.path)
}
private static func delete(_ url: URL) {
static func delete(_ url: URL) {
if !isStored(url) {
return
}
+8 -2
View File
@@ -26,15 +26,21 @@
import Foundation
import UIKit
/**
UTC corresponds to epoch time (number of seconds that have elapsed since January 1, 1970, midnight UTC/GMT). https://www.epochconverter.com/ is a useful site to convert to human readable format.
*/
public typealias UTC = Int
/**
Use to set what will be displayed in the lockscreen.
*/
public struct SALockScreenInfo {
var title: String
var artist: String
var artwork: UIImage
var artwork: UIImage?
var releaseDate: UTC
public init(title: String, artist: String, artwork: UIImage, releaseDate: UTC) {
public init(title: String, artist: String, artwork: UIImage?, releaseDate: UTC) {
self.title = title
self.artist = artist
self.artwork = artwork
+2 -25
View File
@@ -94,7 +94,7 @@ public class SAPlayer {
}
}
//MARK: - Player Controls
//MARK: - External Player Controls
extension SAPlayer {
public func togglePlayAndPause() {
presenter.handleTogglePlayingAndPausing()
@@ -126,31 +126,8 @@ extension SAPlayer {
}
}
extension SAPlayer {
public struct Downloader {
public static func downloadAudio(withRemoteUrl url: URL) {
SAPlayer.shared.addUrlToMapping(url: url)
AudioDataManager.shared.startDownload(withRemoteURL: url)
}
public static func cancelDownload(withRemoteUrl url: URL) {
AudioDataManager.shared.deleteDownload(withRemoteURL: url)
}
public static func deleteDownload(withRemoteUrl url: URL) {
AudioDataManager.shared.deleteDownload(withRemoteURL: url)
}
public static func isDownloaded(withRemoteUrl url: URL) -> Bool {
return AudioDataManager.shared.getPersistedUrl(withRemoteURL: url) != nil
}
public static func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
AudioDataManager.shared.setBackgroundCompletionHandler(completionHandler)
}
}
}
//MARK: - Internal implementation of delegate
extension SAPlayer: SAPlayerDelegate {
func startAudioDownloaded(withSavedUrl url: AudioURL) {
player?.pause()
+102
View File
@@ -0,0 +1,102 @@
//
// SAPlayerDownloader.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-02-25.
// Copyright © 2019 Tanha Kabir, Jon Mercer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
extension SAPlayer {
/**
Actions relating to downloading remote audio to the device for offline playback.
- Note: All saved urls generated from downloaded audio corresponds to a specific remote url. Thus, can be queryed if original remote url is known.
- Important: Please ensure that you have passed in the background download completion handler in the AppDelegate with `setBackgroundCompletionHandler` to allow for downloading audio while app is in the background.
*/
public struct Downloader {
/**
Download audio from a remote url. Will save the audio on the device for playback later.
Save the saved url of the downloaded audio for future playback or query for the saved url with the same remote url in the future.
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter url: The remote url to download audio from.
- Parameter completion: Completion handler that will return once the download is successful and complete.
- Parameter savedUrl: The url of where the audio was saved locally on the device. Will receive once download has completed.
*/
public static func downloadAudio(withRemoteUrl url: URL, completion: @escaping (_ savedUrl: URL) -> ()) {
SAPlayer.shared.addUrlToMapping(url: url)
AudioDataManager.shared.startDownload(withRemoteURL: url, completion: completion)
}
/**
Cancel downloading audio from a specific remote url if actively downloading. If download has not started yet, it will remove from the list of future downloads queued.
- Parameter url: The remote url corresponding to the active download you want to cancel.
*/
public static func cancelDownload(withRemoteUrl url: URL) {
AudioDataManager.shared.cancelDownload(withRemoteURL: url)
}
/**
Delete downloaded audio file from device at url.
- Note: This will delete any file saved on device at the local url. This, however, is intended to use for audio files.
- Parameter url: The url of the audio to delete from the device.
*/
public static func deleteDownloaded(withSavedUrl url: URL) {
AudioDataManager.shared.deleteDownload(withLocalURL: url)
}
/**
Check if audio at remote url is downloaded on device.
- Parameter url: The remote url corresponding to the audio file you want to see if downloaded.
- Returns: Whether of not file at remote url is downloaded on device.
*/
public static func isDownloaded(withRemoteUrl url: URL) -> Bool {
return AudioDataManager.shared.getPersistedUrl(withRemoteURL: url) != nil
}
/**
Get url of audio file downloaded from remote url onto on device if it exists.
- Parameter url: The remote url corresponding to the audio file you want the device url of.
- Returns: Url of audio file on device if it exists.
*/
public static func getSavedUrl(forRemoteUrl url: URL) -> URL? {
return AudioDataManager.shared.getPersistedUrl(withRemoteURL: url)
}
/**
Pass along the completion handler from `AppDelegate` to ensure downloading continues while app is in background.
- Parameter completionHandler: The completion hander from `AppDelegate` to use for app in the background downloads.
*/
public static func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
AudioDataManager.shared.setBackgroundCompletionHandler(completionHandler)
}
}
}
+6 -6
View File
@@ -40,7 +40,7 @@ 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 fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in time.
- Parameter url: The corresponding remote URL for the updated playing time.
@@ -72,7 +72,7 @@ extension SAPlayer {
/**
Subscribe to updates to changes in duration of the current audio initialized.
Note: It's recommended to have a weak reference to a class that uses this fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated duration.
@@ -104,7 +104,7 @@ 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 fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated duration.
@@ -129,14 +129,14 @@ extension SAPlayer {
}
/**
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.
Updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at `SAAudioAvailabilityRange` for more information.
*/
public struct StreamingBuffer {
/**
Subscribe to updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information. For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
Note: It's recommended to have a weak reference to a class that uses this fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated streaming progress.
@@ -168,7 +168,7 @@ extension SAPlayer {
/**
Subscribe to updates to changes in the progress of downloading audio. This does not correspond to progress in streaming downloads, look at StreamingBuffer for streaming progress.
Note: It's recommended to have a weak reference to a class that uses this fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated download progress.
+7 -1
View File
@@ -33,7 +33,13 @@ extension Date {
*/
static func getUTC64() -> UInt {
//"On 32-bit platforms, UInt is the same size as UInt32, and on 64-bit platforms, UInt is the same size as UInt64."
return UInt(Date().timeIntervalSince1970.bitPattern)
if #available(iOS 11.0, *) {
return UInt(Date().timeIntervalSince1970.bitPattern)
} else {
let time = Date().timeIntervalSince1970.bitPattern & 0xFFFFFFFF;
return UInt(time)
}
}
/**
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '0.1.0'
s.version = '1.0.3'
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.