Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0758c14909 | |||
| 03c6a7692c | |||
| 02a3606185 | |||
| 7e45a7b2f5 | |||
| 30b4189778 | |||
| 8bdc2a64f7 | |||
| 65de9d90c0 | |||
| 217a88f171 | |||
| 566dc86f3f | |||
| d8aa58525c | |||
| 8197db0016 | |||
| c2aee1669b | |||
| 334be32bf9 | |||
| a2da46f85b | |||
| aca69debd1 |
@@ -153,7 +153,7 @@ extension PlayerControlsViewModel: AudioPlayerServiceDelegate {
|
||||
updateContent?(.updateMetadata(""))
|
||||
}
|
||||
|
||||
func errorOccured(error _: AudioPlayerError) {}
|
||||
func errorOccurred(error _: AudioPlayerError) {}
|
||||
|
||||
func metadataReceived(metadata: [String: String]) {
|
||||
guard !metadata.isEmpty else { return }
|
||||
|
||||
@@ -96,7 +96,7 @@ extension PlayerViewModel: AudioPlayerServiceDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func errorOccured(error _: AudioPlayerError) {
|
||||
func errorOccurred(error _: AudioPlayerError) {
|
||||
currentPlayingItemIndex = nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ protocol AudioPlayerServiceDelegate: AnyObject {
|
||||
func didStartPlaying()
|
||||
func didStopPlaying()
|
||||
func statusChanged(status: AudioPlayerState)
|
||||
func errorOccured(error: AudioPlayerError)
|
||||
func errorOccurred(error: AudioPlayerError)
|
||||
func metadataReceived(metadata: [String: String])
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ extension AudioPlayerService: AudioPlayerDelegate {
|
||||
}
|
||||
|
||||
func audioPlayerUnexpectedError(player _: AudioPlayer, error: AudioPlayerError) {
|
||||
delegate.invoke(invocation: { $0.errorOccured(error: error) })
|
||||
delegate.invoke(invocation: { $0.errorOccurred(error: error) })
|
||||
}
|
||||
|
||||
func audioPlayerDidCancel(player _: AudioPlayer, queuedItems _: [AudioEntryId]) {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '0.3.0'
|
||||
s.version = '0.8.0'
|
||||
s.license = 'MIT'
|
||||
s.summary = 'An AudioPlayer/Streaming library for iOS written in Swift using AVAudioEngine.'
|
||||
s.homepage = 'https://github.com/dimitris-c/AudioStreaming'
|
||||
|
||||
@@ -52,7 +52,10 @@
|
||||
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59DF1A22493E90C0043C498 /* AudioFileStream+Helpers.swift */; };
|
||||
B5AEDBB824744153007D8101 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AEDBAE24744153007D8101 /* AudioStreaming.framework */; };
|
||||
B5AEDBBF24744153007D8101 /* AudioStreaming.h in Headers */ = {isa = PBXBuildFile; fileRef = B5AEDBB124744153007D8101 /* AudioStreaming.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */; };
|
||||
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */; };
|
||||
B5D4A40925D9321400E1450C /* IcycastHeaderParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */; };
|
||||
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */; };
|
||||
B5D82E65255DD562009EDAA4 /* NetStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D82E64255DD562009EDAA4 /* NetStatusService.swift */; };
|
||||
B5DB66E2255C2EAB00B8DF53 /* AudioEntryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DB66E1255C2EAB00B8DF53 /* AudioEntryProvider.swift */; };
|
||||
B5E1DE2524B70B4200955BFB /* AudioPlayerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E1DE2424B70B4200955BFB /* AudioPlayerConfiguration.swift */; };
|
||||
@@ -142,7 +145,10 @@
|
||||
B5AEDBB224744153007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
B5AEDBB724744153007D8101 /* AudioStreamingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AudioStreamingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B5AEDBBE24744153007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameFilterProcessor.swift; sourceTree = "<group>"; };
|
||||
B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerState.swift; sourceTree = "<group>"; };
|
||||
B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcycastHeaderParser.swift; sourceTree = "<group>"; };
|
||||
B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcycastHeadersProcessor.swift; sourceTree = "<group>"; };
|
||||
B5D82E64255DD562009EDAA4 /* NetStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetStatusService.swift; sourceTree = "<group>"; };
|
||||
B5DB66DA255C079C00B8DF53 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
B5DB66E1255C2EAB00B8DF53 /* AudioEntryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEntryProvider.swift; sourceTree = "<group>"; };
|
||||
@@ -205,6 +211,7 @@
|
||||
B55CEAB32485107C0001C498 /* Parser.swift */,
|
||||
B55A736B247FCB420050C53D /* HTTPHeaderParser.swift */,
|
||||
B55CE96D248058B60001C498 /* MetadataParser.swift */,
|
||||
B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */,
|
||||
);
|
||||
path = Parsers;
|
||||
sourceTree = "<group>";
|
||||
@@ -254,9 +261,11 @@
|
||||
B55CEAC024855AA20001C498 /* Processors */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */,
|
||||
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */,
|
||||
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */,
|
||||
B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */,
|
||||
B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */,
|
||||
);
|
||||
path = Processors;
|
||||
sourceTree = "<group>";
|
||||
@@ -594,6 +603,7 @@
|
||||
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */,
|
||||
B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */,
|
||||
B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */,
|
||||
B5D4A40925D9321400E1450C /* IcycastHeaderParser.swift in Sources */,
|
||||
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */,
|
||||
B54D876D2490E4A000C361A0 /* UnitDescriptions.swift in Sources */,
|
||||
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */,
|
||||
@@ -604,9 +614,11 @@
|
||||
B5EF955B247EBCB3003E8FF8 /* AudioFileType.swift in Sources */,
|
||||
B592E1252545FF9A008866FB /* BiMap.swift in Sources */,
|
||||
B5DB66E2255C2EAB00B8DF53 /* AudioEntryProvider.swift in Sources */,
|
||||
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */,
|
||||
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */,
|
||||
B59D0B6F255C904900D6CCE5 /* FileAudioSource.swift in Sources */,
|
||||
B5EF9555247E9393003E8FF8 /* AudioEntry.swift in Sources */,
|
||||
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */,
|
||||
B51FE0C02488F67C00F2A4D2 /* Queue.swift in Sources */,
|
||||
B5667A922499063D00D93F85 /* AudioPlayerContext.swift in Sources */,
|
||||
B55CE97124810DE20001C498 /* MetadataStreamProcessor.swift in Sources */,
|
||||
@@ -786,6 +798,7 @@
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
@@ -798,7 +811,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
@@ -816,6 +829,7 @@
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
@@ -828,7 +842,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
||||
@@ -9,7 +9,7 @@ extension AVAudioFormat {
|
||||
/// The underlying audio stream description.
|
||||
///
|
||||
/// This exposes the `pointee` value of the `UsafePointer<AudioStreamBasicDescription>`
|
||||
var basicStreamDescription: AudioStreamBasicDescription {
|
||||
public var basicStreamDescription: AudioStreamBasicDescription {
|
||||
return streamDescription.pointee
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ internal enum Logger {
|
||||
}
|
||||
|
||||
static func error(_ message: StaticString, category: Category, args: CVarArg...) {
|
||||
proccess(message, category: category, type: .error, args: args)
|
||||
process(message, category: category, type: .error, args: args)
|
||||
}
|
||||
|
||||
static func error(_ message: StaticString, category: Category) {
|
||||
@@ -39,14 +39,14 @@ internal enum Logger {
|
||||
}
|
||||
|
||||
static func debug(_ message: StaticString, category: Category, args: CVarArg...) {
|
||||
proccess(message, category: category, type: .debug, args: args)
|
||||
process(message, category: category, type: .debug, args: args)
|
||||
}
|
||||
|
||||
static func debug(_ message: StaticString, category: Category) {
|
||||
debug(message, category: category, args: [])
|
||||
}
|
||||
|
||||
private static func proccess(_ message: StaticString, category: Category, type: OSLogType, args: CVarArg...) {
|
||||
private static func process(_ message: StaticString, category: Category, type: OSLogType, args: CVarArg...) {
|
||||
guard isEnabled else { return }
|
||||
os_log(message, log: category.toOSLog(), type: type, args)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ internal class AudioEntry {
|
||||
return Double(audioStreamFormat.mFramesPerPacket) / Double(sampleRate)
|
||||
}
|
||||
|
||||
private var avaragePacketByteSize: Double {
|
||||
private var averagePacketByteSize: Double {
|
||||
let packets = processedPacketsState
|
||||
guard !packets.isEmpty else { return 0 }
|
||||
return Double(packets.sizeTotal / packets.count)
|
||||
@@ -109,7 +109,7 @@ internal class AudioEntry {
|
||||
if packetsCount > estimationMinPacketsPreferred ||
|
||||
(audioStreamFormat.mBytesPerFrame == 0 && packetsCount > estimationMinPackets)
|
||||
{
|
||||
return avaragePacketByteSize / packetDuration * 8
|
||||
return averagePacketByteSize / packetDuration * 8
|
||||
}
|
||||
}
|
||||
return (Double(audioStreamFormat.mBytesPerFrame) * audioStreamFormat.mSampleRate) * 8
|
||||
@@ -151,12 +151,12 @@ extension AudioEntry: AudioStreamSourceDelegate {
|
||||
delegate?.dataAvailable(source: source, data: data)
|
||||
}
|
||||
|
||||
func errorOccured(source: CoreAudioStreamSource, error: Error) {
|
||||
delegate?.errorOccured(source: source, error: error)
|
||||
func errorOccurred(source: CoreAudioStreamSource, error: Error) {
|
||||
delegate?.errorOccurred(source: source, error: error)
|
||||
}
|
||||
|
||||
func endOfFileOccured(source: CoreAudioStreamSource) {
|
||||
delegate?.endOfFileOccured(source: source)
|
||||
func endOfFileOccurred(source: CoreAudioStreamSource) {
|
||||
delegate?.endOfFileOccurred(source: source)
|
||||
}
|
||||
|
||||
func metadataReceived(data: [String: String]) {
|
||||
|
||||
@@ -46,10 +46,10 @@ final class AudioEntryProvider: AudioEntryProviding {
|
||||
FileAudioSource(url: url, underlyingQueue: underlyingQueue)
|
||||
}
|
||||
|
||||
func source(for url: URL, headers _: [String: String]) -> CoreAudioStreamSource {
|
||||
func source(for url: URL, headers: [String: String]) -> CoreAudioStreamSource {
|
||||
guard !url.isFileURL else {
|
||||
return provideFileAudioSource(url: url)
|
||||
}
|
||||
return provideAudioSource(url: url, headers: [:])
|
||||
return provideAudioSource(url: url, headers: headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ protocol AudioStreamSourceDelegate: AnyObject {
|
||||
/// Indicates that there's data available
|
||||
func dataAvailable(source: CoreAudioStreamSource, data: Data)
|
||||
/// Indicates an error occurred
|
||||
func errorOccured(source: CoreAudioStreamSource, error: Error)
|
||||
func errorOccurred(source: CoreAudioStreamSource, error: Error)
|
||||
/// Indicates end of file has occurred
|
||||
func endOfFileOccured(source: CoreAudioStreamSource)
|
||||
func endOfFileOccurred(source: CoreAudioStreamSource)
|
||||
/// Indicates metadata read from stream
|
||||
func metadataReceived(data: [String: String])
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
||||
do {
|
||||
try performOpen(seek: offset)
|
||||
} catch {
|
||||
delegate?.errorOccured(source: self, error: error)
|
||||
delegate?.errorOccurred(source: self, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,11 +139,11 @@ extension FileAudioSource: StreamDelegate {
|
||||
case .hasBytesAvailable:
|
||||
dataAvailable()
|
||||
case .endEncountered:
|
||||
delegate?.endOfFileOccured(source: self)
|
||||
delegate?.endOfFileOccurred(source: self)
|
||||
case .errorOccurred:
|
||||
delegate?.errorOccured(source: self, error: AudioPlayerError.codecError)
|
||||
delegate?.errorOccurred(source: self, error: AudioPlayerError.codecError)
|
||||
case .endEncountered:
|
||||
delegate?.endOfFileOccured(source: self)
|
||||
delegate?.endOfFileOccurred(source: self)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
|
||||
internal var metadataStreamProcessor: MetadataStreamSource
|
||||
|
||||
private var shouldTryParsingIcycastHeaders: Bool = false
|
||||
private let icycastHeadersProcessor: IcycastHeadersProcessor
|
||||
|
||||
internal var audioFileHint: AudioFileTypeID {
|
||||
guard let output = parsedHeaderOutput, output.typeId != 0 else {
|
||||
return audioFileType(fileExtension: url.pathExtension)
|
||||
@@ -48,6 +51,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
|
||||
init(networking: NetworkingClient,
|
||||
metadataStreamSource: MetadataStreamSource,
|
||||
icycastHeadersProcessor: IcycastHeadersProcessor,
|
||||
netStatusProvider: NetStatusProvider,
|
||||
retrier: Retrier,
|
||||
url: URL,
|
||||
@@ -62,6 +66,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
seekOffset = 0
|
||||
supportsSeek = false
|
||||
netStatusService = netStatusProvider
|
||||
self.icycastHeadersProcessor = icycastHeadersProcessor
|
||||
self.underlyingQueue = underlyingQueue
|
||||
streamOperationQueue = OperationQueue()
|
||||
streamOperationQueue.underlyingQueue = underlyingQueue
|
||||
@@ -80,11 +85,13 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
let metadataParser = MetadataParser()
|
||||
let metadataProcessor = MetadataStreamProcessor(parser: metadataParser.eraseToAnyParser())
|
||||
let netStatusProvider = NetStatusService(network: NWPathMonitor())
|
||||
let retrierTimout = Retrier(interval: .seconds(1), maxInterval: 5, underlyingQueue: nil)
|
||||
let icyheaderProcessor = IcycastHeadersProcessor()
|
||||
let retrierTimeout = Retrier(interval: .seconds(1), maxInterval: 5, underlyingQueue: nil)
|
||||
self.init(networking: networking,
|
||||
metadataStreamSource: metadataProcessor,
|
||||
icycastHeadersProcessor: icyheaderProcessor,
|
||||
netStatusProvider: netStatusProvider,
|
||||
retrier: retrierTimout,
|
||||
retrier: retrierTimeout,
|
||||
url: url,
|
||||
underlyingQueue: underlyingQueue,
|
||||
httpHeaders: httpHeaders)
|
||||
@@ -103,7 +110,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
func close() {
|
||||
retrierTimeout.cancel()
|
||||
netStatusService.stop()
|
||||
streamOperationQueue.isSuspended = true
|
||||
streamOperationQueue.isSuspended = false
|
||||
streamOperationQueue.cancelAllOperations()
|
||||
if let streamTask = streamRequest {
|
||||
streamTask.cancel()
|
||||
@@ -124,17 +131,17 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
|
||||
retrierTimeout.cancel()
|
||||
metadataStreamProcessor.reset()
|
||||
icycastHeadersProcessor.reset()
|
||||
shouldTryParsingIcycastHeaders = false
|
||||
|
||||
performOpen(seek: offset)
|
||||
}
|
||||
|
||||
func suspend() {
|
||||
streamRequest?.suspend()
|
||||
streamOperationQueue.isSuspended = true
|
||||
}
|
||||
|
||||
func resume() {
|
||||
streamRequest?.resume()
|
||||
streamOperationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
@@ -176,11 +183,11 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
handleStreamEvent(event: event)
|
||||
case let .complete(event):
|
||||
if let error = event.error {
|
||||
delegate?.errorOccured(source: self, error: error)
|
||||
delegate?.errorOccurred(source: self, error: error)
|
||||
} else {
|
||||
addCompletionOperation { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.delegate?.endOfFileOccured(source: self)
|
||||
self.delegate?.endOfFileOccurred(source: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,16 +196,25 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
private func handleStreamEvent(event: NetworkDataStream.StreamResult) {
|
||||
switch event {
|
||||
case let .success(value):
|
||||
if let data = value.data {
|
||||
if let audioData = value.data {
|
||||
addStreamOperation { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.metadataStreamProcessor.canProccessMetadata {
|
||||
let extractedAudioData = self.metadataStreamProcessor.proccessMetadata(data: data)
|
||||
self.delegate?.dataAvailable(source: self, data: extractedAudioData)
|
||||
} else {
|
||||
self.delegate?.dataAvailable(source: self, data: data)
|
||||
if self.shouldTryParsingIcycastHeaders {
|
||||
let (header, extractedAudio) = self.icycastHeadersProcessor.process(data: audioData)
|
||||
if let header = header {
|
||||
self.shouldTryParsingIcycastHeaders = false
|
||||
let parser = IcycastHeaderParser()
|
||||
self.parsedHeaderOutput = parser.parse(input: header)
|
||||
if let metadataStep = self.parsedHeaderOutput?.metadataStep {
|
||||
self.metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||
}
|
||||
}
|
||||
let audioCount = self.processAudio(data: extractedAudio)
|
||||
self.relativePosition += audioCount
|
||||
return
|
||||
}
|
||||
self.relativePosition += data.count
|
||||
let audioCount = self.processAudio(data: audioData)
|
||||
self.relativePosition += audioCount
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
@@ -211,12 +227,32 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Processing audio data, extracting metadata if needed.
|
||||
/// - Parameter data: The audio to be processed
|
||||
/// - Returns: An `Int` value representing the amount of audio data bytes.
|
||||
private func processAudio(data: Data) -> Int {
|
||||
if self.metadataStreamProcessor.canProcessMetadata {
|
||||
let extractedAudioData = self.metadataStreamProcessor.processMetadata(data: data)
|
||||
self.delegate?.dataAvailable(source: self, data: extractedAudioData)
|
||||
return extractedAudioData.count
|
||||
} else {
|
||||
self.delegate?.dataAvailable(source: self, data: data)
|
||||
return data.count
|
||||
}
|
||||
}
|
||||
|
||||
private func parseResponseHeader(response: HTTPURLResponse?) {
|
||||
guard let response = response else { return }
|
||||
let httpStatusCode = response.statusCode
|
||||
let parser = HTTPHeaderParser()
|
||||
parsedHeaderOutput = parser.parse(input: response)
|
||||
|
||||
if parsedHeaderOutput == nil {
|
||||
shouldTryParsingIcycastHeaders = true
|
||||
checkHTTP(statusCode: httpStatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
if let acceptRanges = parser.value(forHTTPHeaderField: HeaderField.acceptRanges, in: response) {
|
||||
supportsSeek = acceptRanges != "none"
|
||||
}
|
||||
@@ -225,12 +261,16 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
if let metadataStep = parsedHeaderOutput?.metadataStep {
|
||||
metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||
}
|
||||
checkHTTP(statusCode: httpStatusCode)
|
||||
}
|
||||
|
||||
private func checkHTTP(statusCode: Int) {
|
||||
// check for error
|
||||
if httpStatusCode == 416 { // range not satisfied error
|
||||
if statusCode == 416 { // range not satisfied error
|
||||
if length >= 0 { seekOffset = length }
|
||||
delegate?.endOfFileOccured(source: self)
|
||||
} else if httpStatusCode >= 300 {
|
||||
delegate?.errorOccured(source: self, error: NetworkError.serverError)
|
||||
delegate?.endOfFileOccurred(source: self)
|
||||
} else if statusCode >= 300 {
|
||||
delegate?.errorOccurred(source: self, error: NetworkError.serverError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import AVFoundation
|
||||
import CoreAudio
|
||||
|
||||
public final class AudioPlayer {
|
||||
open class AudioPlayer {
|
||||
public weak var delegate: AudioPlayerDelegate?
|
||||
|
||||
public var muted: Bool {
|
||||
@@ -86,6 +86,19 @@ public final class AudioPlayer {
|
||||
/// The current configuration of the player.
|
||||
public let configuration: AudioPlayerConfiguration
|
||||
|
||||
/// A Boolean value that indicates whether the audio engine is running.
|
||||
/// `true` if the engine is running, otherwise, `false`
|
||||
public var isEngineRunning: Bool { audioEngine.isRunning }
|
||||
|
||||
/// The `AVAudioMixerNode` as created by the underlying audio engine
|
||||
public var mainMixerNode: AVAudioMixerNode {
|
||||
audioEngine.mainMixerNode
|
||||
}
|
||||
|
||||
public var frameFiltering: FrameFiltering {
|
||||
frameFilterProcessor
|
||||
}
|
||||
|
||||
/// An `AVAudioFormat` object for the canonical audio stream
|
||||
private var outputAudioFormat: AVAudioFormat = {
|
||||
AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100.0, channels: 2, interleaved: true)!
|
||||
@@ -95,27 +108,23 @@ public final class AudioPlayer {
|
||||
private var stateBeforePaused: InternalState = .initial
|
||||
|
||||
/// The underlying `AVAudioEngine` object
|
||||
let audioEngine = AVAudioEngine()
|
||||
private let audioEngine = AVAudioEngine()
|
||||
/// An `AVAudioUnit` object that represents the audio player
|
||||
private(set) var player = AVAudioUnit()
|
||||
/// An `AVAudioUnitTimePitch` that controls the playback rate of the audio engine
|
||||
let rateNode = AVAudioUnitTimePitch()
|
||||
|
||||
/// A Boolean value that indicates whether the audio engine is running.
|
||||
/// `true` if the engine is running, otherwise, `false`
|
||||
var isEngineRunning: Bool { audioEngine.isRunning }
|
||||
private let rateNode = AVAudioUnitTimePitch()
|
||||
|
||||
/// An object representing the context of the audio render.
|
||||
/// Holds the audio buffer and in/out lists as required by the audio rendering
|
||||
let rendererContext: AudioRendererContext
|
||||
private let rendererContext: AudioRendererContext
|
||||
/// An object representing the context of the player.
|
||||
/// Holds the player's state, current playing and reading entries.
|
||||
let playerContext: AudioPlayerContext
|
||||
private let playerContext: AudioPlayerContext
|
||||
|
||||
let fileStreamProcessor: AudioFileStreamProcessor
|
||||
let playerRenderProcessor: AudioPlayerRenderProcessor
|
||||
private let fileStreamProcessor: AudioFileStreamProcessor
|
||||
private let playerRenderProcessor: AudioPlayerRenderProcessor
|
||||
private let frameFilterProcessor: FrameFilterProcessor
|
||||
|
||||
private let audioReadSource: DispatchTimerSource
|
||||
private let serializationQueue: DispatchQueue
|
||||
private let sourceQueue: DispatchQueue
|
||||
|
||||
@@ -132,7 +141,6 @@ public final class AudioPlayer {
|
||||
|
||||
serializationQueue = DispatchQueue(label: "streaming.core.queue", qos: .userInitiated)
|
||||
sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated)
|
||||
audioReadSource = DispatchTimerSource(interval: .milliseconds(200), queue: sourceQueue)
|
||||
|
||||
entryProvider = AudioEntryProvider(networkingClient: NetworkingClient(),
|
||||
underlyingQueue: sourceQueue,
|
||||
@@ -142,6 +150,8 @@ public final class AudioPlayer {
|
||||
rendererContext: rendererContext,
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
||||
|
||||
frameFilterProcessor = FrameFilterProcessor(mixerNode: audioEngine.mainMixerNode)
|
||||
|
||||
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
|
||||
rendererContext: rendererContext,
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
||||
@@ -154,7 +164,6 @@ public final class AudioPlayer {
|
||||
deinit {
|
||||
playerContext.audioPlayingEntry?.close()
|
||||
clearQueue()
|
||||
stopReadProccessFromSource()
|
||||
rendererContext.clean()
|
||||
}
|
||||
|
||||
@@ -183,14 +192,13 @@ public final class AudioPlayer {
|
||||
do {
|
||||
try self.startEngineIfNeeded()
|
||||
} catch {
|
||||
self.raiseUnxpected(error: .audioSystemError(.engineFailure))
|
||||
self.raiseUnexpected(error: .audioSystemError(.engineFailure))
|
||||
}
|
||||
}
|
||||
|
||||
sourceQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.processSource()
|
||||
self.startReadProcessFromSourceIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +254,6 @@ public final class AudioPlayer {
|
||||
public func stop() {
|
||||
guard playerContext.internalState != .stopped else { return }
|
||||
|
||||
stopReadProccessFromSource()
|
||||
serializationQueue.sync {
|
||||
stopEngine(reason: .userAction)
|
||||
}
|
||||
@@ -277,7 +284,6 @@ public final class AudioPlayer {
|
||||
serializationQueue.sync {
|
||||
pauseEngine()
|
||||
}
|
||||
stopReadProccessFromSource()
|
||||
playerContext.audioPlayingEntry?.suspend()
|
||||
sourceQueue.async { [weak self] in
|
||||
self?.processSource()
|
||||
@@ -303,9 +309,10 @@ public final class AudioPlayer {
|
||||
}
|
||||
startPlayer(resetBuffers: false)
|
||||
}
|
||||
startReadProcessFromSourceIfNeeded()
|
||||
}
|
||||
|
||||
/// Seeks the audio to the specified time.
|
||||
/// - Parameter time: A `Double` value specifing the time of the requested seek in seconds
|
||||
public func seek(to time: Double) {
|
||||
guard let playingEntry = playerContext.audioPlayingEntry else {
|
||||
return
|
||||
@@ -328,10 +335,16 @@ public final class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches the given `AVAudioNode` to the engine
|
||||
/// - Note: The node will be added after the default rate node
|
||||
/// - Parameter node: An instance of `AVAudioNode`
|
||||
public func attach(node: AVAudioNode) {
|
||||
attach(nodes: [node])
|
||||
}
|
||||
|
||||
/// Attaches the given `AVAudioNode`s to the engine
|
||||
/// - Note: The nodes will be added after the default rate node
|
||||
/// - Parameter node: An array of `AVAudioNode` instances
|
||||
public func attach(nodes: [AVAudioNode]) {
|
||||
nodes.forEach { node in
|
||||
customAttachedNodes.append(node)
|
||||
@@ -341,6 +354,8 @@ public final class AudioPlayer {
|
||||
reattachCustomNodes()
|
||||
}
|
||||
|
||||
/// Detaches the given `AVAudioNode` from the engine
|
||||
/// - Parameter node: An instance of `AVAudioNode`
|
||||
public func detach(node: AVAudioNode) {
|
||||
guard customAttachedNodes.contains(node) else {
|
||||
return
|
||||
@@ -350,6 +365,8 @@ public final class AudioPlayer {
|
||||
reattachCustomNodes()
|
||||
}
|
||||
|
||||
/// Detaches the given `AVAudioNode`s from the engine
|
||||
/// - Parameter node: An array of `AVAudioNode` instances
|
||||
public func detachCustomAttachedNodes() {
|
||||
customAttachedNodes.forEach { node in
|
||||
audioEngine.detach(node)
|
||||
@@ -400,7 +417,7 @@ public final class AudioPlayer {
|
||||
self.playerRenderProcessor.attachCallback(on: unit, audioFormat: self.outputAudioFormat)
|
||||
case let .failure(error):
|
||||
assertionFailure("couldn't create player unit: \(error)")
|
||||
self.raiseUnxpected(error: .audioSystemError(.playerNotFound))
|
||||
self.raiseUnexpected(error: .audioSystemError(.playerNotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,12 +445,12 @@ public final class AudioPlayer {
|
||||
fileStreamProcessor.fileStreamCallback = { [weak self] effect in
|
||||
guard let self = self else { return }
|
||||
switch effect {
|
||||
case .proccessSource:
|
||||
case .processSource:
|
||||
self.sourceQueue.async {
|
||||
self.processSource()
|
||||
}
|
||||
case let .raiseError(error):
|
||||
self.raiseUnxpected(error: error)
|
||||
self.raiseUnexpected(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -506,23 +523,6 @@ public final class AudioPlayer {
|
||||
Logger.debug("engine stopped 🛑", category: .generic)
|
||||
}
|
||||
|
||||
/// Starts the timer of `audioReadSource` for proccesing the source read stream
|
||||
///
|
||||
/// This calls `processSource` method every `500 ms`
|
||||
private func startReadProcessFromSourceIfNeeded() {
|
||||
guard audioReadSource.state != .activated else { return }
|
||||
audioReadSource.add { [weak self] in
|
||||
self?.processSource()
|
||||
}
|
||||
audioReadSource.activate()
|
||||
}
|
||||
|
||||
/// Stops and removes the handler from the timer, @see `audioReadSource`
|
||||
private func stopReadProccessFromSource() {
|
||||
audioReadSource.suspend()
|
||||
audioReadSource.removeHandler()
|
||||
}
|
||||
|
||||
/// Starts the audio player, reseting the buffers if requested
|
||||
///
|
||||
/// - parameter resetBuffers: A `Bool` value indicating if the buffers should be reset, prior starting the player.
|
||||
@@ -536,7 +536,7 @@ public final class AudioPlayer {
|
||||
try player.auAudioUnit.startHardware()
|
||||
} catch {
|
||||
stopEngine(reason: .error)
|
||||
raiseUnxpected(error: .audioSystemError(.playerStartError))
|
||||
raiseUnexpected(error: .audioSystemError(.playerStartError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +544,6 @@ public final class AudioPlayer {
|
||||
private func processSource() {
|
||||
dispatchPrecondition(condition: .onQueue(sourceQueue))
|
||||
|
||||
guard !playerContext.disposedRequested else { return }
|
||||
guard playerContext.internalState != .paused else { return }
|
||||
|
||||
if playerContext.internalState == .pendingNext {
|
||||
@@ -581,7 +580,6 @@ public final class AudioPlayer {
|
||||
setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: false)
|
||||
} else if playerContext.audioPlayingEntry == nil {
|
||||
if playerContext.internalState != .stopped {
|
||||
stopReadProccessFromSource()
|
||||
stopEngine(reason: .eof)
|
||||
}
|
||||
}
|
||||
@@ -597,7 +595,7 @@ public final class AudioPlayer {
|
||||
playingEntry.seekRequest.lock.unlock()
|
||||
|
||||
if originalSeekToTimeRequested, playerContext.audioReadingEntry === playingEntry {
|
||||
proccessSeekTime()
|
||||
processSeekTime()
|
||||
|
||||
let version = playingEntry.seekRequest.version.value
|
||||
if currSeekVersion == version {
|
||||
@@ -609,7 +607,7 @@ public final class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
private func proccessSeekTime() {
|
||||
private func processSeekTime() {
|
||||
assert(playerContext.audioReadingEntry === playerContext.audioPlayingEntry,
|
||||
"reading and playing entry must be the same")
|
||||
fileStreamProcessor.processSeek()
|
||||
@@ -724,7 +722,7 @@ public final class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
private func raiseUnxpected(error: AudioPlayerError) {
|
||||
private func raiseUnexpected(error: AudioPlayerError) {
|
||||
playerContext.setInternalState(to: .error)
|
||||
// todo raise on main thread from playback thread
|
||||
asyncOnMain { [weak self] in
|
||||
@@ -745,7 +743,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
||||
let openFileStreamStatus = fileStreamProcessor.openFileStream(with: source.audioFileHint)
|
||||
guard openFileStreamStatus == noErr else {
|
||||
let streamError = AudioFileStreamError(status: openFileStreamStatus)
|
||||
raiseUnxpected(error: .audioSystemError(.fileStreamError(streamError)))
|
||||
raiseUnexpected(error: .audioSystemError(.fileStreamError(streamError)))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -755,7 +753,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
||||
guard streamBytesStatus == noErr else {
|
||||
if let playingEntry = playerContext.audioPlayingEntry, playingEntry.has(same: source) {
|
||||
let streamBytesError = AudioFileStreamError(status: streamBytesStatus)
|
||||
raiseUnxpected(error: .streamParseBytesFailure(streamBytesError))
|
||||
raiseUnexpected(error: .streamParseBytesFailure(streamBytesError))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -766,12 +764,12 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func errorOccured(source: CoreAudioStreamSource, error: Error) {
|
||||
func errorOccurred(source: CoreAudioStreamSource, error: Error) {
|
||||
guard let entry = playerContext.audioReadingEntry, entry.has(same: source) else { return }
|
||||
raiseUnxpected(error: .networkError(.failure(error)))
|
||||
raiseUnexpected(error: .networkError(.failure(error)))
|
||||
}
|
||||
|
||||
func endOfFileOccured(source: CoreAudioStreamSource) {
|
||||
func endOfFileOccurred(source: CoreAudioStreamSource) {
|
||||
let hasSameSource = playerContext.audioReadingEntry?.has(same: source) ?? false
|
||||
guard playerContext.audioReadingEntry == nil || hasSameSource else {
|
||||
source.delegate = nil
|
||||
@@ -802,7 +800,10 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
||||
playerContext.audioReadingEntry = nil
|
||||
playerContext.entriesLock.unlock()
|
||||
|
||||
processSource()
|
||||
sourceQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.processSource()
|
||||
}
|
||||
}
|
||||
|
||||
func metadataReceived(data: [String: String]) {
|
||||
|
||||
@@ -6,30 +6,31 @@
|
||||
import Foundation
|
||||
|
||||
internal final class AudioPlayerContext {
|
||||
var stopReason = Protected<AudioPlayerStopReason>(.none)
|
||||
var stopReason: Protected<AudioPlayerStopReason>
|
||||
|
||||
var state = Protected<AudioPlayerState>(.ready)
|
||||
var state: Protected<AudioPlayerState>
|
||||
var stateChanged: ((_ oldState: AudioPlayerState, _ newState: AudioPlayerState) -> Void)?
|
||||
|
||||
var muted = Protected<Bool>(false)
|
||||
var muted: Protected<Bool>
|
||||
|
||||
var internalState: AudioPlayer.InternalState {
|
||||
playerInternalState.value
|
||||
}
|
||||
|
||||
let entriesLock = UnfairLock()
|
||||
let entriesLock: UnfairLock
|
||||
var audioReadingEntry: AudioEntry?
|
||||
var audioPlayingEntry: AudioEntry?
|
||||
|
||||
var disposedRequested: Bool
|
||||
|
||||
/// This is the player's internal state to use
|
||||
/// - NOTE: Do not use directly instead use the `internalState` to set and get the property
|
||||
/// or the `setInternalState(to:when:)`method
|
||||
private var playerInternalState = Protected<AudioPlayer.InternalState>(.initial)
|
||||
|
||||
init() {
|
||||
disposedRequested = false
|
||||
stopReason = Protected<AudioPlayerStopReason>(.none)
|
||||
state = Protected<AudioPlayerState>(.ready)
|
||||
muted = Protected<Bool>(false)
|
||||
entriesLock = UnfairLock()
|
||||
}
|
||||
|
||||
/// Sets the internal state if given the `inState` will be evaluated before assignment occurs.
|
||||
|
||||
@@ -22,7 +22,7 @@ public protocol AudioPlayerDelegate: AnyObject {
|
||||
stopReason: AudioPlayerStopReason,
|
||||
progress: Double,
|
||||
duration: Double)
|
||||
/// Tells the delegate when an unexpected error occured.
|
||||
/// Tells the delegate when an unexpected error occurred.
|
||||
/// - note: Probably a good time to recreate the player when this occurs
|
||||
func audioPlayerUnexpectedError(player: AudioPlayer, error: AudioPlayerError)
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ final class AudioRendererContext {
|
||||
|
||||
let packetsSemaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
var discontinuous: Bool = false
|
||||
|
||||
let framesRequiredToStartPlaying: UInt32
|
||||
let framesRequiredAfterRebuffering: UInt32
|
||||
let framesRequiredForDataAfterSeekPlaying: UInt32
|
||||
|
||||
@@ -9,7 +9,7 @@ import AVFoundation
|
||||
|
||||
enum AudioConvertStatus: Int32 {
|
||||
case done = 100
|
||||
case proccessed = 0
|
||||
case processed = 0
|
||||
}
|
||||
|
||||
struct AudioConvertInfo {
|
||||
@@ -20,11 +20,11 @@ struct AudioConvertInfo {
|
||||
}
|
||||
|
||||
enum FileStreamProcessorEffect {
|
||||
case proccessSource
|
||||
case processSource
|
||||
case raiseError(AudioPlayerError)
|
||||
}
|
||||
|
||||
/// An object that handles the proccessing of AudioFileStream, its packets etc.
|
||||
/// An object that handles the processing of AudioFileStream, its packets etc.
|
||||
final class AudioFileStreamProcessor {
|
||||
private let maxCompressedPacketForBitrate = 4096
|
||||
|
||||
@@ -38,8 +38,9 @@ final class AudioFileStreamProcessor {
|
||||
internal var audioConverter: AudioConverterRef?
|
||||
internal var discontinuous: Bool = false
|
||||
internal var inputFormat = AudioStreamBasicDescription()
|
||||
internal var fileFormat: String = ""
|
||||
internal let fa4mFormat = "fa4m"
|
||||
|
||||
internal var currentFileFormat: String = ""
|
||||
internal let fileFormatsForDelayedConverterCreation: Set = ["fa4m", "f4pm"]
|
||||
|
||||
var isFileStreamOpen: Bool {
|
||||
audioFileStream != nil
|
||||
@@ -165,7 +166,7 @@ final class AudioFileStreamProcessor {
|
||||
|
||||
var classDesc = AudioClassDescription()
|
||||
var outputFormat = toFormat
|
||||
if getHardwareCodecClassDescripition(formatId: inputFormat.mFormatID, classDesc: &classDesc) {
|
||||
if getHardwareCodecClassDescription(formatId: inputFormat.mFormatID, classDesc: &classDesc) {
|
||||
AudioConverterNewSpecific(&inputFormat, &outputFormat, 1, &classDesc, &audioConverter)
|
||||
}
|
||||
|
||||
@@ -178,11 +179,12 @@ final class AudioFileStreamProcessor {
|
||||
}
|
||||
}
|
||||
self.inputFormat = inputFormat
|
||||
assignMagicCookieToConverterIfNeeded()
|
||||
}
|
||||
|
||||
private func assignMagicCookieToConverterIfNeeded() {
|
||||
// magic cookie info
|
||||
let fileHint = playerContext.audioReadingEntry?.audioFileHint
|
||||
let isProperFormat = fileHint != kAudioFileAAC_ADTSType && fileHint != kAudioFileM4AType && fileHint != kAudioFileMPEG4Type
|
||||
if let fileStream = audioFileStream, isProperFormat {
|
||||
if let fileStream = audioFileStream {
|
||||
var cookieSize: UInt32 = 0
|
||||
guard AudioFileStreamGetPropertyInfo(fileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, nil) == noErr else {
|
||||
return
|
||||
@@ -263,7 +265,7 @@ final class AudioFileStreamProcessor {
|
||||
var size = UInt32(4)
|
||||
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_FileFormat, &size, &fileFormat)
|
||||
if let stringFileFormat = String(data: Data(fileFormat), encoding: .utf8) {
|
||||
self.fileFormat = stringFileFormat
|
||||
self.currentFileFormat = stringFileFormat
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +295,7 @@ final class AudioFileStreamProcessor {
|
||||
entry.processedPacketsState.bufferSize = packetBufferSize
|
||||
}
|
||||
|
||||
if fileFormat != fa4mFormat {
|
||||
if !fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
|
||||
createAudioConverter(from: entry.audioStreamFormat, to: outputAudioFormat)
|
||||
}
|
||||
}
|
||||
@@ -331,7 +333,7 @@ final class AudioFileStreamProcessor {
|
||||
i += step
|
||||
}
|
||||
|
||||
if fileFormat == fa4mFormat {
|
||||
if fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
|
||||
if let inputStreamFormat = playerContext.audioReadingEntry?.audioStreamFormat {
|
||||
createAudioConverter(from: inputStreamFormat, to: outputAudioFormat)
|
||||
}
|
||||
@@ -346,12 +348,12 @@ final class AudioFileStreamProcessor {
|
||||
inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?)
|
||||
{
|
||||
guard let entry = playerContext.audioReadingEntry else { return }
|
||||
guard entry.audioStreamState.processedDataFormat, !playerContext.disposedRequested else { return }
|
||||
guard entry.audioStreamState.processedDataFormat else { return }
|
||||
|
||||
if let playingEntry = playerContext.audioPlayingEntry,
|
||||
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
||||
{
|
||||
fileStreamCallback?(.proccessSource)
|
||||
fileStreamCallback?(.processSource)
|
||||
if rendererContext.waiting.value {
|
||||
rendererContext.packetsSemaphore.signal()
|
||||
}
|
||||
@@ -375,11 +377,11 @@ final class AudioFileStreamProcessor {
|
||||
convertInfo.audioBuffer.mNumberChannels = playingAudioStreamFormat.mChannelsPerFrame
|
||||
}
|
||||
|
||||
updateProccessedPackets(inPacketDescriptions: inPacketDescriptions,
|
||||
updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
|
||||
inNumberPackets: inNumberPackets)
|
||||
|
||||
var status: OSStatus = noErr
|
||||
packetProccess: while status == noErr {
|
||||
packetProcess: while status == noErr {
|
||||
rendererContext.lock.lock()
|
||||
let bufferContext = rendererContext.bufferContext
|
||||
var used = bufferContext.frameUsedCount
|
||||
@@ -401,8 +403,7 @@ final class AudioFileStreamProcessor {
|
||||
if framesLeftInBuffer > 0 {
|
||||
break
|
||||
}
|
||||
if playerContext.disposedRequested
|
||||
|| playerContext.internalState == .disposed
|
||||
if playerContext.internalState == .disposed
|
||||
|| playerContext.internalState == .pendingNext
|
||||
|| playerContext.internalState == .stopped
|
||||
{
|
||||
@@ -412,7 +413,7 @@ final class AudioFileStreamProcessor {
|
||||
if let playingEntry = playerContext.audioPlayingEntry,
|
||||
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
||||
{
|
||||
fileStreamCallback?(.proccessSource)
|
||||
fileStreamCallback?(.processSource)
|
||||
if rendererContext.waiting.value {
|
||||
rendererContext.packetsSemaphore.signal()
|
||||
}
|
||||
@@ -457,7 +458,7 @@ final class AudioFileStreamProcessor {
|
||||
framesToDecode = start
|
||||
if framesToDecode == 0 {
|
||||
fillUsedFrames(framesCount: framesAdded)
|
||||
continue packetProccess
|
||||
continue packetProcess
|
||||
}
|
||||
prefillLocalBufferList(bufferList: localBufferList,
|
||||
dataOffset: 0,
|
||||
@@ -475,9 +476,9 @@ final class AudioFileStreamProcessor {
|
||||
if status == AudioConvertStatus.done.rawValue {
|
||||
fillUsedFrames(framesCount: framesAdded)
|
||||
return
|
||||
} else if status == AudioConvertStatus.proccessed.rawValue {
|
||||
} else if status == AudioConvertStatus.processed.rawValue {
|
||||
fillUsedFrames(framesCount: framesAdded)
|
||||
continue packetProccess
|
||||
continue packetProcess
|
||||
} else if status != 0 {
|
||||
fileStreamCallback?(.raiseError(.codecError))
|
||||
return
|
||||
@@ -502,9 +503,9 @@ final class AudioFileStreamProcessor {
|
||||
if status == AudioConvertStatus.done.rawValue {
|
||||
fillUsedFrames(framesCount: framesAdded)
|
||||
return
|
||||
} else if status == AudioConvertStatus.proccessed.rawValue {
|
||||
} else if status == AudioConvertStatus.processed.rawValue {
|
||||
fillUsedFrames(framesCount: framesAdded)
|
||||
continue packetProccess
|
||||
continue packetProcess
|
||||
} else if status != 0 {
|
||||
fileStreamCallback?(.raiseError(.codecError))
|
||||
return
|
||||
@@ -545,8 +546,8 @@ final class AudioFileStreamProcessor {
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
private func updateProccessedPackets(inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?,
|
||||
inNumberPackets: UInt32)
|
||||
private func updateProcessedPackets(inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?,
|
||||
inNumberPackets: UInt32)
|
||||
{
|
||||
guard let inPacketDescriptions = inPacketDescriptions else { return }
|
||||
guard let readingEntry = playerContext.audioReadingEntry else { return }
|
||||
@@ -618,12 +619,12 @@ private func _converterCallback(inAudioConverter _: AudioConverterRef,
|
||||
ioNumberDataPackets.pointee = convertInfo.pointee.numberOfPackets
|
||||
convertInfo.pointee.done = true
|
||||
|
||||
return AudioConvertStatus.proccessed.rawValue
|
||||
return AudioConvertStatus.processed.rawValue
|
||||
}
|
||||
|
||||
// MARK: HardwareCodedClass method
|
||||
|
||||
private func getHardwareCodecClassDescripition(formatId: UInt32, classDesc: UnsafeMutablePointer<AudioClassDescription>) -> Bool {
|
||||
private func getHardwareCodecClassDescription(formatId: UInt32, classDesc: UnsafeMutablePointer<AudioClassDescription>) -> Bool {
|
||||
#if os(iOS)
|
||||
var size: UInt32 = 0
|
||||
let formatIdSize = UInt32(MemoryLayout.size(ofValue: formatId))
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//
|
||||
// Created by Dimitrios C on 19/05/2021.
|
||||
// Copyright © 2021 Decimal. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
|
||||
///
|
||||
/// - parameter buffer: A buffer of audio captured from the output of an AVAudioNode.
|
||||
/// - parameter when: The time the buffer was captured.
|
||||
///
|
||||
public typealias FilterCallback = (_ buffer: AVAudioPCMBuffer,
|
||||
_ when: AVAudioTime) -> Void
|
||||
|
||||
/// A value type whose instances are used for frame filter
|
||||
/// - Note:
|
||||
/// The filter block will be called from a thread other than the main thread
|
||||
public struct FilterEntry: Equatable {
|
||||
/// A string value indicating the name of the filter
|
||||
public let name: String
|
||||
|
||||
/// A block in which you apply any filtering
|
||||
public let filter: FilterCallback
|
||||
|
||||
public init(name: String, filter: @escaping FilterCallback) {
|
||||
self.name = name
|
||||
self.filter = filter
|
||||
}
|
||||
|
||||
public static func == (lhs: FilterEntry, rhs: FilterEntry) -> Bool {
|
||||
lhs.name == rhs.name
|
||||
}
|
||||
}
|
||||
|
||||
public protocol FrameFiltering {
|
||||
|
||||
/// A Boolean value indicating whether there are filter entries
|
||||
var hasEntries: Bool { get }
|
||||
|
||||
/// Adds a filter entry at the end of the queue
|
||||
/// - Parameter entry: An instance of `FilterEntry`
|
||||
func add(entry: FilterEntry)
|
||||
|
||||
/// Adds a filter entry after the specified name of another entry
|
||||
/// - Parameters:
|
||||
/// - entry: An instance of `FilterEntry`
|
||||
/// - named: The name of a previously added filter
|
||||
func add(entry: FilterEntry, afterEntry named: String)
|
||||
|
||||
/// Adds a filter entry with the given parameters
|
||||
/// - Parameters:
|
||||
/// - named: The name of the entry to be added
|
||||
/// - filter: The block for the filter hanlding
|
||||
func add(entry named: String, filter: @escaping FilterCallback)
|
||||
|
||||
/// Adds a filter entry with the given parameters
|
||||
/// - Parameters:
|
||||
/// - name: The name for the new entry
|
||||
/// - filterName: The name of a previously added filters
|
||||
/// - filter: The block for the filter hanlding
|
||||
func add(entry named: String, after filterName: String, filter: @escaping FilterCallback)
|
||||
|
||||
/// Removes a filter entry
|
||||
/// - Parameter entry: An instance of `FilterEntry` to be removed
|
||||
func remove(entry: FilterEntry)
|
||||
|
||||
/// Attemps to remove a filter entry by its name
|
||||
/// - Parameter named: A `String` representing the name of the filter entry
|
||||
func remove(entry named: String)
|
||||
|
||||
/// Removes all filter entries
|
||||
func removeAll()
|
||||
}
|
||||
|
||||
final class FrameFilterProcessor: NSObject, FrameFiltering {
|
||||
|
||||
public var hasEntries: Bool {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
return !entries.isEmpty
|
||||
}
|
||||
|
||||
private let lock = UnfairLock()
|
||||
private let mixerNode: AVAudioMixerNode
|
||||
|
||||
private(set) var entries: [FilterEntry] = []
|
||||
|
||||
private var hasInstalledTap: Bool = false
|
||||
|
||||
init(mixerNode: AVAudioMixerNode) {
|
||||
self.mixerNode = mixerNode
|
||||
}
|
||||
|
||||
public func add(entry: FilterEntry) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
entries.append(entry)
|
||||
installTapIfNeeded()
|
||||
}
|
||||
|
||||
public func add(entry: FilterEntry, afterEntry named: String) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
guard let entryIndex = entries.firstIndex(where: { $0.name == named }) else {
|
||||
return
|
||||
}
|
||||
if entryIndex.advanced(by: 1) > entries.count {
|
||||
entries.append(entry)
|
||||
} else {
|
||||
entries.insert(entry, at: entryIndex + 1)
|
||||
}
|
||||
installTapIfNeeded()
|
||||
}
|
||||
|
||||
public func add(entry named: String, filter: @escaping FilterCallback) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
entries.append(FilterEntry(name: named, filter: filter))
|
||||
installTapIfNeeded()
|
||||
}
|
||||
|
||||
func add(entry named: String, after filterName: String, filter: @escaping FilterCallback) {
|
||||
let entry = FilterEntry(name: named, filter: filter)
|
||||
add(entry: entry, afterEntry: filterName)
|
||||
}
|
||||
|
||||
public func remove(entry: FilterEntry) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
guard let entryIndex = entries.firstIndex(where: { $0 == entry }) else {
|
||||
return
|
||||
}
|
||||
entries.remove(at: entryIndex)
|
||||
if entries.isEmpty {
|
||||
removeTap()
|
||||
}
|
||||
}
|
||||
|
||||
public func remove(entry named: String) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
guard let entryIndex = entries.firstIndex(where: { $0.name == named }) else {
|
||||
return
|
||||
}
|
||||
entries.remove(at: entryIndex)
|
||||
if entries.isEmpty {
|
||||
removeTap()
|
||||
}
|
||||
}
|
||||
|
||||
public func removeAll() {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
entries.removeAll()
|
||||
removeTap()
|
||||
}
|
||||
|
||||
private func process(buffer: AVAudioPCMBuffer, when: AVAudioTime) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
guard !entries.isEmpty else { return }
|
||||
for entry in entries {
|
||||
entry.filter(buffer, when)
|
||||
}
|
||||
}
|
||||
|
||||
private func installTapIfNeeded() {
|
||||
guard !hasInstalledTap else { return }
|
||||
hasInstalledTap = true
|
||||
let format = mixerNode.outputFormat(forBus: 0)
|
||||
mixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, when in
|
||||
guard let self = self else { return }
|
||||
guard self.hasEntries else { return }
|
||||
self.process(
|
||||
buffer: buffer,
|
||||
when: when
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeTap() {
|
||||
hasInstalledTap = false
|
||||
mixerNode.removeTap(onBus: 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// IcycastHeadersProcessor.swift
|
||||
// AudioStreaming
|
||||
//
|
||||
// Created by Dimitrios C on 14/02/2021.
|
||||
// Copyright © 2021 Decimal. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// ICY is built on HTTP some old servers might still send headers in the stream.
|
||||
/// From a server point of view, this should be considered deprecated and should not be used as it might break HTML5 compatibility.
|
||||
/// Although there are some servers still using this, this class will extract those headers from the stream
|
||||
///
|
||||
/// The format of the headers is as follows:
|
||||
/// ```
|
||||
/// =================================================================
|
||||
/// [ ICY 200 OK ]
|
||||
/// [ icy-mentaint: the number of bytes between 2 metadata chunks ]
|
||||
/// [ icy-br: send the bitrate in kilobits per second ]
|
||||
/// [ icy-genre: sends the genre ]
|
||||
/// [ icy-name: sends the stream's name ]
|
||||
/// [ icy-url: is the URL of the radio station ]
|
||||
/// [ icy-pub: can be 1 or 0 to tell if it is listed or not ]
|
||||
/// =================================================================
|
||||
/// ```
|
||||
|
||||
final class IcycastHeadersProcessor {
|
||||
|
||||
private var icecastHeaders = Data(capacity: 1024)
|
||||
private var searchComplete = false
|
||||
private var iceHeaderAvailable = false
|
||||
|
||||
func reset() {
|
||||
icecastHeaders = Data(capacity: 1024)
|
||||
searchComplete = false
|
||||
iceHeaderAvailable = false
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
func process(data: Data) -> (Data?, Data) {
|
||||
let stopProcessingCheckOne: [UInt8] = Array("\n\n".utf8)
|
||||
let stopProcessingCheckTwo: [UInt8] = Array("\r\n\r\n".utf8)
|
||||
let icyPrefix: [UInt8] = Array("ICY ".utf8)
|
||||
let httpPrefix: [UInt8] = Array("HTTP".utf8)
|
||||
return data.withUnsafeBytes { buffer -> (Data?, Data) in
|
||||
guard !buffer.isEmpty else { return (nil, data) }
|
||||
var bytesRead = 0
|
||||
let bytes = buffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
// Read through the bytes and stop when our search is complete
|
||||
// Since we don't know the amount of bytes to be processed
|
||||
// we add each character up until we found on of the checks as defined above.
|
||||
while bytesRead < buffer.count, !searchComplete {
|
||||
let pointer = bytes + bytesRead
|
||||
icecastHeaders.append(pointer, count: 1)
|
||||
|
||||
if icecastHeaders.count >= stopProcessingCheckOne.count {
|
||||
if icecastHeaders.suffix(stopProcessingCheckOne.count) == stopProcessingCheckOne {
|
||||
iceHeaderAvailable = true
|
||||
searchComplete = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if icecastHeaders.count >= stopProcessingCheckTwo.count {
|
||||
if icecastHeaders.suffix(stopProcessingCheckTwo.count) == stopProcessingCheckTwo {
|
||||
iceHeaderAvailable = true
|
||||
searchComplete = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if icecastHeaders.count >= icyPrefix.count {
|
||||
// in case the first 4 chars are not "ICY " nor "HTTP" then we stop the flow
|
||||
if icecastHeaders[..<icyPrefix.count].elementsEqual(icyPrefix) == false &&
|
||||
icecastHeaders[..<httpPrefix.count].elementsEqual(httpPrefix) == false {
|
||||
iceHeaderAvailable = false
|
||||
searchComplete = true
|
||||
}
|
||||
}
|
||||
|
||||
bytesRead += 1
|
||||
}
|
||||
if !iceHeaderAvailable {
|
||||
return (nil, data)
|
||||
}
|
||||
let extractedAudio = data[icecastHeaders.count...]
|
||||
iceHeaderAvailable = false
|
||||
return (icecastHeaders, extractedAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,15 @@ protocol MetadataStreamSource {
|
||||
var delegate: MetadataStreamSourceDelegate? { get set }
|
||||
|
||||
/// Returns `true` when the stream header has indicated that we can proccess metadata, otherwise `false`.
|
||||
var canProccessMetadata: Bool { get }
|
||||
var canProcessMetadata: Bool { get }
|
||||
|
||||
/// Assigns the metadata step of the metadata
|
||||
func metadataAvailable(step: Int)
|
||||
|
||||
/// Proccess the received data and extract the metadata if any, returns audio data only.
|
||||
/// Process the received data and extract the metadata if any, returns audio data only.
|
||||
/// - parameter data: A `Data` object for parsing any metadata
|
||||
/// - returns: The extracted audio `Data`
|
||||
func proccessMetadata(data: Data) -> Data
|
||||
func processMetadata(data: Data) -> Data
|
||||
|
||||
/// Resets the processor
|
||||
func reset()
|
||||
@@ -44,7 +44,7 @@ protocol MetadataStreamSource {
|
||||
final class MetadataStreamProcessor: MetadataStreamSource {
|
||||
weak var delegate: MetadataStreamSourceDelegate?
|
||||
|
||||
var canProccessMetadata: Bool {
|
||||
var canProcessMetadata: Bool {
|
||||
return metadataStep > 0
|
||||
}
|
||||
|
||||
@@ -73,10 +73,10 @@ final class MetadataStreamProcessor: MetadataStreamSource {
|
||||
audioDataBytesRead = 0
|
||||
}
|
||||
|
||||
// MARK: Proccess Metadata
|
||||
// MARK: Process Metadata
|
||||
|
||||
@inline(__always)
|
||||
func proccessMetadata(data: Data) -> Data {
|
||||
func processMetadata(data: Data) -> Data {
|
||||
data.withUnsafeBytes { buffer -> Data in
|
||||
guard !buffer.isEmpty else { return data }
|
||||
var audioData = Data()
|
||||
|
||||
@@ -14,7 +14,7 @@ struct HeaderField {
|
||||
}
|
||||
|
||||
enum IcyHeaderField {
|
||||
public static let icyMentaint = "icy-metaint"
|
||||
public static let icyMetaint = "icy-metaint"
|
||||
}
|
||||
|
||||
struct HTTPHeaderParserOutput {
|
||||
@@ -39,7 +39,7 @@ struct HTTPHeaderParser: HTTPHeaderParsing {
|
||||
typealias Output = HTTPHeaderParserOutput?
|
||||
|
||||
func parse(input: HTTPURLResponse) -> HTTPHeaderParserOutput? {
|
||||
guard let headers = input.allHeaderFields as? [String: String], !headers.isEmpty else { return nil }
|
||||
guard let headers = input.allHeaderFields as? [String: String], headers.count > 2 else { return nil }
|
||||
|
||||
var typeId: UInt32 = 0
|
||||
if let contentType = input.mimeType {
|
||||
@@ -64,7 +64,7 @@ struct HTTPHeaderParser: HTTPHeaderParsing {
|
||||
}
|
||||
|
||||
var metadataStep = 0
|
||||
if let icyMetaint = value(forHTTPHeaderField: IcyHeaderField.icyMentaint, in: input),
|
||||
if let icyMetaint = value(forHTTPHeaderField: IcyHeaderField.icyMetaint, in: input),
|
||||
let intValue = Int(icyMetaint)
|
||||
{
|
||||
metadataStep = intValue
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// IcycastHeaderParser.swift
|
||||
// AudioStreaming
|
||||
//
|
||||
// Created by Dimitrios C on 14/02/2021.
|
||||
// Copyright © 2021 Decimal. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct IcycastHeaderParser: Parser {
|
||||
|
||||
func parse(input: Data) -> HTTPHeaderParserOutput? {
|
||||
|
||||
guard let icecastValue = String(data: input, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
let headers = icecastValue.components(separatedBy: CharacterSet(charactersIn: "\r\n"))
|
||||
var result = [String: String]()
|
||||
for header in headers where !header.isEmpty {
|
||||
let values = header.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true)
|
||||
if let key = values.first, let value = values.last {
|
||||
result[String(key)] = String(value)
|
||||
}
|
||||
}
|
||||
let metadataStep = Int(result[IcyHeaderField.icyMetaint] ?? "") ?? 0
|
||||
let contentType = result[HeaderField.contentType.lowercased()] ?? "audio/mpeg"
|
||||
let typeId = audioFileType(mimeType: contentType)
|
||||
|
||||
return HTTPHeaderParserOutput(fileLength: 0,
|
||||
typeId: typeId,
|
||||
metadataStep: metadataStep)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ struct MetadataParser: Parser {
|
||||
|
||||
func parse(input: Data) -> MetadataOutput {
|
||||
guard let string = String(data: input, encoding: .utf8) else { return .failure(.unableToParse) }
|
||||
// remove added bytes (zeros) and seperate the string on every ';' char
|
||||
// remove added bytes (zeros) and separate the string on every ';' char
|
||||
let pairs = string.trimmingCharacters(in: CharacterSet(charactersIn: "\0")).components(separatedBy: ";")
|
||||
let temp: [String: String] = [:]
|
||||
let metadata = pairs.reduce(into: temp) { result, next in
|
||||
|
||||
+8
-8
@@ -18,19 +18,19 @@ class MetadataStreamProcessorTests: XCTestCase {
|
||||
let processor = MetadataStreamProcessor(parser: parser.eraseToAnyParser())
|
||||
|
||||
// without calling `metadataAvailable(step:)` it should be false
|
||||
XCTAssertFalse(processor.canProccessMetadata)
|
||||
XCTAssertFalse(processor.canProcessMetadata)
|
||||
|
||||
// calling `metadataAvailable(step:)` with zero
|
||||
processor.metadataAvailable(step: 0)
|
||||
|
||||
// it should be false
|
||||
XCTAssertFalse(processor.canProccessMetadata)
|
||||
XCTAssertFalse(processor.canProcessMetadata)
|
||||
|
||||
// calling `metadataAvailable(step:)` with greater zero
|
||||
processor.metadataAvailable(step: 1)
|
||||
|
||||
// it should be true
|
||||
XCTAssertTrue(processor.canProccessMetadata)
|
||||
XCTAssertTrue(processor.canProcessMetadata)
|
||||
}
|
||||
|
||||
func test_Processor_Outputs_Correct_Metadata_ForStep_WithEmptyMetadata() throws {
|
||||
@@ -45,7 +45,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
||||
// this is the step value as received from the http headers
|
||||
processor.metadataAvailable(step: 16000)
|
||||
|
||||
let audio = processor.proccessMetadata(data: data)
|
||||
let audio = processor.processMetadata(data: data)
|
||||
XCTAssertFalse(audio.isEmpty)
|
||||
|
||||
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
||||
@@ -64,7 +64,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
||||
// this is the step value as received from the http headers
|
||||
processor.metadataAvailable(step: 16000)
|
||||
|
||||
let audio = processor.proccessMetadata(data: data)
|
||||
let audio = processor.processMetadata(data: data)
|
||||
XCTAssertFalse(audio.isEmpty)
|
||||
|
||||
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
||||
@@ -83,7 +83,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
||||
// this is the step value as received from the http headers
|
||||
processor.metadataAvailable(step: 8000)
|
||||
|
||||
let audio = processor.proccessMetadata(data: data)
|
||||
let audio = processor.processMetadata(data: data)
|
||||
XCTAssertFalse(audio.isEmpty)
|
||||
|
||||
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
||||
@@ -106,7 +106,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
||||
// this is the step value as received from the http headers
|
||||
processor.metadataAvailable(step: 16000)
|
||||
|
||||
let audio = processor.proccessMetadata(data: data)
|
||||
let audio = processor.processMetadata(data: data)
|
||||
XCTAssertFalse(audio.isEmpty)
|
||||
|
||||
XCTAssertFalse(metadataDelegateSpy.receivedMetadata.called)
|
||||
@@ -122,7 +122,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
||||
// this is the step value as received from the http headers
|
||||
processor.metadataAvailable(step: 16000)
|
||||
|
||||
let audio = processor.proccessMetadata(data: data)
|
||||
let audio = processor.processMetadata(data: data)
|
||||
XCTAssertTrue(audio.isEmpty)
|
||||
|
||||
XCTAssertFalse(metadataDelegateSpy.receivedMetadata.called)
|
||||
|
||||
@@ -34,7 +34,7 @@ class HTTPHeaderParserTests: XCTestCase {
|
||||
let headers: [String: String] =
|
||||
[HeaderField.contentLength: "1000",
|
||||
HeaderField.contentType: "audio/mp3",
|
||||
IcyHeaderField.icyMentaint: "16000"]
|
||||
IcyHeaderField.icyMetaint: "16000"]
|
||||
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
|
||||
statusCode: 200,
|
||||
httpVersion: "",
|
||||
@@ -57,7 +57,7 @@ class HTTPHeaderParserTests: XCTestCase {
|
||||
let headers: [String: String] =
|
||||
[HeaderField.contentLength.lowercased(): "1000",
|
||||
HeaderField.contentType.lowercased(): "audio/mp3",
|
||||
IcyHeaderField.icyMentaint.lowercased(): "16000"]
|
||||
IcyHeaderField.icyMetaint.lowercased(): "16000"]
|
||||
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
|
||||
statusCode: 200,
|
||||
httpVersion: "",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# AudioStreaming
|
||||
An AudioPlayer/Streaming library for iOS written in Swift, allows playback of online audio streaming, local file as well as gapless queueing.
|
||||
|
||||
Under the hood `AudioStreaming` uses `AVAudioEngine` and `CoreAudio` for playback and provides an easy way of applying real-time [audio enhancements](https://developer.apple.com/documentation/avfoundation/audio_playback_recording_and_processing/avaudioengine/audio_units?language=swift).
|
||||
Under the hood `AudioStreaming` uses `AVAudioEngine` and `CoreAudio` for playback and provides an easy way of applying real-time [audio enhancements](https://developer.apple.com/documentation/avfaudio/audio_engine/audio_units).
|
||||
|
||||
#### Supported audio
|
||||
- Online streaming (Shoutcast/ICY streams) with metadata parsing
|
||||
@@ -124,6 +124,41 @@ player.detachCustomAttachedNodes()
|
||||
|
||||
The example project shows an example of adding a custom `AVAudioUnitEQ` node for adding equaliser to the `AudioPlayer`
|
||||
|
||||
### Adding custom frame filter for recording and observation of audio data
|
||||
|
||||
`AudioStreaming` allow for custom frame fliters to be added so that recording or other observation for audio that's playing.
|
||||
|
||||
You add a frame filter by using the `AudioPlayer`'s property `frameFiltering`.
|
||||
|
||||
```
|
||||
let player = AudioPlayer()
|
||||
let format = player.mainMixerNode.outputFormat(forBus: 0)
|
||||
|
||||
let settings = [
|
||||
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
||||
AVSampleRateKey: format.sampleRate,
|
||||
AVNumberOfChannelsKey: format.channelCount
|
||||
] as [String : Any]
|
||||
|
||||
var audioFile = try? AVAudioFile(
|
||||
forWriting: outputUrl,
|
||||
settings: settings,
|
||||
commonFormat: format.commonFormat,
|
||||
interleaved: format.isInterleaved)
|
||||
|
||||
let record = FilterEntry(name: "record") { buffer, when in
|
||||
try? audioFile?.write(from: buffer)
|
||||
}
|
||||
|
||||
player.frameFiltering.add(entry: record)
|
||||
```
|
||||
See the `FrameFiltering` protocol for more ways of adding and removing frame filters.
|
||||
The callback in which you observe a filter will be run on a thread other than the main thread.
|
||||
|
||||
Under the hood the concrete class for frame filters, `FrameFilterProcessor` installs a tap on the `mainMixerNode` of `AVAudioEngine` in which all the added fitler will be called from.
|
||||
|
||||
**Note** since the `mainMixerNode` is publicly exposed extra care should be taken to not install a tap directly and also use frame filters, this result in an exception because only one tap can be installed on an output node, as per Apple's documention.
|
||||
|
||||
# Installation
|
||||
|
||||
### Cocoapods
|
||||
|
||||
Reference in New Issue
Block a user