Compare commits

...

22 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 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
15 changed files with 143 additions and 66 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
+2
View File
@@ -135,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>"; };
@@ -255,6 +256,7 @@
children = (
15DF3E7F1B5E10B1BBE49D3E9A67C938 /* LICENSE */,
B8C829A46249957CD3056074B5CC0BBB /* README.md */,
A4883AC926CC25DE0073B8B6 /* CHANGELOG.md */,
6EC04ECC8F7CB2AF2E4E042A6A8ECFA1 /* SwiftAudioPlayer.podspec */,
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */,
);
@@ -280,8 +280,15 @@ 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)
+1 -1
View File
@@ -171,7 +171,7 @@ You can also directly access and modify the queue from `SAPlayer.shared.audioQue
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.
+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
+9
View File
@@ -273,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
+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()
+36 -34
View File
@@ -46,13 +46,11 @@ class SAPlayerPresenter {
init(delegate: SAPlayerDelegate?) {
self.delegate = delegate
delegate?.setLockScreenControls(presenter: self)
durationRef = AudioClockDirector.shared.attachToChangesInDuration(closure: { [weak self] (duration) in
guard let self = self else { throw DirectorError.closureIsDead }
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
self.delegate?.updateLockScreenPlaybackDuration(duration: duration)
self.duration = duration
self.delegate?.setLockScreenInfo(withMediaInfo: self.delegate?.mediaInfo, duration: duration)
@@ -62,7 +60,7 @@ class SAPlayerPresenter {
guard let self = self else { throw DirectorError.closureIsDead }
self.needle = needle
self.delegate?.updateLockscreenElapsedTime(needle: needle)
self.delegate?.updateLockScreenElapsedTime(needle: needle)
})
playingStatusRef = AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { [weak self] (isPlaying) in
@@ -106,11 +104,13 @@ class SAPlayerPresenter {
func handlePlaySavedAudio(withSavedUrl url: URL) {
resetCacheForNewAudio(url: url)
delegate?.setLockScreenControls(presenter: self)
delegate?.startAudioDownloaded(withSavedUrl: url)
}
func handlePlayStreamedAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate) {
resetCacheForNewAudio(url: url)
delegate?.setLockScreenControls(presenter: self)
delegate?.startAudioStreamed(withRemoteUrl: url, bitrate: bitrate)
}
@@ -156,16 +156,7 @@ class SAPlayerPresenter {
//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()
@@ -173,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
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '6.4.0'
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.