Compare commits

...

5 Commits

Author SHA1 Message Date
Tiger W 4d9bb98aed Support customizing HTTP method and HTTP body (#108) 2025-05-30 14:38:29 +03:00
Dimitris C. 31368a54c1 Revert "Revert "Expose the framesPlayed attribute so progress can be tracked …" (#111)
This reverts commit d3b563c7cd.
2025-05-30 10:13:46 +03:00
Dimitris C. d3b563c7cd Revert "Expose the framesPlayed attribute so progress can be tracked based on…" (#110)
This reverts commit a416cc8e92.
2025-05-29 17:50:50 +03:00
Jackson Harper a416cc8e92 Expose the framesPlayed attribute so progress can be tracked based on frames instead of time (#109) 2025-05-29 17:45:31 +03:00
Stuart A. Malone f36ca68faa Mark state and error types as Sendable so clients can pass them (#105)
across isolation boundaries.
2025-02-26 17:20:33 +02:00
7 changed files with 75 additions and 12 deletions
@@ -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