Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52ea5a21b1 | |||
| a83c2f702f | |||
| c2cab7b272 | |||
| 8644bf24fb | |||
| 69a979cb98 | |||
| 6ba43e70ea | |||
| 6f19009000 | |||
| 64677ad6ce | |||
| 3894309706 | |||
| e44f16258f | |||
| 1e3cf35b7b | |||
| 4bfb3f1774 | |||
| e056336955 | |||
| 64d2959a27 | |||
| eb1675d4fd | |||
| ca7e48cbe7 | |||
| 653f2817bc | |||
| edff806647 | |||
| c47d623118 | |||
| b270cf86ab | |||
| 5c2fd7dc97 | |||
| d21ef34392 | |||
| e6d54b0c33 | |||
| 7a1e5bca74 | |||
| 1996812c90 | |||
| 6e1f8f12d4 | |||
| 625e1ab169 | |||
| 52c33518ad | |||
| 3f6fc327ff | |||
| e3e4e4dd46 | |||
| b60e567a83 | |||
| 17e0ee5dd8 |
+4
@@ -48,6 +48,7 @@
|
||||
A470FE0925F9ADF800F135FF /* DownloadProgressDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */; };
|
||||
A470FE1C25F9AEB900F135FF /* AudioQueueDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */; };
|
||||
A470FE2125F9AF1400F135FF /* AudioQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE2025F9AF1400F135FF /* AudioQueue.swift */; };
|
||||
A4827771262A216C00B6918A /* StreamingDownloadDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */; };
|
||||
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */; };
|
||||
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */; };
|
||||
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */; };
|
||||
@@ -135,6 +136,7 @@
|
||||
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadProgressDirector.swift; sourceTree = "<group>"; };
|
||||
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioQueueDirector.swift; sourceTree = "<group>"; };
|
||||
A470FE2025F9AF1400F135FF /* AudioQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioQueue.swift; sourceTree = "<group>"; };
|
||||
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingDownloadDirector.swift; sourceTree = "<group>"; };
|
||||
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerDownloader.swift; sourceTree = "<group>"; };
|
||||
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SALockScreenInfo.swift; sourceTree = "<group>"; };
|
||||
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerUpdateSubscription.swift; sourceTree = "<group>"; };
|
||||
@@ -378,6 +380,7 @@
|
||||
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */,
|
||||
A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */,
|
||||
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */,
|
||||
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */,
|
||||
);
|
||||
path = Directors;
|
||||
sourceTree = "<group>";
|
||||
@@ -553,6 +556,7 @@
|
||||
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */,
|
||||
A4681FD5220113BD0018AB51 /* AudioParserErrors.swift in Sources */,
|
||||
A4681FC9220113920018AB51 /* LockScreenViewProtocol.swift in Sources */,
|
||||
A4827771262A216C00B6918A /* StreamingDownloadDirector.swift in Sources */,
|
||||
A4681FD6220113BF0018AB51 /* AudioThrottler.swift in Sources */,
|
||||
A4681FCC2201139B0018AB51 /* AudioDiskEngine.swift in Sources */,
|
||||
A4681FDE220113DE0018AB51 /* Date.swift in Sources */,
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
<rect key="frame" x="16" y="60" width="343" height="32"/>
|
||||
<segments>
|
||||
<segment title="Soundbite"/>
|
||||
<segment title="Acquired"/>
|
||||
<segment title="Y Combinator"/>
|
||||
<segment title="Podcast"/>
|
||||
<segment title="Radio"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="audioSelected:" destination="vXZ-lx-hvc" eventType="valueChanged" id="oYE-yq-348"/>
|
||||
|
||||
@@ -7,24 +7,25 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudioPlayer
|
||||
|
||||
struct AudioInfo: Hashable {
|
||||
var index: Int = 0
|
||||
|
||||
var urls: [URL] = [URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!,
|
||||
URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!,
|
||||
URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&sd=1&u=1549423185")!]
|
||||
URL(string: "https://ice6.somafm.com/groovesalad-256-mp3")!]
|
||||
|
||||
var url: URL {
|
||||
switch index {
|
||||
case 0:
|
||||
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
|
||||
return urls[0]
|
||||
case 1:
|
||||
return URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!
|
||||
return urls[1]
|
||||
case 2:
|
||||
return URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&sd=1&u=1549423185")!
|
||||
return urls[2]
|
||||
default:
|
||||
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
|
||||
return urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +34,9 @@ struct AudioInfo: Hashable {
|
||||
case 0:
|
||||
return "Soundbite"
|
||||
case 1:
|
||||
return "Acquired"
|
||||
return "Podcast"
|
||||
case 2:
|
||||
return "Y Combinator"
|
||||
return "Radio"
|
||||
default:
|
||||
return "Soundbite"
|
||||
}
|
||||
@@ -44,6 +45,12 @@ struct AudioInfo: Hashable {
|
||||
let artist: String = "SwiftAudioPlayer Sample App"
|
||||
let releaseDate: Int = 1550790640
|
||||
|
||||
var lockscreenInfo: SALockScreenInfo {
|
||||
get {
|
||||
return SALockScreenInfo(title: self.title, artist: self.artist, artwork: nil, releaseDate: self.releaseDate)
|
||||
}
|
||||
}
|
||||
|
||||
var savedUrl: URL? {
|
||||
get {
|
||||
return savedUrls[index]
|
||||
|
||||
@@ -295,7 +295,7 @@ class ViewController: UIViewController {
|
||||
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
|
||||
self.selectedAudio.addSavedUrl(url)
|
||||
|
||||
SAPlayer.shared.startSavedAudio(withSavedUrl: url)
|
||||
SAPlayer.shared.startSavedAudio(withSavedUrl: url, mediaInfo: self.selectedAudio.lockscreenInfo)
|
||||
self.lastPlayedAudioIndex = self.selectedAudio.index
|
||||
}
|
||||
})
|
||||
@@ -311,7 +311,12 @@ class ViewController: UIViewController {
|
||||
|
||||
@IBAction func streamTouched(_ sender: Any) {
|
||||
if !isStreaming {
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
|
||||
if selectedAudio.index == 2 { // radio
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, bitrate: .low, mediaInfo: selectedAudio.lockscreenInfo)
|
||||
} else {
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, mediaInfo: selectedAudio.lockscreenInfo)
|
||||
}
|
||||
|
||||
lastPlayedAudioIndex = selectedAudio.index
|
||||
streamButton.setTitle("Cancel streaming", for: .normal)
|
||||
downloadButton.isEnabled = false
|
||||
|
||||
@@ -88,7 +88,7 @@ override func viewDidLoad() {
|
||||
}
|
||||
}
|
||||
```
|
||||
Look at the [Updates](#SAPlayer.Updates) section to see usage details and other updates to follow.
|
||||
Look at the [Updates](#saplayerupdates) section to see usage details and other updates to follow.
|
||||
|
||||
|
||||
For realtime audio manipulations, [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes are used. For example to adjust the reverb through a slider in the UI:
|
||||
@@ -113,6 +113,7 @@ For a more detailed explanation on usage, look at the [Realtime Audio Manipulati
|
||||
|
||||
For more details and specifics look at the [API documentation](#api-in-detail) below.
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
### Issues
|
||||
@@ -146,9 +147,9 @@ Known supported file types are `.mp3` and `.wav`.
|
||||
|
||||
To set up player with audio to play, use either:
|
||||
* `startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
|
||||
* `startRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
|
||||
* `startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
|
||||
|
||||
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen.
|
||||
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen. For streamed audio you can optionally set the bitrate to be `.high` or `.low`. High is more performant but won't work well for radio streams; for radio streams you should use low. The default bitrate if you don't set it is `.high`.
|
||||
|
||||
For streaming remote audio, subscribe to `SAPlayer.Updates.StreamingBuffer` for updates on streaming progress.
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// StreamingDownloadDirector.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 4/16/21.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
class StreamingDownloadDirector {
|
||||
static let shared = StreamingDownloadDirector()
|
||||
|
||||
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
|
||||
|
||||
private init() {}
|
||||
|
||||
func create() {}
|
||||
|
||||
func clear() {
|
||||
closures.clear()
|
||||
}
|
||||
|
||||
func attach(closure: @escaping (Key, Double) throws -> Void) -> UInt {
|
||||
return closures.attach(closure: closure)
|
||||
}
|
||||
|
||||
func detach(withID id: UInt) {
|
||||
closures.detach(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension StreamingDownloadDirector {
|
||||
func didUpdate(_ key: Key, networkStreamProgress: Double) {
|
||||
closures.broadcast(key: key, payload: networkStreamProgress)
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class AudioStreamEngine: AudioEngine {
|
||||
//Constants
|
||||
private let MAX_POLL_BUFFER_COUNT = 300 //Having one buffer in engine at a time is choppy.
|
||||
private let MIN_BUFFERS_TO_BE_PLAYABLE = 1
|
||||
private let PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
|
||||
private var PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
|
||||
|
||||
private let queue = DispatchQueue(label: "SwiftAudioPlayer.StreamEngine", qos: .userInitiated)
|
||||
|
||||
@@ -68,6 +68,7 @@ class AudioStreamEngine: AudioEngine {
|
||||
|
||||
//Fields
|
||||
private var currentTimeOffset: TimeInterval = 0
|
||||
private var streamChangeListenerId: UInt?
|
||||
|
||||
private var numberOfBuffersScheduledInTotal = 0 {
|
||||
didSet {
|
||||
@@ -133,15 +134,31 @@ class AudioStreamEngine: AudioEngine {
|
||||
}
|
||||
}
|
||||
|
||||
init(withRemoteUrl url: AudioURL, delegate:AudioEngineDelegate?) {
|
||||
init(withRemoteUrl url: AudioURL, delegate:AudioEngineDelegate?, bitrate: SAPlayerBitrate) {
|
||||
Log.info(url)
|
||||
super.init(url: url, delegate: delegate, engineAudioFormat: AudioEngine.defaultEngineAudioFormat)
|
||||
|
||||
switch bitrate {
|
||||
case .high:
|
||||
PCM_BUFFER_SIZE = 8192
|
||||
case .low:
|
||||
PCM_BUFFER_SIZE = 4096
|
||||
}
|
||||
|
||||
do {
|
||||
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat, withPCMBufferSize: PCM_BUFFER_SIZE)
|
||||
} catch {
|
||||
delegate?.didError()
|
||||
}
|
||||
|
||||
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (key, 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
|
||||
}
|
||||
|
||||
|
||||
let timeInterval = 1 / (converter.engineAudioFormat.sampleRate / Double(PCM_BUFFER_SIZE))
|
||||
|
||||
@@ -149,22 +166,36 @@ class AudioStreamEngine: AudioEngine {
|
||||
guard let self = self else { return }
|
||||
guard self.playingStatus != .ended else { return }
|
||||
|
||||
self.pollForNextBufferRecursive()
|
||||
self.updateNetworkBufferRange()
|
||||
self.updateNeedle()
|
||||
self.updateIsPlaying()
|
||||
self.updateDuration()
|
||||
self.repeatedUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let id = streamChangeListenerId {
|
||||
StreamingDownloadDirector.shared.detach(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
private func repeatedUpdates() {
|
||||
self.pollForNextBuffer()
|
||||
self.updateNetworkBufferRange() // thread issues if I try to update buffer status in streaming listener
|
||||
self.updateNeedle()
|
||||
self.updateIsPlaying()
|
||||
self.updateDuration()
|
||||
}
|
||||
|
||||
//MARK:- Timer loop
|
||||
|
||||
//Called when
|
||||
//1. First time audio is finally parsed
|
||||
//2. When we run to the end of the network buffer and we're waiting again
|
||||
private func pollForNextBufferRecursive() {
|
||||
private func pollForNextBuffer() {
|
||||
guard shouldPollForNextBuffer else { return }
|
||||
|
||||
pollForNextBufferRecursive()
|
||||
}
|
||||
|
||||
private func pollForNextBufferRecursive() {
|
||||
do {
|
||||
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer()
|
||||
numberOfBuffersScheduledFromPoll += 1
|
||||
@@ -174,7 +205,7 @@ class AudioStreamEngine: AudioEngine {
|
||||
queue.async { [weak self] in
|
||||
if #available(iOS 11.0, *) {
|
||||
// to make sure the pcm buffers are properly free'd from memory we need to nil them after the player has used them
|
||||
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataRendered, completionHandler: { (_) in
|
||||
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataConsumed, completionHandler: { (_) in
|
||||
nextScheduledBuffer = nil
|
||||
self?.numberOfBuffersScheduledInTotal -= 1
|
||||
self?.pollForNextBufferRecursive()
|
||||
@@ -286,6 +317,16 @@ class AudioStreamEngine: AudioEngine {
|
||||
super.pause()
|
||||
}
|
||||
|
||||
override func play() {
|
||||
queue.async { [weak self] in
|
||||
self?.playHelperDispatchQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private func playHelperDispatchQueue() {
|
||||
super.play()
|
||||
}
|
||||
|
||||
override func invalidate() {
|
||||
super.invalidate()
|
||||
converter.invalidate()
|
||||
|
||||
@@ -27,74 +27,30 @@ import Foundation
|
||||
|
||||
protocol AudioThrottleDelegate: AnyObject {
|
||||
func didUpdate(totalBytesExpected bytes: Int64)
|
||||
func didUpdate(networkStreamProgress progress: Double)
|
||||
func shouldProcess(networkData data: Data)
|
||||
}
|
||||
|
||||
protocol AudioThrottleable {
|
||||
init(withRemoteUrl url: AudioURL, withDelegate delegate: AudioThrottleDelegate)
|
||||
func tellAudioFormatFound()
|
||||
func tellByteOffset(offset: UInt64)
|
||||
func pullNextDataPacket(_ callback: @escaping (Data?) -> ())
|
||||
func tellSeek(offset: UInt64)
|
||||
func tellBytesPerAudioPacket(count: UInt64)
|
||||
func pollRangeOfBytesAvailable() -> (UInt64, UInt64)
|
||||
func invalidate()
|
||||
}
|
||||
|
||||
class AudioThrottler: AudioThrottleable {
|
||||
private class NetworkDataWrapper: NSObject {
|
||||
let startOffset: UInt
|
||||
var data: Data
|
||||
var alreadySent: Bool
|
||||
var next: NetworkDataWrapper?
|
||||
|
||||
var byteCount: UInt {
|
||||
return UInt(data.count)
|
||||
}
|
||||
|
||||
var endOffset: UInt {
|
||||
return startOffset + UInt(data.count) - 1
|
||||
}
|
||||
|
||||
init(startingOffset: UInt, data: Data) {
|
||||
self.startOffset = startingOffset
|
||||
self.data = data
|
||||
self.alreadySent = false
|
||||
}
|
||||
|
||||
func containsOffset(_ offset: UInt) -> Bool {
|
||||
return startOffset <= offset && offset <= endOffset
|
||||
}
|
||||
|
||||
func isNextSent() -> Bool {
|
||||
return next?.alreadySent ?? false
|
||||
}
|
||||
|
||||
//FIXME: what is the offset was at the edge of the split? We will have empty data
|
||||
func splitToRight(atOffset offset: UInt) -> NetworkDataWrapper {
|
||||
let splitPoint:Int = Int(offset - startOffset)
|
||||
let leftData = data.subdata(in: 0..<splitPoint)
|
||||
let rightData = data.subdata(in: splitPoint..<data.count)
|
||||
|
||||
data = leftData
|
||||
|
||||
let rightWrapper:NetworkDataWrapper = NetworkDataWrapper(startingOffset: offset, data: rightData)
|
||||
rightWrapper.next = next
|
||||
next = rightWrapper
|
||||
|
||||
return rightWrapper
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "startOffset:\(startOffset), endOffset:\(endOffset), dataCount:\(data.count), sent:\(alreadySent), next:\(next != nil ?"hasNext":"noNext")"
|
||||
}
|
||||
}
|
||||
private let queue = DispatchQueue(label: "SwiftAudioPlayer.Throttler", qos: .userInitiated)
|
||||
|
||||
//Init
|
||||
let url: AudioURL
|
||||
weak var delegate: AudioThrottleDelegate?
|
||||
|
||||
private var networkData: [NetworkDataWrapper] = []
|
||||
private var networkData: [Data] = [] {
|
||||
didSet {
|
||||
// Log.test("NETWORK DATA \(networkData.count)")
|
||||
}
|
||||
}
|
||||
private var lastSentDataPacketIndex = -1
|
||||
|
||||
var shouldThrottle = false
|
||||
var byteOffsetBecauseOfSeek: UInt = 0
|
||||
|
||||
@@ -116,131 +72,103 @@ class AudioThrottler: AudioThrottleable {
|
||||
AudioDataManager.shared.startStream(withRemoteURL: url) { [weak self] (pto: StreamProgressPTO) in
|
||||
guard let self = self else {return}
|
||||
Log.debug("received stream data of size \(pto.getData().count) and progress: \(pto.getProgress())")
|
||||
self.delegate?.didUpdate(networkStreamProgress: pto.getProgress())
|
||||
|
||||
|
||||
if let totalBytesExpected = pto.getTotalBytesExpected() {
|
||||
self.totalBytesExpected = totalBytesExpected
|
||||
}
|
||||
|
||||
let lastItem = self.networkData.last
|
||||
let startoffset = lastItem == nil ? self.byteOffsetBecauseOfSeek : lastItem!.endOffset + 1
|
||||
let wrappedNetworkData = NetworkDataWrapper(startingOffset: startoffset, data: pto.getData())
|
||||
lastItem?.next = wrappedNetworkData
|
||||
self.networkData.append(wrappedNetworkData)
|
||||
|
||||
if !self.shouldThrottle {
|
||||
Log.debug("sending up packet from stream untrottled at start: \(wrappedNetworkData.startOffset)")
|
||||
//NOTE: the order here matters.
|
||||
//We have to set to true before sending up to be processed because
|
||||
//tellByteOffset() is ran in a separate thread than this one
|
||||
//We got in a state where 10% of the time an episode will keep polling because
|
||||
//the first 30 buffers have not been filled
|
||||
wrappedNetworkData.alreadySent = true
|
||||
delegate.shouldProcess(networkData: wrappedNetworkData.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tellAudioFormatFound() {
|
||||
shouldThrottle = true //the above layer has enough info that we can throttle
|
||||
}
|
||||
|
||||
func tellBytesPerAudioPacket(count: UInt64) {
|
||||
if count > largestPollingOffsetDifference {
|
||||
largestPollingOffsetDifference = count
|
||||
}
|
||||
}
|
||||
|
||||
func tellByteOffset(offset: UInt64) {
|
||||
Log.debug("offset \(offset)")
|
||||
|
||||
for wrappedNetworkData in networkData {
|
||||
if wrappedNetworkData.containsOffset(UInt(offset)) {
|
||||
Log.debug("offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset) is next sent: \(wrappedNetworkData.isNextSent())")
|
||||
|
||||
if wrappedNetworkData.alreadySent {
|
||||
Log.debug("already sent offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
|
||||
|
||||
var bytesSent: UInt = 0
|
||||
var current = wrappedNetworkData
|
||||
|
||||
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
|
||||
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
|
||||
while bytesSent < largestPollingOffsetDifference {
|
||||
if let next = current.next {
|
||||
if !next.alreadySent {
|
||||
Log.info("Sending next network packet with range: \(next.startOffset) to \(next.endOffset), have sent \(bytesSent) bytes so far from \(largestPollingOffsetDifference) bytes")
|
||||
next.alreadySent = true
|
||||
delegate?.shouldProcess(networkData: next.data)
|
||||
}
|
||||
bytesSent += next.byteCount
|
||||
current = next
|
||||
} else {
|
||||
Log.debug("next package doesn't exist, bytes sent so far: \(bytesSent)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
|
||||
wrappedNetworkData.alreadySent = true
|
||||
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
|
||||
return
|
||||
self.queue.async { [weak self] in
|
||||
self?.networkData.append(pto.getData())
|
||||
StreamingDownloadDirector.shared.didUpdate(url.key, networkStreamProgress: pto.getProgress())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func tellSeek(offset: UInt64) {
|
||||
Log.info("seek with offset: \(offset)")
|
||||
|
||||
self.queue.async { [weak self] in
|
||||
self?.seekQueueHelper(offset)
|
||||
}
|
||||
}
|
||||
|
||||
func seekQueueHelper(_ offset: UInt64) {
|
||||
let offsetToFind = Int(offset) - Int(byteOffsetBecauseOfSeek)
|
||||
|
||||
var shouldStartNewStream: Bool = false
|
||||
|
||||
// if we have no data start a new stream after seek
|
||||
if networkData.count == 0 {
|
||||
shouldStartNewStream = true
|
||||
}
|
||||
|
||||
// if what we're looking for is outside of available data, start a new stream
|
||||
if offset < byteOffsetBecauseOfSeek || offsetToFind > networkData.sum {
|
||||
shouldStartNewStream = true
|
||||
}
|
||||
|
||||
// we should have the data within our cache. find it and save the index for the next pull
|
||||
if let indexOfDataContainingOffset = networkData.getIndexContainingByteOffset(offsetToFind) {
|
||||
lastSentDataPacketIndex = indexOfDataContainingOffset - 1
|
||||
}
|
||||
|
||||
if shouldStartNewStream {
|
||||
byteOffsetBecauseOfSeek = UInt(offset)
|
||||
lastSentDataPacketIndex = -1
|
||||
AudioDataManager.shared.seekStream(withRemoteURL: url, toByteOffset: offset)
|
||||
|
||||
networkData = []
|
||||
return
|
||||
}
|
||||
|
||||
if let finalOffset = networkData.last?.endOffset, let firstOffset = networkData.first?.startOffset {
|
||||
if offset < firstOffset || offset > finalOffset {
|
||||
byteOffsetBecauseOfSeek = UInt(offset)
|
||||
AudioDataManager.shared.seekStream(withRemoteURL: url, toByteOffset: offset)
|
||||
|
||||
networkData = []
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (i, d) in networkData.enumerated() {
|
||||
if offset > d.endOffset {
|
||||
d.alreadySent = false
|
||||
continue
|
||||
}
|
||||
|
||||
if d.containsOffset(UInt(offset)) {
|
||||
let wrappedData = d.splitToRight(atOffset: UInt(offset))
|
||||
networkData.insert(wrappedData, at: i+1)
|
||||
|
||||
d.alreadySent = false
|
||||
wrappedData.alreadySent = true
|
||||
Log.info("\(d) ::: \(wrappedData)")
|
||||
|
||||
delegate?.shouldProcess(networkData: wrappedData.data)
|
||||
return
|
||||
}
|
||||
}
|
||||
Log.error("83672 Should not get here")
|
||||
}
|
||||
|
||||
func pollRangeOfBytesAvailable() -> (UInt64, UInt64) {
|
||||
let start = networkData.first?.startOffset ?? 0
|
||||
let end = networkData.last?.endOffset ?? 0
|
||||
let start = byteOffsetBecauseOfSeek
|
||||
let end = networkData.sum + Int(byteOffsetBecauseOfSeek)
|
||||
|
||||
return (UInt64(start), UInt64(end))
|
||||
}
|
||||
|
||||
func pullNextDataPacket(_ callback: @escaping (Data?) -> ()) {
|
||||
queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard self.lastSentDataPacketIndex < self.networkData.count - 1 else {
|
||||
callback(nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.lastSentDataPacketIndex += 1
|
||||
|
||||
callback(self.networkData[self.lastSentDataPacketIndex])
|
||||
}
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
AudioDataManager.shared.deleteStream(withRemoteURL: url)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Data {
|
||||
var sum: Int {
|
||||
get {
|
||||
return self.reduce(0) { $0 + $1.count }
|
||||
}
|
||||
}
|
||||
|
||||
func getIndexContainingByteOffset(_ offset: Int) -> Int? {
|
||||
var dataCount = 0
|
||||
|
||||
for (i, data) in self.enumerated() {
|
||||
if offset >= dataCount && offset <= dataCount + data.count {
|
||||
return i
|
||||
}
|
||||
|
||||
dataCount += data.count
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class AudioConverter: AudioConvertable {
|
||||
self.pcmBufferSize = size
|
||||
|
||||
do {
|
||||
parser = try AudioParser(withRemoteUrl: url, parsedFileAudioFormatCallback: {
|
||||
parser = try AudioParser(withRemoteUrl: url, bufferSize: Int(size), parsedFileAudioFormatCallback: {
|
||||
[weak self] (fileAudioFormat: AVAudioFormat) in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@ import AVFoundation
|
||||
//TODO: what if user seeks beyond the data we have? What if we're done but user seeks even further than what we have
|
||||
|
||||
class AudioParser: AudioParsable {
|
||||
private var MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING = 8192 // this will be modified when we know the file format to be just enough packets to fill up 1 pcm buffer
|
||||
private var framesPerBuffer: Int = 1
|
||||
|
||||
//MARK:- For OS parser class
|
||||
var parsedAudioHeaderPacketCount: UInt64 = 0
|
||||
var parsedAudioPacketDataSize: UInt64 = 0
|
||||
@@ -61,8 +64,8 @@ class AudioParser: AudioParsable {
|
||||
public var fileAudioFormat: AVAudioFormat? {
|
||||
didSet {
|
||||
if let format = fileAudioFormat, oldValue == nil {
|
||||
MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING = framesPerBuffer/Int(format.streamDescription.pointee.mFramesPerPacket)
|
||||
parsedFileAudioFormatCallback(format)
|
||||
throttler.tellAudioFormatFound()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,16 +103,9 @@ class AudioParser: AudioParsable {
|
||||
return predictedCount
|
||||
}
|
||||
|
||||
var sumOfParsedAudioBytes:UInt32 = 0 {
|
||||
didSet {
|
||||
if let byteCount = averageBytesPerPacket {
|
||||
throttler.tellBytesPerAudioPacket(count: UInt64(byteCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sumOfParsedAudioBytes:UInt32 = 0
|
||||
var numberOfPacketsParsed:UInt32 = 0
|
||||
var audioPackets: [(AudioStreamPacketDescription?,Data)] = [] {
|
||||
var audioPackets: [(AudioStreamPacketDescription?,Data)] = [] {
|
||||
didSet {
|
||||
if let audioPacketByteSize = audioPackets.last?.0?.mDataByteSize {
|
||||
sumOfParsedAudioBytes += audioPacketByteSize
|
||||
@@ -122,6 +118,8 @@ class AudioParser: AudioParsable {
|
||||
//TODO: duration will not be accurate with WAV or AIFF
|
||||
}
|
||||
}
|
||||
private let lockQueue = DispatchQueue(label: "SwiftAudioPlayer.Parser.packets.lock")
|
||||
var lastSentAudioPacketIndex = -1
|
||||
|
||||
/**
|
||||
Audio packets varry in size. The first one parsed in a batch of audio
|
||||
@@ -148,12 +146,30 @@ class AudioParser: AudioParsable {
|
||||
return audioPackets.count == totalPredictedPacketCount
|
||||
}
|
||||
|
||||
var streamChangeListenerId: UInt?
|
||||
|
||||
init(withRemoteUrl url: AudioURL, parsedFileAudioFormatCallback: @escaping(AVAudioFormat) -> ()) throws {
|
||||
init(withRemoteUrl url: AudioURL, bufferSize: Int, parsedFileAudioFormatCallback: @escaping(AVAudioFormat) -> ()) throws {
|
||||
self.url = url
|
||||
self.framesPerBuffer = bufferSize
|
||||
self.parsedFileAudioFormatCallback = parsedFileAudioFormatCallback
|
||||
|
||||
self.throttler = AudioThrottler(withRemoteUrl: url, withDelegate: self)
|
||||
|
||||
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (key, progress) in
|
||||
guard let self = self else { return }
|
||||
guard key == url.key else { return }
|
||||
self.networkProgress = progress
|
||||
|
||||
// initially parse a bunch of packets
|
||||
self.lockQueue.sync {
|
||||
if self.fileAudioFormat == nil {
|
||||
self.processNextDataPacket()
|
||||
} else if self.audioPackets.count - self.lastSentAudioPacketIndex < self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
|
||||
//Open the stream and when we call parse data is fed into this stream
|
||||
guard AudioFileStreamOpen(context, ParserPropertyListener, ParserPacketListener, kAudioFileMP3Type, &streamID) == noErr else {
|
||||
@@ -161,33 +177,61 @@ class AudioParser: AudioParsable {
|
||||
}
|
||||
}
|
||||
|
||||
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data) {
|
||||
if let offset = getOffset(fromPacketIndex: index) {
|
||||
throttler.tellByteOffset(offset: offset)
|
||||
deinit {
|
||||
if let id = streamChangeListenerId {
|
||||
StreamingDownloadDirector.shared.detach(withID: id)
|
||||
}
|
||||
}
|
||||
|
||||
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data) {
|
||||
determineIfMoreDataNeedsToBeParsed(index: index)
|
||||
|
||||
// Check if we've reached the end of the packets. We have two scenarios:
|
||||
// 1. We've reached the end of the packet data and the file has been completely parsed
|
||||
// 2. We've reached the end of the data we currently have downloaded, but not the file
|
||||
let packetIndex = index - indexSeekOffset
|
||||
let isEndOfData = packetIndex >= audioPackets.count
|
||||
if isEndOfData {
|
||||
if isParsingComplete {
|
||||
throw ParserError.readerAskingBeyondEndOfFile
|
||||
} else {
|
||||
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(audioPackets.count), we predict \(totalPredictedPacketCount) in total")
|
||||
throw ParserError.notEnoughDataForReader
|
||||
|
||||
var exception: ParserError? = nil
|
||||
var packet: (AudioStreamPacketDescription?, Data) = (nil, Data())
|
||||
lockQueue.sync {
|
||||
if packetIndex >= self.audioPackets.count {
|
||||
if isParsingComplete {
|
||||
exception = ParserError.readerAskingBeyondEndOfFile
|
||||
return
|
||||
} else {
|
||||
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(self.audioPackets.count), we predict \(self.totalPredictedPacketCount) in total")
|
||||
exception = ParserError.notEnoughDataForReader
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lastSentAudioPacketIndex = Int(packetIndex)
|
||||
packet = audioPackets[Int(packetIndex)]
|
||||
}
|
||||
if let exception = exception {
|
||||
throw exception
|
||||
} else {
|
||||
return packet
|
||||
}
|
||||
}
|
||||
|
||||
private func determineIfMoreDataNeedsToBeParsed(index: AVAudioPacketCount) {
|
||||
lockQueue.sync {
|
||||
if index > self.audioPackets.count - self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
}
|
||||
|
||||
return audioPackets[Int(packetIndex)]
|
||||
}
|
||||
|
||||
func tellSeek(toIndex index: AVAudioPacketCount) {
|
||||
//Already within the processed audio packets. Ignore
|
||||
if indexSeekOffset <= index && index < audioPackets.count + Int(indexSeekOffset) {
|
||||
return
|
||||
var isIndexValid: Bool = true
|
||||
lockQueue.sync {
|
||||
if self.indexSeekOffset <= index && index < self.audioPackets.count + Int(self.indexSeekOffset) {
|
||||
isIndexValid = false
|
||||
}
|
||||
}
|
||||
guard isIndexValid else { return }
|
||||
|
||||
guard let byteOffset = getOffset(fromPacketIndex: index) else {
|
||||
return
|
||||
@@ -199,9 +243,12 @@ class AudioParser: AudioParsable {
|
||||
// NOTE: Order matters. Need to prevent appending to the array before we clean it. Just in case
|
||||
// then we tell the throttler to send us appropriate packet
|
||||
shouldPreventPacketFromFillingUp = true
|
||||
audioPackets = []
|
||||
lockQueue.sync {
|
||||
self.audioPackets = []
|
||||
}
|
||||
|
||||
throttler.tellSeek(offset: byteOffset)
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
|
||||
private func getOffset(fromPacketIndex index: AVAudioPacketCount) -> UInt64? {
|
||||
@@ -254,6 +301,12 @@ class AudioParser: AudioParsable {
|
||||
return Needle(TimeInterval(frame)/TimeInterval(frameCount)*duration)
|
||||
}
|
||||
|
||||
func append(description: AudioStreamPacketDescription?, data: Data) {
|
||||
lockQueue.sync {
|
||||
self.audioPackets.append((description, data))
|
||||
}
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
throttler.invalidate()
|
||||
|
||||
@@ -281,6 +334,32 @@ class AudioParser: AudioParsable {
|
||||
|
||||
}
|
||||
|
||||
private func processNextDataPacket() {
|
||||
throttler.pullNextDataPacket { [weak self] (d) in
|
||||
guard let self = self else { return }
|
||||
guard let data = d else { return }
|
||||
|
||||
self.lockQueue.sync {
|
||||
Log.debug("processing data count: \(data.count) :: already had \(self.audioPackets.count) audio packets")
|
||||
}
|
||||
self.shouldPreventPacketFromFillingUp = false
|
||||
do {
|
||||
let sID = self.streamID!
|
||||
let dataSize = data.count
|
||||
|
||||
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
|
||||
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
|
||||
guard result == noErr else {
|
||||
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
|
||||
throw ParserError.failedToParseBytes(result)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
Log.monitor(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK:- AudioThrottleDelegate
|
||||
@@ -288,27 +367,4 @@ extension AudioParser: AudioThrottleDelegate {
|
||||
func didUpdate(totalBytesExpected bytes: Int64) {
|
||||
expectedFileSizeInBytes = UInt64(bytes)
|
||||
}
|
||||
|
||||
func didUpdate(networkStreamProgress progress: Double) {
|
||||
networkProgress = progress
|
||||
}
|
||||
|
||||
func shouldProcess(networkData data: Data) {
|
||||
Log.debug("processing data count: \(data.count) :: already had \(audioPackets.count) audio packets")
|
||||
self.shouldPreventPacketFromFillingUp = false
|
||||
do {
|
||||
let sID = self.streamID!
|
||||
let dataSize = data.count
|
||||
|
||||
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
|
||||
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
|
||||
guard result == noErr else {
|
||||
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
|
||||
throw ParserError.failedToParseBytes(result)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
Log.monitor(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func parserPacket(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ pac
|
||||
let audioPacketStart = Int(audioPacketDescription.mStartOffset)
|
||||
let audioPacketSize = Int(audioPacketDescription.mDataByteSize)
|
||||
let audioPacketData = Data(bytes: streamData.advanced(by: audioPacketStart), count: audioPacketSize)
|
||||
selfAudioParser.audioPackets.append((audioPacketDescription,audioPacketData))
|
||||
selfAudioParser.append(description: audioPacketDescription, data: audioPacketData)
|
||||
}
|
||||
} else { // not compressed audio (.wav)
|
||||
Log.debug("uncompressed audio")
|
||||
@@ -75,7 +75,7 @@ func parserPacket(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ pac
|
||||
let audioPacketStart = i * bytesPerAudioPacket
|
||||
let audioPacketSize = bytesPerAudioPacket
|
||||
let audioPacketData = Data(bytes: streamData.advanced(by: audioPacketStart), count: audioPacketSize)
|
||||
selfAudioParser.audioPackets.append((nil, audioPacketData))
|
||||
selfAudioParser.append(description: nil, data: audioPacketData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-14
@@ -81,14 +81,14 @@ public class SAPlayer {
|
||||
*/
|
||||
public var volume: Float? {
|
||||
get {
|
||||
return player?.engine.mainMixerNode.volume
|
||||
return player?.playerNode.volume
|
||||
}
|
||||
|
||||
set {
|
||||
guard let value = newValue else { return }
|
||||
guard value >= 0.0 && value <= 1.0 else { return }
|
||||
|
||||
player?.engine.mainMixerNode.volume = value
|
||||
player?.playerNode.volume = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,9 +281,8 @@ public class SAPlayer {
|
||||
*/
|
||||
public static func prettifyTimestamp(_ timestamp: Double) -> String {
|
||||
let hours = Int(timestamp / 60 / 60)
|
||||
let minutes = Int((timestamp - Double(hours * 60)) / 60)
|
||||
|
||||
let secondsLeft = Int(timestamp) - (minutes * 60)
|
||||
let minutes = Int((timestamp - Double(hours * 60 * 60)) / 60)
|
||||
let secondsLeft = Int(timestamp - Double(hours * 60 * 60) - Double(minutes * 60))
|
||||
|
||||
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", secondsLeft))"
|
||||
}
|
||||
@@ -297,6 +296,14 @@ public class SAPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SAPlayerBitrate {
|
||||
/// This bitrate is good for radio streams that are passing ittle amounts of audio data at a time. This will allow the player to process the audio data in a fast enough rate to not pause or get stuck playing. This rate however ends up using more CPU and is worse for your battery-life and performance of your app.
|
||||
case low
|
||||
|
||||
/// This bitrate is good for streaming saved audio files like podcasts where most of the audio data will be received from the remote server at the beginning in a short time. This rate is more performant by using much less CPU and being better for your battery-life and app performance.
|
||||
case high // go for audio files being streamed. This is uses less CPU and
|
||||
}
|
||||
|
||||
//MARK: - External Player Controls
|
||||
extension SAPlayer {
|
||||
/**
|
||||
@@ -442,17 +449,18 @@ extension SAPlayer {
|
||||
- Note: Subscribe to `SAPlayer.Updates.StreamingBuffer` to see updates in streaming progress.
|
||||
|
||||
- Parameter withRemoteUrl: The URL of the remote audio.
|
||||
- Parameter bitrate: The bitrate of the streamed audio. By default the bitrate is set to high for streaming saved audio files. If you want to stream radios then you should use the `low` bitrate option.
|
||||
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
|
||||
*/
|
||||
public func startRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
public func startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate = .high, mediaInfo: SALockScreenInfo? = nil) {
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: bitrate)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "startRemoteAudio")
|
||||
public func initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: .high)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -495,8 +503,8 @@ extension SAPlayer: SAPlayerDelegate {
|
||||
player = AudioDiskEngine(withSavedUrl: url, delegate: presenter)
|
||||
}
|
||||
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL) {
|
||||
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter)
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate) {
|
||||
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter, bitrate: bitrate)
|
||||
}
|
||||
|
||||
func clearEngine() {
|
||||
@@ -515,12 +523,10 @@ extension SAPlayer: SAPlayerDelegate {
|
||||
private func becomeDeviceAudioPlayer() {
|
||||
do {
|
||||
if #available(iOS 11.0, *) {
|
||||
// try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, policy: .longForm, options: [])
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, policy: .longFormAudio, options: [])
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
|
||||
}
|
||||
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
|
||||
|
||||
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
|
||||
} catch {
|
||||
Log.monitor("Problem setting up AVAudioSession to play in:: \(error.localizedDescription)")
|
||||
|
||||
@@ -31,7 +31,7 @@ protocol SAPlayerDelegate: AnyObject, LockScreenViewProtocol {
|
||||
var skipBackwardSeconds: Double { get set }
|
||||
|
||||
func startAudioDownloaded(withSavedUrl url: AudioURL)
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL)
|
||||
func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate)
|
||||
func clearEngine()
|
||||
func playEngine()
|
||||
func pauseEngine()
|
||||
|
||||
@@ -24,6 +24,7 @@ extension SAPlayer {
|
||||
public struct SkipSilences {
|
||||
|
||||
static var enabled: Bool = false
|
||||
static var originalRate: Float = 1.0
|
||||
|
||||
/**
|
||||
Enable feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected. This can be called at any point of audio playback.
|
||||
@@ -35,7 +36,7 @@ extension SAPlayer {
|
||||
|
||||
Log.info("enabling skip silences feature")
|
||||
enabled = true
|
||||
let originalRate = SAPlayer.shared.rate ?? 1.0
|
||||
originalRate = SAPlayer.shared.rate ?? 1.0
|
||||
let format = engine.mainMixerNode.outputFormat(forBus: 0)
|
||||
|
||||
|
||||
@@ -58,10 +59,10 @@ extension SAPlayer {
|
||||
Log.debug("meterLevel: \(meterLevel)")
|
||||
if meterLevel < 0.6 { // below 0.6 decibels is below audible audio
|
||||
SAPlayer.shared.rate = originalRate + 0.5
|
||||
Log.test("speed up rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
Log.debug("speed up rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
} else {
|
||||
SAPlayer.shared.rate = originalRate
|
||||
Log.test("slow down rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
Log.debug("slow down rate to \(String(describing: SAPlayer.shared.rate))")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +75,10 @@ extension SAPlayer {
|
||||
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
|
||||
*/
|
||||
public static func disable() -> Bool {
|
||||
// TODO fix disabling on speed up portion and being stuck at faster speed https://github.com/tanhakabir/SwiftAudioPlayer/issues/76
|
||||
guard let engine = SAPlayer.shared.engine else { return false }
|
||||
Log.info("disabling skip silences feature")
|
||||
engine.mainMixerNode.removeTap(onBus: 0)
|
||||
SAPlayer.shared.rate = originalRate
|
||||
enabled = false
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -87,13 +87,13 @@ class SAPlayerPresenter {
|
||||
delegate?.startAudioDownloaded(withSavedUrl: url)
|
||||
}
|
||||
|
||||
func handlePlayStreamedAudio(withRemoteUrl url: URL) {
|
||||
func handlePlayStreamedAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate) {
|
||||
// Because we support queueing, we want to clear off any existing players.
|
||||
// Therefore, instantiate new player every time, destroy any existing ones.
|
||||
// This prevents a crash where an owning engine already exists.
|
||||
handleClear()
|
||||
attachForUpdates(url: url)
|
||||
delegate?.startAudioStreamed(withRemoteUrl: url)
|
||||
delegate?.startAudioStreamed(withRemoteUrl: url, bitrate: bitrate)
|
||||
}
|
||||
|
||||
func handleQueueStreamedAudio(withRemoteUrl url: URL) {
|
||||
@@ -252,7 +252,7 @@ extension SAPlayerPresenter {
|
||||
|
||||
switch nextAudioURL.0 {
|
||||
case .remote:
|
||||
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.1)
|
||||
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.1, bitrate: .high) // TODO fix to add option for low birate
|
||||
break
|
||||
case .disk:
|
||||
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.1)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioPlayer'
|
||||
s.version = '4.1.0'
|
||||
s.version = '5.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.
|
||||
@@ -26,7 +26,7 @@ SwiftAudioPlayer is a Swift based audio player that can handle streaming from a
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'tanhakabir' => 'tanhakabir.ca@gmail.com', 'JonMercer' => 'mercer.jon@gmail.com' }
|
||||
s.source = { :git => 'https://github.com/tanhakabir/SwiftAudioPlayer.git', :tag => s.version.to_s }
|
||||
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
|
||||
s.social_media_url = 'https://twitter.com/_tanhakabir'
|
||||
|
||||
s.ios.deployment_target = '10.0'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user