Compare commits

...

12 Commits

Author SHA1 Message Date
Tanha 8d9e9d92f4 Release 2.8.0 2020-01-04 13:24:47 -08:00
tanhakabir 03392c21e0 fix bug in notification of end of audio in PlayingStatus (#25) 2020-01-04 13:23:50 -08:00
Tanha 924170d159 Release 2.7.0 2020-01-04 02:04:35 -08:00
tanhakabir b355eb4e09 add api for buffer progress in SAAvailabilityRange (#24) 2020-01-04 02:02:56 -08:00
Tanha 1373a816a6 Release 2.6.0 2019-12-18 21:23:07 -08:00
tanhakabir 196b04a703 Rename and deprecate initialize functions (#23) 2019-12-18 21:22:28 -08:00
Tanha ac971e65a6 Release 2.5.2 2019-12-18 16:19:57 -08:00
Tanha 2c50502b28 update documentation for live streaming audio 2019-12-18 16:19:37 -08:00
Tanha c222b5a745 Release 2.5.1 2019-12-18 00:27:49 -08:00
Tanha 2e86a6503c Clean up debug logging 2019-12-18 00:27:21 -08:00
Tanha 9ebd7fa7fe Release 2.5.0 2019-12-18 00:24:11 -08:00
tanhakabir 5197a16023 Fix live streams/servers with unpredictable size at beginning of stream being playable (#21)
* Handle case when header for stream does not contain expected content

* update documentation

* fix elapsed time updating on seek in example app
2019-12-18 00:22:31 -08:00
12 changed files with 76 additions and 31 deletions
@@ -117,7 +117,6 @@ class ViewController: UIViewController {
_ = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
guard let self = self else { return }
guard self.beingSeeked == false else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
@@ -146,11 +145,9 @@ class ViewController: UIViewController {
if self.duration == 0.0 { return }
let progress = Float((buffer.totalDurationBuffered + buffer.startingBufferTimePositon) / self.duration)
self.bufferProgress.progress = Float(buffer.bufferingProgress)
self.bufferProgress.progress = progress
if progress >= 0.99 {
if buffer.bufferingProgress >= 0.99 {
self.streamButton.isEnabled = false
}
@@ -245,7 +242,7 @@ class ViewController: UIViewController {
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
self.savedUrls[self.selectedAudio] = url
SAPlayer.shared.initializeSavedAudio(withSavedUrl: url)
SAPlayer.shared.startSavedAudio(withSavedUrl: url)
}
})
streamButton.isEnabled = false
@@ -260,7 +257,7 @@ class ViewController: UIViewController {
@IBAction func streamTouched(_ sender: Any) {
if !isStreaming {
SAPlayer.shared.initializeRemoteAudio(withRemoteUrl: selectedAudio.url)
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
streamButton.setTitle("Cancel streaming", for: .normal)
downloadButton.isEnabled = false
} else {
+3 -3
View File
@@ -39,7 +39,7 @@ pod 'SwiftAudioPlayer'
To play remote audio:
```swift
let url = URL(string: "https://randomwebsite.com/audio.mp3")!
SAPlayer.shared.initializeAudio(withRemoteUrl: url)
SAPlayer.shared.startRemoteAudio(withRemoteUrl: url)
SAPlayer.shared.play()
```
@@ -121,8 +121,8 @@ Access the player and all of its fields and functions through `SAPlayer.shared`.
### Playing Audio (Basic Commands)
To set up player with audio to play, use either:
* `initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
* `initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
* `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.
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen.
+1 -1
View File
@@ -64,7 +64,7 @@ class AudioDiskEngine: AudioEngine {
audioSampleRate = Float(audioFormat?.sampleRate ?? 44100)
audioLengthSeconds = Float(audioLengthSamples) / audioSampleRate
duration = Duration(audioLengthSeconds)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: 0, durationLoadedByNetwork: duration, isPlayable: true)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: 0, durationLoadedByNetwork: duration, predictedDurationToLoad: duration, isPlayable: true)
} else {
Log.monitor("Could not load downloaded file with url: \(url)")
}
+7 -3
View File
@@ -86,8 +86,8 @@ class AudioEngine: AudioEngineProtocol {
}
}
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, isPlayable: false)
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, isPlayable: false) {
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false)
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false) {
didSet {
if bufferedSeconds.startingNeedle == 0.0 && bufferedSeconds.durationLoadedByNetwork == 0.0 {
bufferedSecondsDebouncer = bufferedSeconds
@@ -154,7 +154,11 @@ class AudioEngine: AudioEngineProtocol {
func updateIsPlaying() {
if !bufferedSeconds.isPlayable {
playingStatus = .buffering
if bufferedSeconds.bufferingProgress == 1.0 {
playingStatus = .ended
} else {
playingStatus = .buffering
}
return
}
+1 -7
View File
@@ -218,7 +218,7 @@ class AudioStreamEngine: AudioEngine {
let range = converter.pollNetworkAudioAvailabilityRange()
isPlayable = (numberOfBuffersScheduledInTotal >= MIN_BUFFERS_TO_BE_PLAYABLE && range.1 > 0) && predictedStreamDuration > 0
Log.debug("loaded \(range), numberOfBuffersScheduledInTotal: \(numberOfBuffersScheduledInTotal), isPlayable: \(isPlayable)")
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: range.0, durationLoadedByNetwork: range.1, isPlayable: isPlayable)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: range.0, durationLoadedByNetwork: range.1, predictedDurationToLoad: predictedStreamDuration, isPlayable: isPlayable)
}
private func updateNeedle() {
@@ -236,12 +236,6 @@ class AudioStreamEngine: AudioEngine {
var currentTime = TimeInterval(playerTime.sampleTime) / playerTime.sampleRate
currentTime = currentTime > 0 ? currentTime : 0
if currentTime > predictedStreamDuration {
Log.info("reached end of audio")
seek(toNeedle: 0)
pause()
playingStatus = .ended
}
needle = (currentTime + currentTimeOffset)
}
+10 -3
View File
@@ -97,7 +97,15 @@ class AudioThrottler: AudioThrottleable {
private var networkData: [NetworkDataWrapper] = []
var shouldThrottle = false
var byteOffsetBecauseOfSeek: UInt = 0
var totalBytesExpected: Int64? //this got sent up twice. Once at beginning of stream and second from network seek. We honor the first send
//This will be sent once at beginning of stream and every network seek
var totalBytesExpected: Int64? {
didSet {
if let bytes = totalBytesExpected {
delegate?.didUpdate(totalBytesExpected: Int64(byteOffsetBecauseOfSeek) + bytes)
}
}
}
var largestPollingOffsetDifference: UInt64 = 1
@@ -110,9 +118,8 @@ class AudioThrottler: AudioThrottleable {
Log.debug("received stream data of size \(pto.getData().count) and progress: \(pto.getProgress())")
self.delegate?.didUpdate(networkStreamProgress: pto.getProgress())
if self.totalBytesExpected == nil, let totalBytesExpected = pto.getTotalBytesExpected() {
if let totalBytesExpected = pto.getTotalBytesExpected() {
self.totalBytesExpected = totalBytesExpected
self.delegate?.didUpdate(totalBytesExpected: totalBytesExpected)
}
let lastItem = self.networkData.last
+3 -1
View File
@@ -85,7 +85,9 @@ class AudioParser: AudioParsable {
return max(AVAudioPacketCount(parsedAudioHeaderPacketCount), AVAudioPacketCount(audioPackets.count))
}
guard let sizeOfFileInBytes = expectedFileSizeInBytes, let bytesPerPacket = averageBytesPerPacket else {
let sizeOfFileInBytes: UInt64 = expectedFileSizeInBytes != nil ? expectedFileSizeInBytes! : 0
guard let bytesPerPacket = averageBytesPerPacket else {
return AVAudioPacketCount(0)
}
@@ -29,8 +29,15 @@ import Foundation
public struct SAAudioAvailabilityRange {
let startingNeedle: Needle
let durationLoadedByNetwork: Duration
let predictedDurationToLoad: Duration
let isPlayable: Bool
public var bufferingProgress: Double {
get {
return (startingNeedle + durationLoadedByNetwork) / predictedDurationToLoad
}
}
public var startingBufferTimePositon: Double {
get {
return startingNeedle
@@ -52,4 +59,8 @@ public struct SAAudioAvailabilityRange {
public func contains(_ needle: Double) -> Bool {
return needle >= startingNeedle && (needle - startingNeedle) < durationLoadedByNetwork
}
public func isCompletelyBuffered() -> Bool {
return startingNeedle + durationLoadedByNetwork >= predictedDurationToLoad
}
}
@@ -247,11 +247,15 @@ extension AudioStreamWorker: URLSessionDataDelegate {
return
}
guard let totalBytesExpected = totalBytesExpectedForCurrentStream, totalBytesExpected > 0 else {
guard var totalBytesExpected = totalBytesExpectedForCurrentStream else {
Log.monitor("should not be called 223r2")
return
}
if totalBytesExpected <= 0 {
totalBytesExpected = totalBytesReceived
}
totalBytesReceived = totalBytesReceived + Int64(data.count)
let progress = Double(totalBytesReceived)/Double(totalBytesExpected)
+24 -4
View File
@@ -104,6 +104,8 @@ public class SAPlayer {
/**
Total duration of current audio initialized. Returns nil if no audio is initialized in player.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, this value will be constantly updating to best known value at the time.
*/
public var duration: Double? {
get {
@@ -196,21 +198,27 @@ public class SAPlayer {
//MARK: - External Player Controls
extension SAPlayer {
/**
Toggles between the play and pause state of the player if the player is not buffering (thus is playable).
Toggles between the play and pause state of the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note: If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func togglePlayAndPause() {
presenter.handleTogglePlayingAndPausing()
}
/**
Attempts to play the player even if nothing playable is loaded (aka still in buffering state or no audio is initialized).
Attempts to play the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note: If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func play() {
presenter.handlePlay()
}
/**
Attempts to pause the player even if nothing playable is loaded (aka still in buffering state or no audio is initialized).
Attempts to pause the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note:If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func pause() {
presenter.handlePause()
@@ -262,13 +270,19 @@ extension SAPlayer {
- Parameter withSavedUrl: The URL of the audio saved on the device.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
@available(*, deprecated, renamed: "startSavedAudio")
public func initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
/**
Sets up player to play audio that will be streamed from a remote location.
Sets up player to play audio that will be streamed from a remote location. After this is called, it will connect to the server and start to receive and process data. The player is not playable the SAAudioAvailabilityRange notifies that player is ready for playing (you can subscribe to these updates through `SAPlayer.Updates.StreamingBuffer`). You can alternatively see when the player is available to play by subscribing to `SAPlayer.Updates.PlayingStatus` and waiting for a status that isn't `.buffering`.
- Important: If intending to use [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers during playback, the list of audio modifiers under `SAPlayer.shared.audioModifiers` must be finalized before calling this function. After all realtime audio manipulations within the this will be effective.
@@ -277,6 +291,12 @@ extension SAPlayer {
- Parameter withRemoteUrl: The URL of the remote audio.
- 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) {
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
}
@available(*, deprecated, renamed: "startRemoteAudio")
public func initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
+6
View File
@@ -66,12 +66,16 @@ extension SAPlayer {
/**
Updates to changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
*/
public struct Duration {
/**
Subscribe to updates to changes in duration of the current audio initialized.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
@@ -136,6 +140,8 @@ extension SAPlayer {
/**
Subscribe to updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information. For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
- Note: For live streams that don't have an expected audio length from the beginning of the stream; the duration is constantly changing and equal to the total seconds buffered from the SAAudioAvailabilityRange.
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '2.4.0'
s.version = '2.8.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.