Makes player non optional
This commit is contained in:
+1
-4
@@ -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"
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user