Makes player non optional

This commit is contained in:
Dimitris C
2020-10-29 21:22:14 +00:00
parent b68691ab3b
commit 101c7ddf34
14 changed files with 95 additions and 71 deletions
+1 -4
View File
@@ -6,6 +6,7 @@ disabled_rules: # rule identifiers to exclude from running
- opening_brace
- trailing_comma
- file_length
- type_body_length
opt_in_rules: # some rules are only opt-in
# Find all the available rules by running:
# swiftlint rules
@@ -28,10 +29,6 @@ line_length:
ignores_function_declarations: true
ignores_comments: true
ignores_urls: true
# they can set both implicitly with an array
type_body_length:
- 300 # warning
- 400 # error
# or they can set both explicitly
function_body_length:
warning: 200
@@ -60,7 +60,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"
enableThreadSanitizer = "YES"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
+28 -6
View File
@@ -143,6 +143,10 @@ class ViewController: UIViewController {
slider.tintColor = .darkGray
slider.thumbTintColor = .black
}
slider.isContinuous = true
slider.addTarget(self, action: #selector(sliderTouchedDown), for: .touchDown)
slider.addTarget(self, action: #selector(sliderTouchedUp), for: [.touchUpInside, .touchUpOutside])
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
elapsedPlayTimeLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
elapsedPlayTimeLabel.textAlignment = .left
@@ -183,6 +187,22 @@ class ViewController: UIViewController {
return button
}
@objc
func sliderTouchedDown() {
stopDisplayLink(resetLabels: false)
}
@objc
func sliderTouchedUp() {
startDisplayLink()
}
@objc
func sliderValueChanged() {
print(slider.value)
}
@objc
func play(button: UIButton) {
if let content = AudioContent(rawValue: button.tag) {
@@ -242,18 +262,20 @@ class ViewController: UIViewController {
@objc
private func tick() {
if player.duration() > 0 {
let elapsed = Int(player.progress())
let remaining = Int(player.duration() - player.progress())
let duration = player.duration()
let progress = player.progress()
if duration > 0 {
let elapsed = Int(progress)
let remaining = Int(duration - progress)
slider.minimumValue = 0
slider.maximumValue = Float(player.duration())
slider.value = Float(player.progress())
slider.maximumValue = Float(duration)
slider.value = Float(progress)
elapsedPlayTimeLabel.text = timeFrom(seconds: elapsed)
remainingPlayTimeLabel.text = timeFrom(seconds: remaining)
} else {
let elapsed = Int(player.progress())
let elapsed = Int(progress)
elapsedPlayTimeLabel.text = "Live broadcast"
remainingPlayTimeLabel.text = timeFrom(seconds: elapsed)
}
@@ -56,7 +56,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableThreadSanitizer = "YES"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
@@ -12,21 +12,19 @@ extension AVAudioUnit {
///
/// - parameter description: An `AudioComponentDescription` object that defines the AudioUnit's description
/// - parameter completion: A block that will get call once the instantiation of an AVAudioUnit will occur.
///
static func createAudioUnit(with description: AudioComponentDescription,
completion: @escaping (Result<AVAudioUnit, Error>) -> Void)
{
AVAudioUnit.instantiate(with: description, options: .loadOutOfProcess) { audioUnit, error in
if let error = error {
completion(.failure(error))
guard let audioUnit = audioUnit else {
if let error = error {
completion(.failure(error))
return
}
completion(.failure(AudioPlayerError.audioSystemError(.playerNotFound)))
return
}
if let audioUnit = audioUnit {
completion(.success(audioUnit))
} else {
completion(.failure(AudioPlayerError.audioSystemError(.playerNotFound)))
}
completion(.success(audioUnit))
}
}
}
@@ -16,17 +16,25 @@ internal final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
return taskProvider.dataStream(for: task)
}
internal func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
internal func urlSession(_: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data)
{
guard let stream = self.stream(for: dataTask) else {
return
}
stream.didReceive(data: data, response: dataTask.response as? HTTPURLResponse)
stream.didReceive(data: data,
response: dataTask.response as? HTTPURLResponse)
}
internal func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let stream = self.stream(for: task) {
stream.didComplete(with: error, response: task.response as? HTTPURLResponse)
internal func urlSession(_: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?)
{
guard let stream = self.stream(for: task) else {
return
}
stream.didComplete(with: error, response: task.response as? HTTPURLResponse)
}
func urlSession(_: URLSession,
@@ -36,6 +36,7 @@ extension URLSessionConfiguration {
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
configuration.urlCache = nil
configuration.httpCookieStorage = nil
configuration.shouldUseExtendedBackgroundIdleMode = true
return configuration
}
}
@@ -50,7 +51,7 @@ internal final class NetworkingClient {
internal init(configuration: URLSessionConfiguration = .networkingConfiguration,
delegate: NetworkSessionDelegate = NetworkSessionDelegate(),
networkQueue: DispatchQueue = DispatchQueue(label: "com.decimal.session.network.queue"))
networkQueue: DispatchQueue = DispatchQueue(label: "audio.streaming.session.network.queue"))
{
let delegateQueue = operationQueue(underlyingQueue: networkQueue)
let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue)
@@ -58,7 +58,7 @@ public final class AudioPlayer {
/// The underlying `AVAudioEngine` object
let audioEngine = AVAudioEngine()
/// An `AVAudioUnit` object that represents the audio player
private(set) var player: AVAudioUnit?
private(set) var player = AVAudioUnit()
/// An `AVAudioUnitTimePitch` that controls the playback rate of the audio engine
let rateNode = AVAudioUnitTimePitch()
@@ -90,7 +90,6 @@ public final class AudioPlayer {
rendererContext = AudioRendererContext(configuration: configuration, outputAudioFormat: outputAudioFormat)
playerContext = AudioPlayerContext()
entriesQueue = PlayerQueueEntries()
sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated, target: underlyingQueue)
@@ -277,13 +276,12 @@ public final class AudioPlayer {
/// Creates and configures an `AVAudioUnit` with an output configuration
/// and assigns it to the `player` variable.
private func configPlayerNode() {
let playerRenderProcessor = self.playerRenderProcessor
AVAudioUnit.createAudioUnit(with: UnitDescriptions.output) { [weak self] result in
guard let self = self else { return }
switch result {
case let .success(unit):
self.player = unit
playerRenderProcessor.attachCallback(on: unit, audioFormat: self.outputAudioFormat)
self.playerRenderProcessor.attachCallback(on: unit, audioFormat: self.outputAudioFormat)
case let .failure(error):
assertionFailure("couldn't create player unit: \(error)")
self.raiseUnxpected(error: .audioSystemError(.playerNotFound))
@@ -341,7 +339,7 @@ public final class AudioPlayer {
private func pauseEngine() {
guard isEngineRunning else { return }
audioEngine.pause()
player?.auAudioUnit.stopHardware()
player.auAudioUnit.stopHardware()
Logger.debug("engine paused ⏸", category: .generic)
}
@@ -354,7 +352,7 @@ public final class AudioPlayer {
return
}
audioEngine.stop()
player?.auAudioUnit.stopHardware()
player.auAudioUnit.stopHardware()
rendererContext.resetBuffers()
playerContext.internalState = .stopped
playerContext.stopReason.write { $0 = reason }
@@ -364,7 +362,6 @@ public final class AudioPlayer {
/// 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
@@ -383,7 +380,6 @@ public final class AudioPlayer {
///
/// - parameter resetBuffers: A `Bool` value indicating if the buffers should be reset, prior starting the player.
private func startPlayer(resetBuffers: Bool) {
guard let player = player else { return }
if resetBuffers {
rendererContext.resetBuffers()
}
@@ -394,6 +390,7 @@ public final class AudioPlayer {
do {
try player.auAudioUnit.startHardware()
} catch {
stopEngine(reason: .error)
raiseUnxpected(error: .audioSystemError(.playerStartError))
}
// TODO: stop system background task
@@ -604,7 +601,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
playerContext.entriesLock.lock()
playerContext.audioReadingEntry = nil
playerContext.entriesLock.unlock()
processSource()
}
@@ -9,7 +9,6 @@ import CoreAudio
internal var maxFramesPerSlice: AVAudioFrameCount = 8192
final class AudioRendererContext {
var waiting = Protected<Bool>(false)
let lock = UnfairLock()
@@ -26,7 +25,7 @@ final class AudioRendererContext {
let framesRequiredToStartPlaying: UInt32
let framesRequiredAfterRebuffering: UInt32
let configuration: AudioPlayerConfiguration
private let configuration: AudioPlayerConfiguration
init(configuration: AudioPlayerConfiguration, outputAudioFormat: AVAudioFormat) {
self.configuration = configuration
@@ -387,7 +387,6 @@ final class AudioFileStreamProcessor {
/// raise undexpected error... codec error
return
}
} else {
var framesAdded: UInt32 = 0
var framesToDecode: UInt32 = start - end
@@ -430,11 +429,7 @@ final class AudioFileStreamProcessor {
framesToDecode: UInt32)
{
if let mData = rendererContext.audioBuffer.mData {
if dataOffset > 0 {
bufferList[0].mData = mData + dataOffset
} else {
bufferList[0].mData = mData
}
bufferList[0].mData = dataOffset > 0 ? mData + dataOffset : mData
}
bufferList[0].mDataByteSize = framesToDecode * rendererContext.bufferContext.sizeInBytes
bufferList[0].mNumberChannels = rendererContext.audioBuffer.mNumberChannels
@@ -445,9 +440,10 @@ final class AudioFileStreamProcessor {
/// - parameter frameCount: An `UInt32` value to be added to the used count of the buffers.
@inline(__always)
private func fillUsedFrames(framesCount: UInt32) {
rendererContext.lock.around {
rendererContext.bufferContext.frameUsedCount += framesCount
}
rendererContext.lock.lock()
rendererContext.bufferContext.frameUsedCount += framesCount
rendererContext.lock.unlock()
playerContext.audioReadingEntry?.lock.lock()
playerContext.audioReadingEntry?.framesState.queued += Int(framesCount)
playerContext.audioReadingEntry?.lock.unlock()
@@ -462,7 +458,7 @@ final class AudioFileStreamProcessor {
let processedPackCount = readingEntry.processedPacketsState.count
if processedPackCount < maxCompressedPacketForBitrate {
let count = min(Int(inNumberPackets), maxCompressedPacketForBitrate - Int(processedPackCount))
for i in 0..<count {
for i in 0 ..< count {
let packet = inPacketDescriptions[i]
let packetSize: UInt32 = packet.mDataByteSize
readingEntry.lock.lock()
@@ -32,7 +32,7 @@ protocol CoreAudioStreamSource: AnyObject {
/// Suspends the underlying stream
func suspend()
// Resumes the underlying stream
/// Resumes the underlying stream
func resume()
/// Seeks the stream at the specified offset
@@ -1,12 +1,11 @@
//
// Models.swift
// EntryFramesState.swift
// AudioStreaming
//
// Created by Dimitrios Chatzieleftheriou on 24/10/2020.
// Copyright © 2020 Decimal. All rights reserved.
//
import AVFoundation
import Foundation
final class EntryFramesState {
@@ -19,7 +19,7 @@ public class RemoteAudioSource: AudioStreamSource {
}
private let url: URL
private let networking: NetworkingClient
private let networkingClient: NetworkingClient
private var streamRequest: NetworkDataStream?
private var additionalRequestHeaders: [String: String]
@@ -38,7 +38,7 @@ public class RemoteAudioSource: AudioStreamSource {
}
internal let underlyingQueue: DispatchQueue
internal let networkStreamQueue: OperationQueue
internal let streamOperationQueue: OperationQueue
init(networking: NetworkingClient,
metadataStreamSource: MetadataStreamSource,
@@ -46,17 +46,18 @@ public class RemoteAudioSource: AudioStreamSource {
underlyingQueue: DispatchQueue,
httpHeaders: [String: String])
{
self.networking = networking
networkingClient = networking
metadataStreamProccessor = metadataStreamSource
self.url = url
additionalRequestHeaders = httpHeaders
relativePosition = 0
seekOffset = 0
self.underlyingQueue = underlyingQueue
networkStreamQueue = OperationQueue()
networkStreamQueue.underlyingQueue = underlyingQueue
networkStreamQueue.maxConcurrentOperationCount = 1
networkStreamQueue.isSuspended = true
streamOperationQueue = OperationQueue()
streamOperationQueue.underlyingQueue = underlyingQueue
streamOperationQueue.maxConcurrentOperationCount = 1
streamOperationQueue.isSuspended = true
streamOperationQueue.name = "remote.audio.source.data.stream.queue"
}
convenience init(networking: NetworkingClient,
@@ -86,10 +87,10 @@ public class RemoteAudioSource: AudioStreamSource {
func close() {
streamRequest?.cancel()
if let streamTask = streamRequest {
networking.remove(task: streamTask)
networkingClient.remove(task: streamTask)
}
streamRequest = nil
networkStreamQueue.cancelAllOperations()
streamOperationQueue.cancelAllOperations()
}
func seek(at offset: Int) {
@@ -109,11 +110,11 @@ public class RemoteAudioSource: AudioStreamSource {
}
func suspend() {
networkStreamQueue.isSuspended = true
streamOperationQueue.isSuspended = true
}
func resume() {
networkStreamQueue.isSuspended = false
streamOperationQueue.isSuspended = false
}
// MARK: Private
@@ -121,12 +122,14 @@ public class RemoteAudioSource: AudioStreamSource {
private func performOpen(seek seekOffset: Int) {
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
streamRequest = networking.stream(request: urlRequest)
let request = networkingClient.stream(request: urlRequest)
.responseStream { [weak self] event in
guard let self = self else { return }
self.handleResponse(event: event)
}
streamRequest?.resume()
.resume()
streamRequest = request
metadataStreamProccessor.delegate = self
}
@@ -135,7 +138,9 @@ public class RemoteAudioSource: AudioStreamSource {
private func handleResponse(event: NetworkDataStream.StreamEvent) {
switch event {
case let .response(urlResponse):
parseResponseHeader(response: urlResponse)
addStreamOperation { [weak self] in
self?.parseResponseHeader(response: urlResponse)
}
case let .stream(event):
addStreamOperation { [weak self] in
self?.handleStreamEvent(event: event)
@@ -171,11 +176,9 @@ public class RemoteAudioSource: AudioStreamSource {
private func parseResponseHeader(response: HTTPURLResponse?) {
guard let response = response else { return }
// TODO: Parse Icy header
let httpStatusCode = response.statusCode
let parser = HTTPHeaderParser()
parsedHeaderOutput = parser.parse(input: response)
// parse the header response
// check to see if we have metadata to proccess
if let metadataStep = parsedHeaderOutput?.metadataStep {
metadataStreamProccessor.metadataAvailable(step: metadataStep)
@@ -210,17 +213,23 @@ public class RemoteAudioSource: AudioStreamSource {
// MARK: - Network Stream Operation Queue
/// Schedules the given block on the stream operation queue
///
/// - Parameter block: A closure to be executed
private func addStreamOperation(_ block: @escaping () -> Void) {
let operation = BlockOperation(block: block)
networkStreamQueue.addOperation(operation)
streamOperationQueue.addOperation(operation)
}
/// Schedules the given block on the stream operation queue as a completion
///
/// - Parameter block: A closure to be executed
private func addCompletionOperation(_ block: @escaping () -> Void) {
let operation = BlockOperation(block: block)
if let lastOperation = networkStreamQueue.operations.last {
if let lastOperation = streamOperationQueue.operations.last {
operation.addDependency(lastOperation)
}
networkStreamQueue.addOperation(operation)
streamOperationQueue.addOperation(operation)
}
}
@@ -12,7 +12,7 @@ class ProtectedTests: XCTestCase {
measure {
let protected = Protected<Int>(0)
DispatchQueue.concurrentPerform(iterations: 1_000_000) { i in
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
_ = protected.value
protected.write { $0 += 1 }
}