Compare commits

...

4 Commits

Author SHA1 Message Date
Dimitris C c2aee1669b Bump version (#17)
Co-authored-by: Dimitrios C <dimitrisc@DimitrisC-Macbook-Pro.local>
2021-03-22 12:15:42 +02:00
Mushthak Ebrahim 334be32bf9 Fix header not get passed into method (#16) 2021-03-02 18:15:26 +02:00
Dimitris C a2da46f85b Bump version (#15) 2021-02-14 16:37:49 +02:00
Dimitris C aca69debd1 Adds support for Shoutcast headers in audio stream (#14)
* Adds support for Shoutcast headers in audio stream

* Renames proccessIcecastHeaders to process(data:

* Updates comment on IcycastHeadersProcessor
2021-02-14 16:33:37 +02:00
7 changed files with 191 additions and 15 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'AudioStreaming'
s.version = '0.3.0'
s.version = '0.5.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'
+10 -2
View File
@@ -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 */,
@@ -798,7 +806,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.3.0;
MARKETING_VERSION = 0.5.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -828,7 +836,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.3.0;
MARKETING_VERSION = 0.5.0;
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)
}
}
@@ -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)
}
}