Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 566dc86f3f | |||
| d8aa58525c | |||
| 8197db0016 | |||
| c2aee1669b | |||
| 334be32bf9 | |||
| a2da46f85b | |||
| aca69debd1 |
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '0.3.0'
|
||||
s.version = '0.5.1'
|
||||
s.license = 'MIT'
|
||||
s.summary = 'An AudioPlayer/Streaming library for iOS written in Swift using AVAudioEngine.'
|
||||
s.homepage = 'https://github.com/dimitris-c/AudioStreaming'
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
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, ); }; };
|
||||
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 */; };
|
||||
@@ -143,6 +145,8 @@
|
||||
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>"; };
|
||||
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 +209,7 @@
|
||||
B55CEAB32485107C0001C498 /* Parser.swift */,
|
||||
B55A736B247FCB420050C53D /* HTTPHeaderParser.swift */,
|
||||
B55CE96D248058B60001C498 /* MetadataParser.swift */,
|
||||
B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */,
|
||||
);
|
||||
path = Parsers;
|
||||
sourceTree = "<group>";
|
||||
@@ -257,6 +262,7 @@
|
||||
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */,
|
||||
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */,
|
||||
B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */,
|
||||
B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */,
|
||||
);
|
||||
path = Processors;
|
||||
sourceTree = "<group>";
|
||||
@@ -594,6 +600,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,6 +611,7 @@
|
||||
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 */,
|
||||
@@ -786,6 +794,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 +807,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = 0.5.1;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
@@ -816,6 +825,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 +838,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = 0.5.1;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +85,11 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
let metadataParser = MetadataParser()
|
||||
let metadataProcessor = MetadataStreamProcessor(parser: metadataParser.eraseToAnyParser())
|
||||
let netStatusProvider = NetStatusService(network: NWPathMonitor())
|
||||
let icyheaderProcessor = IcycastHeadersProcessor()
|
||||
let retrierTimout = Retrier(interval: .seconds(1), maxInterval: 5, underlyingQueue: nil)
|
||||
self.init(networking: networking,
|
||||
metadataStreamSource: metadataProcessor,
|
||||
icycastHeadersProcessor: icyheaderProcessor,
|
||||
netStatusProvider: netStatusProvider,
|
||||
retrier: retrierTimout,
|
||||
url: url,
|
||||
@@ -124,6 +131,8 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
|
||||
retrierTimeout.cancel()
|
||||
metadataStreamProcessor.reset()
|
||||
icycastHeadersProcessor.reset()
|
||||
shouldTryParsingIcycastHeaders = false
|
||||
|
||||
performOpen(seek: offset)
|
||||
}
|
||||
@@ -189,16 +198,26 @@ 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.proccess(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 +230,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.canProccessMetadata {
|
||||
let extractedAudioData = self.metadataStreamProcessor.proccessMetadata(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,11 +264,15 @@ 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 {
|
||||
} else if statusCode >= 300 {
|
||||
delegate?.errorOccured(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 {
|
||||
@@ -95,25 +95,30 @@ 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()
|
||||
private 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 }
|
||||
public var isEngineRunning: Bool { audioEngine.isRunning }
|
||||
|
||||
/// The `AVAudioMixerNode` as created by the underlying audio engine
|
||||
public var mainMixerNode: AVAudioMixerNode {
|
||||
audioEngine.mainMixerNode
|
||||
}
|
||||
|
||||
/// 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 audioReadSource: DispatchTimerSource
|
||||
private let serializationQueue: DispatchQueue
|
||||
@@ -306,6 +311,8 @@ public final class AudioPlayer {
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// 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 proccess(data: Data) -> (Data?, Data) {
|
||||
let stopProccessingCheckOne: [UInt8] = Array("\n\n".utf8)
|
||||
let stopProccessingCheckTwo: [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
|
||||
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 proccessed
|
||||
// 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 >= stopProccessingCheckOne.count {
|
||||
if icecastHeaders.suffix(stopProccessingCheckOne.count) == stopProccessingCheckOne {
|
||||
iceHeaderAvailable = true
|
||||
searchComplete = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if icecastHeaders.count >= stopProccessingCheckTwo.count {
|
||||
if icecastHeaders.suffix(stopProccessingCheckTwo.count) == stopProccessingCheckTwo {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.icyMentaint] ?? "") ?? 0
|
||||
let contentType = result[HeaderField.contentType.lowercased()] ?? "audio/mpeg"
|
||||
let typeId = audioFileType(mimeType: contentType)
|
||||
|
||||
return HTTPHeaderParserOutput(fileLength: 0,
|
||||
typeId: typeId,
|
||||
metadataStep: metadataStep)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user