Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d9bb98aed | |||
| 31368a54c1 | |||
| d3b563c7cd | |||
| a416cc8e92 | |||
| f36ca68faa |
@@ -5,7 +5,7 @@
|
||||
|
||||
import AVFoundation
|
||||
|
||||
public enum AudioConverterError: CustomDebugStringConvertible {
|
||||
public enum AudioConverterError: CustomDebugStringConvertible, Sendable {
|
||||
case badPropertySizeError
|
||||
case formatNotSupported
|
||||
case inputSampleRateOutOfRange
|
||||
|
||||
@@ -29,7 +29,7 @@ func fileStreamGetPropertyInfo(fileStream streamId: AudioFileStreamID, propertyI
|
||||
///
|
||||
/// Reference:
|
||||
/// [Audio File Stream Errors](https://developer.apple.com/documentation/audiotoolbox/1391572-audio_file_stream_errors?language=objc)
|
||||
public enum AudioFileStreamError: CustomDebugStringConvertible {
|
||||
public enum AudioFileStreamError: CustomDebugStringConvertible, Sendable {
|
||||
case badPropertySize
|
||||
case dataUnavailable
|
||||
case discontinuityCantRecover
|
||||
|
||||
@@ -37,6 +37,11 @@ class AudioEntry {
|
||||
return seekTime + (Double(framesState.played) / outputAudioFormat.sampleRate)
|
||||
}
|
||||
|
||||
var framesPlayed: Int {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
return framesState.played
|
||||
}
|
||||
|
||||
var audioStreamFormat = AudioStreamBasicDescription()
|
||||
|
||||
/// Hold the seek time, if a seek was requested
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import AVFoundation
|
||||
|
||||
protocol AudioEntryProviding {
|
||||
func provideAudioEntry(url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) -> AudioEntry
|
||||
func provideAudioEntry(url: URL, headers: [String: String]) -> AudioEntry
|
||||
func provideAudioEntry(url: URL) -> AudioEntry
|
||||
}
|
||||
@@ -25,7 +26,14 @@ final class AudioEntryProvider: AudioEntryProviding {
|
||||
}
|
||||
|
||||
func provideAudioEntry(url: URL, headers: [String: String]) -> AudioEntry {
|
||||
let source = self.source(for: url, headers: headers)
|
||||
let source = self.source(for: url, httpMethod: nil, httpBody: nil, headers: headers)
|
||||
return AudioEntry(source: source,
|
||||
entryId: AudioEntryId(id: url.absoluteString),
|
||||
outputAudioFormat: outputAudioFormat)
|
||||
}
|
||||
|
||||
func provideAudioEntry(url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) -> AudioEntry {
|
||||
let source = self.source(for: url, httpMethod: httpMethod, httpBody: httpBody, headers: headers)
|
||||
return AudioEntry(source: source,
|
||||
entryId: AudioEntryId(id: url.absoluteString),
|
||||
outputAudioFormat: outputAudioFormat)
|
||||
@@ -34,10 +42,12 @@ final class AudioEntryProvider: AudioEntryProviding {
|
||||
func provideAudioEntry(url: URL) -> AudioEntry {
|
||||
provideAudioEntry(url: url, headers: [:])
|
||||
}
|
||||
|
||||
func provideAudioSource(url: URL, headers: [String: String]) -> AudioStreamSource {
|
||||
|
||||
func provideAudioSource(url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) -> AudioStreamSource {
|
||||
RemoteAudioSource(networking: networkingClient,
|
||||
url: url,
|
||||
httpMethod: httpMethod,
|
||||
httpBody: httpBody,
|
||||
underlyingQueue: underlyingQueue,
|
||||
httpHeaders: headers)
|
||||
}
|
||||
@@ -46,10 +56,10 @@ final class AudioEntryProvider: AudioEntryProviding {
|
||||
FileAudioSource(url: url, underlyingQueue: underlyingQueue)
|
||||
}
|
||||
|
||||
func source(for url: URL, headers: [String: String]) -> CoreAudioStreamSource {
|
||||
func source(for url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) -> CoreAudioStreamSource {
|
||||
guard !url.isFileURL else {
|
||||
return provideFileAudioSource(url: url)
|
||||
}
|
||||
return provideAudioSource(url: url, headers: headers)
|
||||
return provideAudioSource(url: url, httpMethod: httpMethod, httpBody: httpBody, headers: headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
}
|
||||
|
||||
private let url: URL
|
||||
private let httpMethod: String?
|
||||
private let httpBody: Data?
|
||||
private let networkingClient: NetworkingClient
|
||||
private var streamRequest: NetworkDataStream?
|
||||
|
||||
@@ -61,12 +63,16 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
netStatusProvider: NetStatusProvider,
|
||||
retrier: Retrier,
|
||||
url: URL,
|
||||
httpMethod: String?,
|
||||
httpBody: Data?,
|
||||
underlyingQueue: DispatchQueue,
|
||||
httpHeaders: [String: String])
|
||||
{
|
||||
networkingClient = networking
|
||||
metadataStreamProcessor = metadataStreamSource
|
||||
self.url = url
|
||||
self.httpMethod = httpMethod
|
||||
self.httpBody = httpBody
|
||||
additionalRequestHeaders = httpHeaders
|
||||
relativePosition = 0
|
||||
seekOffset = 0
|
||||
@@ -83,9 +89,11 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
mp4Restructure = RemoteMp4Restructure(url: url, networking: networkingClient)
|
||||
startNetworkService()
|
||||
}
|
||||
|
||||
|
||||
convenience init(networking: NetworkingClient,
|
||||
url: URL,
|
||||
httpMethod: String?,
|
||||
httpBody: Data?,
|
||||
underlyingQueue: DispatchQueue,
|
||||
httpHeaders: [String: String])
|
||||
{
|
||||
@@ -100,6 +108,21 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
netStatusProvider: netStatusProvider,
|
||||
retrier: retrierTimeout,
|
||||
url: url,
|
||||
httpMethod: httpMethod,
|
||||
httpBody: httpBody,
|
||||
underlyingQueue: underlyingQueue,
|
||||
httpHeaders: httpHeaders)
|
||||
}
|
||||
|
||||
convenience init(networking: NetworkingClient,
|
||||
url: URL,
|
||||
underlyingQueue: DispatchQueue,
|
||||
httpHeaders: [String: String])
|
||||
{
|
||||
self.init(networking: networking,
|
||||
url: url,
|
||||
httpMethod: nil,
|
||||
httpBody: nil,
|
||||
underlyingQueue: underlyingQueue,
|
||||
httpHeaders: httpHeaders)
|
||||
}
|
||||
@@ -347,6 +370,8 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
urlRequest.networkServiceType = .avStreaming
|
||||
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
|
||||
urlRequest.timeoutInterval = 60
|
||||
urlRequest.httpMethod = httpMethod
|
||||
urlRequest.httpBody = httpBody
|
||||
|
||||
for header in additionalRequestHeaders {
|
||||
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
|
||||
@@ -366,6 +391,8 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
urlRequest.networkServiceType = .avStreaming
|
||||
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
|
||||
urlRequest.timeoutInterval = 60
|
||||
urlRequest.httpMethod = httpMethod
|
||||
urlRequest.httpBody = httpBody
|
||||
|
||||
for header in additionalRequestHeaders {
|
||||
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
|
||||
|
||||
@@ -81,6 +81,16 @@ open class AudioPlayer {
|
||||
return entry.progress
|
||||
}
|
||||
|
||||
/// The number of audio frames that have been played
|
||||
public var framesPlayed: Int {
|
||||
guard playerContext.internalState != .pendingNext else { return 0 }
|
||||
playerContext.entriesLock.lock()
|
||||
let playingEntry = playerContext.audioPlayingEntry
|
||||
playerContext.entriesLock.unlock()
|
||||
guard let entry = playingEntry else { return 0 }
|
||||
return entry.framesPlayed
|
||||
}
|
||||
|
||||
public private(set) var customAttachedNodes = [AVAudioNode]()
|
||||
|
||||
/// The current configuration of the player.
|
||||
@@ -192,6 +202,17 @@ open class AudioPlayer {
|
||||
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
|
||||
play(audioEntry: audioEntry)
|
||||
}
|
||||
|
||||
/// Starts the audio playback for the given URL
|
||||
///
|
||||
/// - parameter url: A `URL` specifying the audio context to be played.
|
||||
/// - parameter httpMethod: A `String` specifying the HTTP method to use (e.g. "GET", "POST").
|
||||
/// - parameter httpBody: A "Data" specifying the HTTP request body, if any.
|
||||
/// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request.
|
||||
public func play(url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) {
|
||||
let audioEntry = entryProvider.provideAudioEntry(url: url, httpMethod: httpMethod, httpBody: httpBody, headers: headers)
|
||||
play(audioEntry: audioEntry)
|
||||
}
|
||||
|
||||
/// Starts the audio playback for the supplied stream
|
||||
///
|
||||
|
||||
@@ -55,7 +55,7 @@ func playerStateAndStopReason(
|
||||
|
||||
// MARK: Public States
|
||||
|
||||
public enum AudioPlayerState: Equatable {
|
||||
public enum AudioPlayerState: Equatable, Sendable {
|
||||
case ready
|
||||
case running
|
||||
case playing
|
||||
@@ -66,7 +66,7 @@ public enum AudioPlayerState: Equatable {
|
||||
case disposed
|
||||
}
|
||||
|
||||
public enum AudioPlayerStopReason: Equatable {
|
||||
public enum AudioPlayerStopReason: Equatable, Sendable {
|
||||
case none
|
||||
case eof
|
||||
case userAction
|
||||
@@ -74,7 +74,7 @@ public enum AudioPlayerStopReason: Equatable {
|
||||
case disposed
|
||||
}
|
||||
|
||||
public enum AudioPlayerError: LocalizedError, Equatable {
|
||||
public enum AudioPlayerError: LocalizedError, Equatable, Sendable {
|
||||
case streamParseBytesFailure(AudioFileStreamError)
|
||||
case audioSystemError(AudioSystemError)
|
||||
case codecError
|
||||
@@ -100,7 +100,7 @@ public enum AudioPlayerError: LocalizedError, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum AudioSystemError: LocalizedError, Equatable {
|
||||
public enum AudioSystemError: LocalizedError, Equatable, Sendable {
|
||||
case engineFailure
|
||||
case playerNotFound
|
||||
case playerStartError
|
||||
|
||||
Reference in New Issue
Block a user