Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e032d34ff7 | |||
| 280d3464c1 | |||
| f0811c4fc8 | |||
| 6c9ef18d4e | |||
| db8aa646da | |||
| c84f4d9d24 | |||
| 22e46114a6 | |||
| 38bdd32526 | |||
| 28fa4463e0 | |||
| abd8c91b46 |
@@ -55,7 +55,7 @@ class PlayerViewController: UIViewController {
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "PlaylistCell")
|
||||
tableView.register(PlaylistTableViewCell.self, forCellReuseIdentifier: "PlaylistCell")
|
||||
|
||||
let controlsController = controlsProvider()
|
||||
playerControlsController = controlsController
|
||||
@@ -120,6 +120,7 @@ extension PlayerViewController: UITableViewDataSource {
|
||||
return cell
|
||||
}
|
||||
cell.textLabel?.text = item.name
|
||||
cell.detailTextLabel?.text = item.queues ? "Queue item" : nil
|
||||
update(status: item.status, of: cell)
|
||||
return cell
|
||||
}
|
||||
@@ -147,3 +148,15 @@ extension PlayerViewController: UITableViewDelegate {
|
||||
viewModel.playItem(at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class PlaylistTableViewCell: UITableViewCell {
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ final class PlayerViewModel {
|
||||
print("malformed url error")
|
||||
return
|
||||
}
|
||||
playlistItemsService.add(item: PlaylistItem(url: url, name: urlString, status: .stopped))
|
||||
playlistItemsService.add(item: PlaylistItem(url: url, name: urlString, status: .stopped, queues: false))
|
||||
reloadContent?(.all)
|
||||
}
|
||||
|
||||
@@ -57,13 +57,20 @@ final class PlayerViewModel {
|
||||
|
||||
func playItem(at indexPath: IndexPath) {
|
||||
guard let item = item(at: indexPath) else { return }
|
||||
if let index = currentPlayingItemIndex {
|
||||
playlistItemsService.setStatus(for: index, status: .stopped)
|
||||
reloadContent?(.item(IndexPath(row: index, section: 0)))
|
||||
currentPlayingItemIndex = nil
|
||||
if item.queues {
|
||||
playerService.queue(url: item.url)
|
||||
if currentPlayingItemIndex == nil {
|
||||
currentPlayingItemIndex = indexPath.row
|
||||
}
|
||||
} else {
|
||||
if let index = currentPlayingItemIndex {
|
||||
playlistItemsService.setStatus(for: index, status: .stopped)
|
||||
reloadContent?(.item(IndexPath(row: index, section: 0)))
|
||||
currentPlayingItemIndex = nil
|
||||
}
|
||||
playerService.play(url: item.url)
|
||||
currentPlayingItemIndex = indexPath.row
|
||||
}
|
||||
playerService.play(url: item.url)
|
||||
currentPlayingItemIndex = indexPath.row
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ enum AudioContent: Int, CaseIterable {
|
||||
case offradio
|
||||
case enlefko
|
||||
case pepper966
|
||||
case kosmos
|
||||
case radiox
|
||||
case khruangbin
|
||||
case piano
|
||||
@@ -26,6 +27,8 @@ enum AudioContent: Int, CaseIterable {
|
||||
return "Enlefko (stream)"
|
||||
case .pepper966:
|
||||
return "Pepper 96.6 (stream)"
|
||||
case .kosmos:
|
||||
return "Kosmos 93.6 (stream)"
|
||||
case .radiox:
|
||||
return "Radio X (stream)"
|
||||
case .khruangbin:
|
||||
@@ -47,6 +50,8 @@ enum AudioContent: Int, CaseIterable {
|
||||
return URL(string: "https://s3.yesstreaming.net:17062/stream")!
|
||||
case .pepper966:
|
||||
return URL(string: "https://ample-09.radiojar.com/pepper.m4a?1593699983=&rj-tok=AAABcw_1KyMAIViq2XpI098ZSQ&rj-ttl=5")!
|
||||
case .kosmos:
|
||||
return URL(string: "https://radiostreaming.ert.gr/ert-kosmos")!
|
||||
case .radiox:
|
||||
return URL(string: "https://media-ssl.musicradio.com/RadioXLondon")!
|
||||
case .khruangbin:
|
||||
|
||||
@@ -56,6 +56,11 @@ final class AudioPlayerService {
|
||||
player.play(url: url)
|
||||
}
|
||||
|
||||
func queue(url: URL) {
|
||||
activateAudioSession()
|
||||
player.queue(url: url)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
player.stop()
|
||||
deactivateAudioSession()
|
||||
|
||||
@@ -19,17 +19,20 @@ struct PlaylistItem: Equatable {
|
||||
let url: URL
|
||||
let name: String
|
||||
let status: Status
|
||||
let queues: Bool
|
||||
|
||||
init(content: AudioContent) {
|
||||
init(content: AudioContent, queues: Bool) {
|
||||
name = content.title
|
||||
url = content.streamUrl
|
||||
status = .stopped
|
||||
self.queues = queues
|
||||
}
|
||||
|
||||
init(url: URL, name: String, status: Status) {
|
||||
init(url: URL, name: String, status: Status, queues: Bool) {
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.status = status
|
||||
self.queues = queues
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +73,14 @@ final class PlaylistItemsService {
|
||||
guard let item = item(at: index) else {
|
||||
return
|
||||
}
|
||||
items[index] = PlaylistItem(url: item.url, name: item.name, status: status)
|
||||
items[index] = PlaylistItem(url: item.url, name: item.name, status: status, queues: item.queues)
|
||||
}
|
||||
}
|
||||
|
||||
func provideInitialPlaylistItems() -> [PlaylistItem] {
|
||||
AudioContent.allCases.map(PlaylistItem.init(content:))
|
||||
let allCases = AudioContent.allCases
|
||||
let casesForQueueing: [AudioContent] = [.piano, .local, .khruangbin]
|
||||
let allItems = allCases.map { PlaylistItem.init(content: $0 , queues: false) }
|
||||
let casesForQueuingItems = casesForQueueing.map { PlaylistItem.init(content: $0 , queues: true) }
|
||||
return allItems + casesForQueuingItems
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '0.2.0'
|
||||
s.version = '0.3.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'
|
||||
|
||||
@@ -798,7 +798,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
@@ -828,7 +828,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
||||
@@ -29,6 +29,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
private var parsedHeaderOutput: HTTPHeaderParserOutput?
|
||||
private var relativePosition: Int
|
||||
private var seekOffset: Int
|
||||
private var supportsSeek: Bool
|
||||
|
||||
internal var metadataStreamProcessor: MetadataStreamSource
|
||||
|
||||
@@ -59,6 +60,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
additionalRequestHeaders = httpHeaders
|
||||
relativePosition = 0
|
||||
seekOffset = 0
|
||||
supportsSeek = false
|
||||
netStatusService = netStatusProvider
|
||||
self.underlyingQueue = underlyingQueue
|
||||
streamOperationQueue = OperationQueue()
|
||||
@@ -116,9 +118,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
relativePosition = 0
|
||||
seekOffset = offset
|
||||
|
||||
if let supportsSeek = parsedHeaderOutput?.supportsSeek,
|
||||
!supportsSeek, offset != relativePosition
|
||||
{
|
||||
if !supportsSeek, offset != relativePosition {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -216,6 +216,11 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
let httpStatusCode = response.statusCode
|
||||
let parser = HTTPHeaderParser()
|
||||
parsedHeaderOutput = parser.parse(input: response)
|
||||
|
||||
if let acceptRanges = parser.value(forHTTPHeaderField: HeaderField.acceptRanges, in: response) {
|
||||
supportsSeek = acceptRanges != "none"
|
||||
}
|
||||
|
||||
// check to see if we have metadata to proccess
|
||||
if let metadataStep = parsedHeaderOutput?.metadataStep {
|
||||
metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||
@@ -242,7 +247,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
urlRequest.addValue("1", forHTTPHeaderField: "Icy-MetaData")
|
||||
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
||||
|
||||
if let supportsSeek = parsedHeaderOutput?.supportsSeek, supportsSeek, seekOffset > 0 {
|
||||
if supportsSeek && seekOffset > 0 {
|
||||
urlRequest.addValue("bytes=\(seekOffset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
return urlRequest
|
||||
@@ -281,4 +286,3 @@ extension RemoteAudioSource: MetadataStreamSourceDelegate {
|
||||
delegate?.metadataReceived(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,14 @@ public final class AudioPlayer {
|
||||
playerContext.entriesLock.lock()
|
||||
let playingEntry = playerContext.audioPlayingEntry
|
||||
playerContext.entriesLock.unlock()
|
||||
guard let entry = playingEntry, !entry.seekRequest.requested else { return 0 }
|
||||
guard let entry = playingEntry else { return 0 }
|
||||
entry.seekRequest.lock.lock()
|
||||
let seekRequested = entry.seekRequest.requested
|
||||
let seekTime = entry.seekRequest.time
|
||||
entry.seekRequest.lock.unlock()
|
||||
if seekRequested {
|
||||
return seekTime
|
||||
}
|
||||
return entry.progress
|
||||
}
|
||||
|
||||
@@ -109,7 +116,7 @@ public final class AudioPlayer {
|
||||
let playerRenderProcessor: AudioPlayerRenderProcessor
|
||||
|
||||
private let audioReadSource: DispatchTimerSource
|
||||
private let underlyingQueue = DispatchQueue(label: "streaming.core.queue", qos: .userInitiated, attributes: .concurrent)
|
||||
private let serializationQueue: DispatchQueue
|
||||
private let sourceQueue: DispatchQueue
|
||||
|
||||
private let entryProvider: AudioEntryProviding
|
||||
@@ -123,7 +130,8 @@ public final class AudioPlayer {
|
||||
playerContext = AudioPlayerContext()
|
||||
entriesQueue = PlayerQueueEntries()
|
||||
|
||||
sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated, target: underlyingQueue)
|
||||
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(),
|
||||
@@ -144,7 +152,6 @@ public final class AudioPlayer {
|
||||
}
|
||||
|
||||
deinit {
|
||||
// todo more stuff to release...
|
||||
playerContext.audioPlayingEntry?.close()
|
||||
clearQueue()
|
||||
stopReadProccessFromSource()
|
||||
@@ -167,18 +174,21 @@ public final class AudioPlayer {
|
||||
public func play(url: URL, headers: [String: String]) {
|
||||
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
|
||||
audioEntry.delegate = self
|
||||
clearQueue()
|
||||
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
|
||||
playerContext.setInternalState(to: .pendingNext)
|
||||
|
||||
checkRenderWaitingAndNotifyIfNeeded()
|
||||
sourceQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
serializationQueue.sync {
|
||||
clearQueue()
|
||||
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
|
||||
playerContext.setInternalState(to: .pendingNext)
|
||||
do {
|
||||
try self.startEngineIfNeeded()
|
||||
} catch {
|
||||
self.raiseUnxpected(error: .audioSystemError(.engineFailure))
|
||||
}
|
||||
}
|
||||
|
||||
sourceQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.processSource()
|
||||
self.startReadProcessFromSourceIfNeeded()
|
||||
}
|
||||
@@ -186,19 +196,47 @@ public final class AudioPlayer {
|
||||
|
||||
/// Queues the specified URL
|
||||
///
|
||||
/// - Parameter url: A `URL` specifying the audio context to be played.
|
||||
/// - Parameter url: A `URL` specifying the audio content to be played.
|
||||
public func queue(url: URL) {
|
||||
queue(url: url, headers: [:])
|
||||
}
|
||||
|
||||
/// Queues the specified URLs
|
||||
///
|
||||
/// - Parameter url: A `URL` specifying the audio content to be played.
|
||||
public func queue(urls: [URL]) {
|
||||
queue(urls: urls, headers: [:])
|
||||
}
|
||||
|
||||
/// Queues the specified URL
|
||||
///
|
||||
/// - Parameter url: A `URL` specifying the audio context to be played.
|
||||
/// - Parameter url: A `URL` specifying the audio content to be played.
|
||||
/// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request.
|
||||
public func queue(url: URL, headers: [String: String]) {
|
||||
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
|
||||
audioEntry.delegate = self
|
||||
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
|
||||
serializationQueue.sync {
|
||||
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
|
||||
audioEntry.delegate = self
|
||||
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
|
||||
}
|
||||
checkRenderWaitingAndNotifyIfNeeded()
|
||||
sourceQueue.async { [weak self] in
|
||||
self?.processSource()
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues the specified URLs
|
||||
///
|
||||
/// - Parameter url: A array of `URL`s specifying the audio content to be played.
|
||||
/// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request.
|
||||
public func queue(urls: [URL], headers: [String: String]) {
|
||||
serializationQueue.sync {
|
||||
for url in urls {
|
||||
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
|
||||
audioEntry.delegate = self
|
||||
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
|
||||
}
|
||||
}
|
||||
checkRenderWaitingAndNotifyIfNeeded()
|
||||
sourceQueue.async { [weak self] in
|
||||
self?.processSource()
|
||||
}
|
||||
@@ -208,8 +246,11 @@ public final class AudioPlayer {
|
||||
public func stop() {
|
||||
guard playerContext.internalState != .stopped else { return }
|
||||
|
||||
stopEngine(reason: .userAction)
|
||||
stopReadProccessFromSource()
|
||||
serializationQueue.sync {
|
||||
stopEngine(reason: .userAction)
|
||||
}
|
||||
checkRenderWaitingAndNotifyIfNeeded()
|
||||
sourceQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.playerContext.audioReadingEntry?.delegate = nil
|
||||
@@ -226,7 +267,6 @@ public final class AudioPlayer {
|
||||
|
||||
self.processSource()
|
||||
}
|
||||
checkRenderWaitingAndNotifyIfNeeded()
|
||||
}
|
||||
|
||||
/// Pauses the audio playback
|
||||
@@ -234,8 +274,9 @@ public final class AudioPlayer {
|
||||
if playerContext.internalState != .paused, playerContext.internalState.contains(.running) {
|
||||
stateBeforePaused = playerContext.internalState
|
||||
playerContext.setInternalState(to: .paused)
|
||||
|
||||
pauseEngine()
|
||||
serializationQueue.sync {
|
||||
pauseEngine()
|
||||
}
|
||||
stopReadProccessFromSource()
|
||||
playerContext.audioPlayingEntry?.suspend()
|
||||
sourceQueue.async { [weak self] in
|
||||
@@ -248,20 +289,20 @@ public final class AudioPlayer {
|
||||
public func resume() {
|
||||
guard playerContext.internalState == .paused else { return }
|
||||
playerContext.setInternalState(to: stateBeforePaused)
|
||||
// check if seek time requested and reset buffers
|
||||
do {
|
||||
try startEngine()
|
||||
} catch {
|
||||
Logger.debug("resuming audio engine failed: %@", category: .generic, args: error.localizedDescription)
|
||||
}
|
||||
|
||||
if let playingEntry = playerContext.audioReadingEntry {
|
||||
if playingEntry.seekRequest.requested {
|
||||
rendererContext.resetBuffers()
|
||||
serializationQueue.sync {
|
||||
do {
|
||||
try startEngine()
|
||||
} catch {
|
||||
Logger.debug("resuming audio engine failed: %@", category: .generic, args: error.localizedDescription)
|
||||
}
|
||||
playingEntry.resume()
|
||||
if let playingEntry = playerContext.audioReadingEntry {
|
||||
if playingEntry.seekRequest.requested {
|
||||
rendererContext.resetBuffers()
|
||||
}
|
||||
playingEntry.resume()
|
||||
}
|
||||
startPlayer(resetBuffers: false)
|
||||
}
|
||||
startPlayer(resetBuffers: false)
|
||||
startReadProcessFromSourceIfNeeded()
|
||||
}
|
||||
|
||||
@@ -375,9 +416,11 @@ public final class AudioPlayer {
|
||||
|
||||
playerRenderProcessor.audioFinishedPlaying = { [weak self] entry in
|
||||
guard let self = self else { return }
|
||||
self.sourceQueue.async {
|
||||
self.serializationQueue.sync {
|
||||
let nextEntry = self.entriesQueue.dequeue(type: .buffering)
|
||||
self.processFinishPlaying(entry: entry, with: nextEntry)
|
||||
}
|
||||
self.sourceQueue.async {
|
||||
self.processSource()
|
||||
}
|
||||
}
|
||||
@@ -455,10 +498,6 @@ public final class AudioPlayer {
|
||||
///
|
||||
/// - parameter reason: A value of `AudioPlayerStopReason` indicating the reason the engine stopped.
|
||||
private func stopEngine(reason: AudioPlayerStopReason) {
|
||||
guard isEngineRunning && player.auAudioUnit.isRunning else {
|
||||
Logger.debug("already already stopped 🛑", category: .generic)
|
||||
return
|
||||
}
|
||||
audioEngine.stop()
|
||||
player.auAudioUnit.stopHardware()
|
||||
rendererContext.resetBuffers()
|
||||
@@ -491,11 +530,8 @@ public final class AudioPlayer {
|
||||
if resetBuffers {
|
||||
rendererContext.resetBuffers()
|
||||
}
|
||||
if !isEngineRunning && !player.auAudioUnit.isRunning {
|
||||
Logger.debug("trying to start the player when audio engine and player are already running", category: .generic)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try startEngineIfNeeded()
|
||||
try player.auAudioUnit.allocateRenderResources()
|
||||
try player.auAudioUnit.startHardware()
|
||||
} catch {
|
||||
@@ -542,7 +578,7 @@ public final class AudioPlayer {
|
||||
let entry = entriesQueue.dequeue(type: .upcoming)
|
||||
let shouldStartPlaying = playerContext.audioPlayingEntry == nil
|
||||
playerContext.setInternalState(to: .waitingForData)
|
||||
setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: true)
|
||||
setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: false)
|
||||
} else if playerContext.audioPlayingEntry == nil {
|
||||
if playerContext.internalState != .stopped {
|
||||
stopReadProccessFromSource()
|
||||
@@ -574,7 +610,8 @@ public final class AudioPlayer {
|
||||
}
|
||||
|
||||
private func proccessSeekTime() {
|
||||
assert(playerContext.audioReadingEntry === playerContext.audioPlayingEntry, "reading and playing entry must be the same")
|
||||
assert(playerContext.audioReadingEntry === playerContext.audioPlayingEntry,
|
||||
"reading and playing entry must be the same")
|
||||
fileStreamProcessor.processSeek()
|
||||
}
|
||||
|
||||
@@ -662,7 +699,9 @@ public final class AudioPlayer {
|
||||
playerContext.audioPlayingEntry = nil
|
||||
playerContext.entriesLock.unlock()
|
||||
}
|
||||
processSource()
|
||||
sourceQueue.async { [weak self] in
|
||||
self?.processSource()
|
||||
}
|
||||
checkRenderWaitingAndNotifyIfNeeded()
|
||||
}
|
||||
|
||||
|
||||
@@ -191,7 +191,11 @@ final class AudioFileStreamProcessor {
|
||||
guard AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, &cookie) == noErr else {
|
||||
return
|
||||
}
|
||||
guard AudioFileStreamSetProperty(fileStream, kAudioConverterDecompressionMagicCookie, cookieSize, cookie) == noErr else {
|
||||
guard let converter = audioConverter else {
|
||||
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
|
||||
return
|
||||
}
|
||||
guard AudioConverterSetProperty(converter, kAudioConverterDecompressionMagicCookie, cookieSize, cookie) == noErr else {
|
||||
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
|
||||
return
|
||||
}
|
||||
@@ -446,7 +450,7 @@ final class AudioFileStreamProcessor {
|
||||
fillUsedFrames(framesCount: framesAdded)
|
||||
return
|
||||
} else if status != 0 {
|
||||
/// raise undexpected error... codec error
|
||||
fileStreamCallback?(.raiseError(.codecError))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -475,7 +479,7 @@ final class AudioFileStreamProcessor {
|
||||
fillUsedFrames(framesCount: framesAdded)
|
||||
continue packetProccess
|
||||
} else if status != 0 {
|
||||
/// raise undexpected error... codec error
|
||||
fileStreamCallback?(.raiseError(.codecError))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -502,7 +506,7 @@ final class AudioFileStreamProcessor {
|
||||
fillUsedFrames(framesCount: framesAdded)
|
||||
continue packetProccess
|
||||
} else if status != 0 {
|
||||
/// raise undexpected error... codec error
|
||||
fileStreamCallback?(.raiseError(.codecError))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,22 +18,29 @@ enum IcyHeaderField {
|
||||
}
|
||||
|
||||
struct HTTPHeaderParserOutput {
|
||||
let supportsSeek: Bool
|
||||
let fileLength: Int
|
||||
let typeId: AudioFileTypeID
|
||||
// Metadata Support
|
||||
let metadataStep: Int
|
||||
}
|
||||
|
||||
struct HTTPHeaderParser: Parser {
|
||||
protocol HTTPHeaderParsing: Parser {
|
||||
/// Returns the value for the given field of the headers in the given `HTTPURLResponse`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - field: The header field to be searched
|
||||
/// - response: The `HTTPURLResponse` for the header
|
||||
/// - Returns: A `String` if the field exists in the headers otherwise `nil`
|
||||
func value(forHTTPHeaderField field: String, in response: HTTPURLResponse) -> String?
|
||||
}
|
||||
|
||||
struct HTTPHeaderParser: HTTPHeaderParsing {
|
||||
typealias Input = HTTPURLResponse
|
||||
typealias Output = HTTPHeaderParserOutput?
|
||||
|
||||
func parse(input: HTTPURLResponse) -> HTTPHeaderParserOutput? {
|
||||
guard let headers = input.allHeaderFields as? [String: String], !headers.isEmpty else { return nil }
|
||||
|
||||
let supportsSeek = headers[HeaderField.acceptRanges] != "none"
|
||||
|
||||
var typeId: UInt32 = 0
|
||||
if let contentType = input.mimeType {
|
||||
typeId = audioFileType(mimeType: contentType)
|
||||
@@ -41,13 +48,12 @@ struct HTTPHeaderParser: Parser {
|
||||
|
||||
var fileLength: Int = 0
|
||||
if input.statusCode == 200 {
|
||||
if let contentLength = headers[HeaderField.contentLength],
|
||||
let length = Int(contentLength)
|
||||
{
|
||||
let contentLength = value(forHTTPHeaderField: HeaderField.contentLength, in: input)
|
||||
if let contentLength = contentLength, let length = Int(contentLength) {
|
||||
fileLength = length
|
||||
}
|
||||
} else if input.statusCode == 206 {
|
||||
if let contentLength = headers[HeaderField.contentRange] {
|
||||
if let contentLength = value(forHTTPHeaderField: HeaderField.contentRange, in: input) {
|
||||
let components = contentLength.components(separatedBy: "/")
|
||||
if components.count == 2 {
|
||||
if let last = components.last, let length = Int(last) {
|
||||
@@ -58,15 +64,38 @@ struct HTTPHeaderParser: Parser {
|
||||
}
|
||||
|
||||
var metadataStep = 0
|
||||
if let icyMetaint = headers[IcyHeaderField.icyMentaint],
|
||||
if let icyMetaint = value(forHTTPHeaderField: IcyHeaderField.icyMentaint, in: input),
|
||||
let intValue = Int(icyMetaint)
|
||||
{
|
||||
metadataStep = intValue
|
||||
}
|
||||
|
||||
return HTTPHeaderParserOutput(supportsSeek: supportsSeek,
|
||||
fileLength: fileLength,
|
||||
return HTTPHeaderParserOutput(fileLength: fileLength,
|
||||
typeId: typeId,
|
||||
metadataStep: metadataStep)
|
||||
}
|
||||
}
|
||||
|
||||
extension Parser where Self: HTTPHeaderParsing {
|
||||
func value(forHTTPHeaderField field: String, in response: HTTPURLResponse) -> String? {
|
||||
if #available(iOS 13.0, *) {
|
||||
return response.value(forHTTPHeaderField: field)
|
||||
} else {
|
||||
if let fields = response.allHeaderFields as? [String: String] {
|
||||
return valueForCaseInsensitiveKey(field, fields: fields)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func valueForCaseInsensitiveKey(_ key: String, fields: [String: String]) -> String? {
|
||||
let keyToBeFound = key.lowercased()
|
||||
for (key, value) in fields {
|
||||
if key.lowercased() == keyToBeFound {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ class HTTPHeaderParserTests: XCTestCase {
|
||||
|
||||
// When
|
||||
let headers: [String: String] =
|
||||
[HeaderField.acceptRanges: "range",
|
||||
HeaderField.contentLength: "1000",
|
||||
[HeaderField.contentLength: "1000",
|
||||
HeaderField.contentType: "audio/mp3",
|
||||
IcyHeaderField.icyMentaint: "16000"]
|
||||
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
|
||||
@@ -46,23 +45,21 @@ class HTTPHeaderParserTests: XCTestCase {
|
||||
// Then
|
||||
XCTAssertNotNil(output)
|
||||
XCTAssertEqual(output!.fileLength, 1000)
|
||||
XCTAssertEqual(output!.supportsSeek, true)
|
||||
XCTAssertEqual(output!.typeId, kAudioFileMP3Type)
|
||||
XCTAssertEqual(output!.metadataStep, 16000)
|
||||
}
|
||||
|
||||
func testReturnCorrectValuesOnRequestThatSupportsSeekRanges() throws {
|
||||
func testReturnCorectValuesOnCaseInsensitiveHeaderFiels() throws {
|
||||
// Given
|
||||
let parser = HTTPHeaderParser()
|
||||
|
||||
// When
|
||||
let headers: [String: String] =
|
||||
[HeaderField.acceptRanges: "range",
|
||||
HeaderField.contentLength: "1000",
|
||||
HeaderField.contentType: "audio/mp3",
|
||||
HeaderField.contentRange: "100/1000"]
|
||||
[HeaderField.contentLength.lowercased(): "1000",
|
||||
HeaderField.contentType.lowercased(): "audio/mp3",
|
||||
IcyHeaderField.icyMentaint.lowercased(): "16000"]
|
||||
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
|
||||
statusCode: 206,
|
||||
statusCode: 200,
|
||||
httpVersion: "",
|
||||
headerFields: headers)
|
||||
|
||||
@@ -71,7 +68,7 @@ class HTTPHeaderParserTests: XCTestCase {
|
||||
// Then
|
||||
XCTAssertNotNil(output)
|
||||
XCTAssertEqual(output!.fileLength, 1000)
|
||||
XCTAssertEqual(output!.supportsSeek, true)
|
||||
XCTAssertEqual(output!.typeId, kAudioFileMP3Type)
|
||||
XCTAssertEqual(output!.metadataStep, 16000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,14 @@ player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
### Queueing audio files
|
||||
```
|
||||
let player = AudioPlayer()
|
||||
// when you want to queue a single url
|
||||
player.queue(url: URL(string: "https://your-remote-url/to/audio-file.mp3")!)
|
||||
player.queue(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
|
||||
// or if you want to queue a list of urls use
|
||||
player.queue(urls: [
|
||||
URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!,
|
||||
URL(fileURLWithPath: "your-local-path/to/audio-file-2.mp3")!
|
||||
])
|
||||
```
|
||||
|
||||
### Adjusting playback properties
|
||||
|
||||
Reference in New Issue
Block a user