Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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.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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user