Compare commits

..

14 Commits

Author SHA1 Message Date
dimitris-c 578bbcdbe8 Version bump
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-02-28 17:46:41 +02:00
dimitris-c 56c6483fc0 Some nits
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-02-28 17:46:28 +02:00
Dimitris C fca0930b01 Fixes remote audio source network issues (#35) 2022-02-27 00:05:15 +02:00
Dimitrios C 2f08ea4131 Fixes AudioExample from not compiling 2022-02-21 23:08:31 +02:00
Dimitris C 5ac825ed7a Update swift.yml 2022-02-21 22:59:05 +02:00
Dimitris C 4856a30bb6 Update swift.yml 2022-02-21 22:56:57 +02:00
Dimitris Apostolou f15f0f6eae Fix typos (#34) 2022-02-20 22:40:50 +02:00
Dimitris C da19dd9488 Update swift.yml 2022-02-20 22:30:45 +02:00
Dimitris C e57c6aabe5 Update README.md 2021-12-16 17:46:29 +02:00
Dimitris C f6f9554b25 Update swift.yml 2021-12-07 12:44:03 +02:00
Dimitris C e9bace4447 Update swift.yml 2021-12-07 12:42:33 +02:00
Dimitris C 40b9d03ea8 Update swift.yml 2021-12-07 12:38:06 +02:00
Dimitris C 3247b54c86 Update swift.yml 2021-12-07 12:36:04 +02:00
Dimitris C d78de29daf Update swift.yml 2021-12-06 20:03:09 +02:00
29 changed files with 176 additions and 150 deletions
+2 -2
View File
@@ -14,10 +14,10 @@ jobs:
name: Test iOS
runs-on: macOS-latest
env:
DEVELOPER_DIR: /Applications/Xcode_12.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
strategy:
matrix:
destination: ["OS=14.0,name=iPhone 11 Pro"] #, "OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X", "OS=10.3.1,name=iPhone SE"]
destination: ["OS=latest,name=iPhone 13 Pro"]
steps:
- uses: actions/checkout@v2
- name: iOS - ${{ matrix.destination }}
@@ -9,7 +9,6 @@
import UIKit
final class AppCoordinator {
enum Route {
case equalizer
}
@@ -44,8 +43,8 @@ final class AppCoordinator {
private func routeTo(_ route: AppCoordinator.Route) {
switch route {
case .equalizer:
showEqualizerControls()
case .equalizer:
showEqualizerControls()
}
}
@@ -9,7 +9,6 @@
import UIKit
class EqualizerViewController: UIViewController {
private lazy var enableTextLabel = UILabel()
private lazy var enableButton = UISwitch()
@@ -22,7 +21,8 @@ class EqualizerViewController: UIViewController {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -69,7 +69,7 @@ class EqualizerViewController: UIViewController {
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.8)
stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.8),
]
)
}
@@ -86,7 +86,7 @@ class EqualizerViewController: UIViewController {
private func buildSliders() -> [UIView] {
var sliders = [UIView]()
for index in 0..<viewModel.numberOfBands() {
for index in 0 ..< viewModel.numberOfBands() {
guard let item = viewModel.band(at: index) else { continue }
let slider = buildSlider(item: item, index: index)
sliders.append(slider)
@@ -16,7 +16,6 @@ struct EQBand {
}
final class EqualzerViewModel {
private var bands: [EQBand] = []
private let equalizerService: EqualizerService
@@ -31,7 +30,7 @@ final class EqualzerViewModel {
bands = equalizerService.bands.map { item in
var measurement = item.frequency
var frequency = String(Int(measurement))
if item.frequency >= 1_000 {
if item.frequency >= 1000 {
measurement = item.frequency / 1000
frequency = "\(String(Int(measurement)))K"
}
@@ -43,7 +42,7 @@ final class EqualzerViewModel {
if enable {
equalizerService.activate()
} else {
equalizerService.deactive()
equalizerService.deactivate()
}
}
@@ -51,7 +51,7 @@ class PlayerViewController: UIViewController {
style: .plain,
target: self,
action: #selector(showEqualizer))
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
@@ -92,12 +92,13 @@ class PlayerViewController: UIViewController {
@objc private func addNowPlaylistItem() {
let controller = UIAlertController(title: "Add new item", message: "", preferredStyle: .alert)
controller.addTextField { (textField) in
controller.addTextField { textField in
textField.placeholder = "Insert url here"
}
let saveAction = UIAlertAction(title: "Save", style: .default) { [viewModel] action in
let saveAction = UIAlertAction(title: "Save", style: .default) { [viewModel] _ in
if let textfield = controller.textFields?.first,
let text = textfield.text {
let text = textfield.text
{
viewModel.add(urlString: text)
}
}
@@ -105,7 +106,7 @@ class PlayerViewController: UIViewController {
controller.addAction(saveAction)
controller.addAction(cancelAction)
self.present(controller, animated: true, completion: nil)
present(controller, animated: true, completion: nil)
}
}
@@ -149,14 +150,13 @@ extension PlayerViewController: UITableViewDelegate {
}
}
final class PlaylistTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
override init(style _: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -18,14 +18,15 @@ final class PlayerViewModel {
private let playerService: AudioPlayerService
private let playlistItemsService: PlaylistItemsService
private let routeTo: ((AppCoordinator.Route) -> Void)
private let routeTo: (AppCoordinator.Route) -> Void
private var currentPlayingItemIndex: Int?
var reloadContent: ((ReloadAction) -> Void)?
init(playlistItemsService: PlaylistItemsService,
playerService: AudioPlayerService,
routeTo: @escaping (AppCoordinator.Route) -> Void) {
routeTo: @escaping (AppCoordinator.Route) -> Void)
{
self.playlistItemsService = playlistItemsService
self.playerService = playerService
self.routeTo = routeTo
@@ -10,7 +10,7 @@ import AVFoundation
final class EqualizerService {
private let playerService: AudioPlayerService
private let _freqs = [32, 64, 128, 250, 500, 1_000, 2_000, 4_000, 8_000, 16_000]
private let _freqs = [32, 64, 128, 250, 500, 1000, 2000, 4000, 8000, 16000]
private let eqUnit: AVAudioUnitEQ
var bands: [AVAudioUnitEQFilterParameters] {
@@ -23,7 +23,7 @@ final class EqualizerService {
self.playerService = playerService
eqUnit = AVAudioUnitEQ(numberOfBands: _freqs.count)
for i in 0..<_freqs.count {
for i in 0 ..< _freqs.count {
eqUnit.bands[i].bypass = false
eqUnit.bands[i].filterType = .parametric
eqUnit.bands[i].frequency = Float(_freqs[i])
@@ -45,7 +45,7 @@ final class EqualizerService {
playerService.add(eqUnit)
}
func deactive() {
func deactivate() {
isActivated = false
playerService.remove(eqUnit)
}
@@ -9,10 +9,9 @@
import MediaPlayer
final class NowPlayingCenter {
private let infoCenter: MPNowPlayingInfoCenter
init(infoCenter: MPNowPlayingInfoCenter = .default()){
init(infoCenter: MPNowPlayingInfoCenter = .default()) {
self.infoCenter = infoCenter
}
@@ -80,7 +80,7 @@ final class PlaylistItemsService {
func provideInitialPlaylistItems() -> [PlaylistItem] {
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) }
let allItems = allCases.map { PlaylistItem(content: $0, queues: false) }
let casesForQueuingItems = casesForQueueing.map { PlaylistItem(content: $0, queues: true) }
return allItems + casesForQueuingItems
}
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'AudioStreaming'
s.version = '0.8.0'
s.version = '0.9.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'
+2 -2
View File
@@ -811,7 +811,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.8.0;
MARKETING_VERSION = 0.9.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -842,7 +842,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.8.0;
MARKETING_VERSION = 0.9.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -5,11 +5,11 @@
import AVFoundation
extension AVAudioFormat {
public extension AVAudioFormat {
/// The underlying audio stream description.
///
/// This exposes the `pointee` value of the `UsafePointer<AudioStreamBasicDescription>`
public var basicStreamDescription: AudioStreamBasicDescription {
var basicStreamDescription: AudioStreamBasicDescription {
return streamDescription.pointee
}
}
+2 -1
View File
@@ -13,7 +13,7 @@ final class Retrier {
private let maxInterval: Int
private let timeoutTimer: DispatchTimerSource
/// Initiliazes a new object with the given parameters
/// Initializes a new object with the given parameters
/// - Parameters:
/// - interval: The Mach absolute time at which to execute the dispatch source's event handler.
/// - maxInterval: The maximum interval in which the internal timer will retry the callback.
@@ -38,6 +38,7 @@ final class Retrier {
/// Cancels retrying
func cancel() {
interval = .seconds(1)
timeoutTimer.removeHandler()
timeoutTimer.suspend()
}
@@ -9,12 +9,14 @@ import Network
enum NetConnectionType: Equatable {
case cellular(connected: Bool)
case wifi(connected: Bool)
case other(connected: Bool)
case undetermined
var isConnected: Bool {
switch self {
case let .cellular(connected),
let .wifi(connected):
let .wifi(connected),
let .other(connected):
return connected
default:
return false
@@ -39,15 +41,13 @@ final class NetStatusService: NetStatusProvider {
network.currentPath.toNetConnectionType()
}
private var currentConnectionType: NetConnectionType = .undetermined
private let network: NWPathMonitor
private let monitorQueue: DispatchQueue
init(network: NWPathMonitor) {
self.network = network
monitorQueue = DispatchQueue(label: "net.path.queue", qos: .background)
monitorQueue = DispatchQueue(label: "net.path.queue", qos: .utility)
}
deinit {
@@ -59,20 +59,15 @@ final class NetStatusService: NetStatusProvider {
/// - parameter connectionChange: A callback block to listen to changes of the network type, this skips duplicates.
/// - Note: The callback will be executed on the main thread.
func start(connectionChange: @escaping (NetConnectionType) -> Void) {
network.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
let connecionType = path.toNetConnectionType()
if self.currentConnectionType != connecionType {
connectionChange(self.connectionType)
self.currentConnectionType = self.connectionType
}
network.pathUpdateHandler = { path in
let connectionType = path.toNetConnectionType()
connectionChange(connectionType)
}
startIfNeeded()
}
func stop() {
network.cancel()
network.pathUpdateHandler = nil
}
func startIfNeeded() {
@@ -85,12 +80,17 @@ extension NWPath {
func toNetConnectionType() -> NetConnectionType {
let isCellular = usesInterfaceType(.cellular)
let isWifi = usesInterfaceType(.wifi)
let isOther = usesInterfaceType(.loopback)
|| usesInterfaceType(.other)
|| usesInterfaceType(.wiredEthernet)
let isConnected = status == .satisfied
if isCellular {
return .cellular(connected: isConnected)
} else if isWifi {
return .wifi(connected: isConnected)
} else if isOther {
return .other(connected: isConnected)
}
return .undetermined
@@ -13,12 +13,15 @@ enum DataStreamError: Error {
public enum NetworkError: Error, Equatable {
case failure(Error)
case serverError
case missingData
public static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
switch (lhs, rhs) {
case (.failure, failure):
return true
case (.serverError, .serverError):
return true
case (.missingData, .missingData):
return true
default:
return false
}
@@ -82,7 +85,7 @@ internal final class NetworkingClient {
// MARK: Private
/// Schedules the given `NetworkDataStream` to be performed immediatelly
/// Schedules the given `NetworkDataStream` to be performed immediately
/// - parameter stream: The `NetworkDataStream` object to be performed
/// - parameter request: The `URLRequest` for the `stream`
private func setupRequest(_ stream: NetworkDataStream, request: URLRequest) {
+1 -1
View File
@@ -5,7 +5,7 @@
import Foundation
/// A convenient type that holds tasks in a two-way manner, such as `URLSessionTask` to `NetworkDataStream` and reverved
/// A convenient type that holds tasks in a two-way manner, such as `URLSessionTask` to `NetworkDataStream` and reversed
struct BiMap<Left, Right> where Left: Hashable, Right: Hashable {
private var leftToRight: [Left: Right] = [:]
private var rightToLeft: [Right: Left] = [:]
@@ -109,7 +109,6 @@ public class RemoteAudioSource: AudioStreamSource {
func close() {
retrierTimeout.cancel()
netStatusService.stop()
streamOperationQueue.isSuspended = false
streamOperationQueue.cancelAllOperations()
if let streamTask = streamRequest {
@@ -152,8 +151,8 @@ public class RemoteAudioSource: AudioStreamSource {
guard let self = self else { return }
guard connection.isConnected else { return }
if self.waitingForNetwork {
self.seek(at: self.supportsSeek ? self.position : 0)
self.waitingForNetwork = false
self.seek(at: self.position)
}
}
}
@@ -161,14 +160,13 @@ public class RemoteAudioSource: AudioStreamSource {
private func performOpen(seek seekOffset: Int) {
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
let request = networkingClient.stream(request: urlRequest)
streamRequest = networkingClient.stream(request: urlRequest)
.responseStream { [weak self] event in
guard let self = self else { return }
self.handleResponse(event: event)
}
.resume()
streamRequest = request
metadataStreamProcessor.delegate = self
}
@@ -179,8 +177,10 @@ public class RemoteAudioSource: AudioStreamSource {
case let .response(urlResponse):
parseResponseHeader(response: urlResponse)
streamOperationQueue.isSuspended = false
case let .stream(event):
handleStreamEvent(event: event)
case let .stream(.success(response)):
handleSuccessfulStreamEvent(response: response)
case let .stream(.failure(error)):
handleFailedStreamEvent(error: error)
case let .complete(event):
if let error = event.error {
delegate?.errorOccurred(source: self, error: error)
@@ -193,50 +193,51 @@ public class RemoteAudioSource: AudioStreamSource {
}
}
private func handleStreamEvent(event: NetworkDataStream.StreamResult) {
switch event {
case let .success(value):
if let audioData = value.data {
addStreamOperation { [weak self] in
guard let self = self else { return }
if self.shouldTryParsingIcycastHeaders {
let (header, extractedAudio) = self.icycastHeadersProcessor.process(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
private func handleSuccessfulStreamEvent(response: NetworkDataStream.Response) {
guard let audioData = response.data else {
self.delegate?.errorOccurred(source: self, error: NetworkError.missingData)
return
}
addStreamOperation { [weak self] in
guard let self = self else { return }
if self.shouldTryParsingIcycastHeaders {
let (header, extractedAudio) = self.icycastHeadersProcessor.process(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: audioData)
self.relativePosition += audioCount
}
}
case .failure:
if !netStatusService.isConnected {
waitingForNetwork = true
let audioCount = self.processAudio(data: extractedAudio)
self.relativePosition += audioCount
return
}
waitingForNetwork = false
retryOnError()
let audioCount = self.processAudio(data: audioData)
self.relativePosition += audioCount
}
}
private func handleFailedStreamEvent(error: Error) {
if !netStatusService.isConnected {
waitingForNetwork = true
return
}
waitingForNetwork = false
retryOnError()
}
/// 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.canProcessMetadata {
let extractedAudioData = self.metadataStreamProcessor.processMetadata(data: data)
self.delegate?.dataAvailable(source: self, data: extractedAudioData)
if metadataStreamProcessor.canProcessMetadata {
let extractedAudioData = metadataStreamProcessor.processMetadata(data: data)
delegate?.dataAvailable(source: self, data: extractedAudioData)
return extractedAudioData.count
} else {
self.delegate?.dataAvailable(source: self, data: data)
delegate?.dataAvailable(source: self, data: data)
return data.count
}
}
@@ -257,7 +258,7 @@ public class RemoteAudioSource: AudioStreamSource {
supportsSeek = acceptRanges != "none"
}
// check to see if we have metadata to proccess
// check to see if we have metadata to process
if let metadataStep = parsedHeaderOutput?.metadataStep {
metadataStreamProcessor.metadataAvailable(step: metadataStep)
}
@@ -270,7 +271,10 @@ public class RemoteAudioSource: AudioStreamSource {
if length >= 0 { seekOffset = length }
delegate?.endOfFileOccurred(source: self)
} else if statusCode >= 300 {
delegate?.errorOccurred(source: self, error: NetworkError.serverError)
delegate?.errorOccurred(
source: self,
error: NetworkError.serverError
)
}
}
@@ -287,7 +291,7 @@ public class RemoteAudioSource: AudioStreamSource {
urlRequest.addValue("1", forHTTPHeaderField: "Icy-MetaData")
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
if supportsSeek && seekOffset > 0 {
if supportsSeek, seekOffset > 0 {
urlRequest.addValue("bytes=\(seekOffset)-", forHTTPHeaderField: "Range")
}
return urlRequest
@@ -296,7 +300,7 @@ public class RemoteAudioSource: AudioStreamSource {
private func retryOnError() {
retrierTimeout.retry { [weak self] in
guard let self = self else { return }
self.seek(at: self.position)
self.seek(at: self.supportsSeek ? self.position : 0)
}
}
@@ -312,7 +312,7 @@ open class AudioPlayer {
}
/// Seeks the audio to the specified time.
/// - Parameter time: A `Double` value specifing the time of the requested seek in seconds
/// - Parameter time: A `Double` value specifying the time of the requested seek in seconds
public func seek(to time: Double) {
guard let playingEntry = playerContext.audioPlayingEntry else {
return
@@ -402,7 +402,7 @@ open class AudioPlayer {
audioEngine.prepare()
try audioEngine.start()
} catch {
Logger.error("⚠️ error setuping audio engine: %@", category: .generic, args: error.localizedDescription)
Logger.error("⚠️ error setting up audio engine: %@", category: .generic, args: error.localizedDescription)
}
}
@@ -470,7 +470,7 @@ open class AudioPlayer {
if let first = customAttachedNodes.first {
audioEngine.connect(rateNode, to: first, format: nil)
}
for index in 0..<customAttachedNodes.count - 1 {
for index in 0 ..< customAttachedNodes.count - 1 {
let current = customAttachedNodes[index]
let next = customAttachedNodes[index + 1]
let format = current.inputFormat(forBus: 0)
@@ -523,7 +523,7 @@ open class AudioPlayer {
Logger.debug("engine stopped 🛑", category: .generic)
}
/// Starts the audio player, reseting the buffers if requested
/// Starts the audio player, resetting the buffers if requested
///
/// - parameter resetBuffers: A `Bool` value indicating if the buffers should be reset, prior starting the player.
private func startPlayer(resetBuffers: Bool) {
@@ -13,11 +13,11 @@ public struct AudioPlayerConfiguration: Equatable {
/// Number of seconds of audio required to before playback first starts.
/// - note: Must be larger that `bufferSizeInSeconds`
let secondsRequiredToStartPlaying: Double
/// Number of seconds of audio required after seek occcurs.
/// Number of seconds of audio required after seek occurs.
let gracePeriodAfterSeekInSeconds: Double
/// Number of seconds of audio required to before playback resumes after a buffer underun
/// Number of seconds of audio required to before playback resumes after a buffer underrun
/// - note: Must be larger that `bufferSizeInSeconds`
let secondsRequiredToStartPlayingAfterBufferUnderun: Int
let secondsRequiredToStartPlayingAfterBufferUnderrun: Int
/// Enables the internal logs
let enableLogs: Bool
@@ -26,7 +26,7 @@ public struct AudioPlayerConfiguration: Equatable {
bufferSizeInSeconds: 10,
secondsRequiredToStartPlaying: 1,
gracePeriodAfterSeekInSeconds: 0.5,
secondsRequiredToStartPlayingAfterBufferUnderun: 1,
secondsRequiredToStartPlayingAfterBufferUnderrun: 1,
enableLogs: false)
/// Initializes the configuration for the `AudioPlayer`
///
@@ -35,22 +35,22 @@ public struct AudioPlayerConfiguration: Equatable {
/// - parameter flushQueueOnSeek: All pending items will be flushed when seeking a track if this is set to `true`
/// - parameter bufferSizeInSeconds: The size of the decompressed buffer.
/// - parameter secondsRequiredToStartPlaying: Number of seconds of audio required to before playback first starts.
/// - parameter gracePeriodAfterSeekInSeconds: Number of seconds of audio required after seek occcurs.
/// - parameter secondsRequiredToStartPlayingAfterBufferUnderun: Number of seconds of audio required to before playback resumes after a buffer underun
/// - parameter gracePeriodAfterSeekInSeconds: Number of seconds of audio required after seek occurs.
/// - parameter secondsRequiredToStartPlayingAfterBufferUnderrun: Number of seconds of audio required to before playback resumes after a buffer underrun
/// - parameter enableLogs: Enables the internal logs
///
public init(flushQueueOnSeek: Bool = true,
bufferSizeInSeconds: Double = 10,
secondsRequiredToStartPlaying: Double = 1,
gracePeriodAfterSeekInSeconds: Double = 0.5,
secondsRequiredToStartPlayingAfterBufferUnderun: Int = 1,
secondsRequiredToStartPlayingAfterBufferUnderrun: Int = 1,
enableLogs: Bool = false)
{
self.flushQueueOnSeek = flushQueueOnSeek
self.bufferSizeInSeconds = bufferSizeInSeconds
self.secondsRequiredToStartPlaying = secondsRequiredToStartPlaying
self.gracePeriodAfterSeekInSeconds = gracePeriodAfterSeekInSeconds
self.secondsRequiredToStartPlayingAfterBufferUnderun = secondsRequiredToStartPlayingAfterBufferUnderun
self.secondsRequiredToStartPlayingAfterBufferUnderrun = secondsRequiredToStartPlayingAfterBufferUnderrun
self.enableLogs = enableLogs
}
@@ -70,15 +70,15 @@ public struct AudioPlayerConfiguration: Equatable {
? defaults.gracePeriodAfterSeekInSeconds
: self.gracePeriodAfterSeekInSeconds
let secondsRequiredToStartPlayingAfterBufferUnderun = self.secondsRequiredToStartPlayingAfterBufferUnderun == 0
? defaults.secondsRequiredToStartPlayingAfterBufferUnderun
: self.secondsRequiredToStartPlayingAfterBufferUnderun
let secondsRequiredToStartPlayingAfterBufferUnderrun = self.secondsRequiredToStartPlayingAfterBufferUnderrun == 0
? defaults.secondsRequiredToStartPlayingAfterBufferUnderrun
: self.secondsRequiredToStartPlayingAfterBufferUnderrun
return AudioPlayerConfiguration(flushQueueOnSeek: flushQueueOnSeek,
bufferSizeInSeconds: bufferSizeInSeconds,
secondsRequiredToStartPlaying: secondsRequiredToStartPlaying,
gracePeriodAfterSeekInSeconds: gracePeriodAfterSeekInSeconds,
secondsRequiredToStartPlayingAfterBufferUnderun: secondsRequiredToStartPlayingAfterBufferUnderun,
secondsRequiredToStartPlayingAfterBufferUnderrun: secondsRequiredToStartPlayingAfterBufferUnderrun,
enableLogs: enableLogs)
}
}
@@ -34,7 +34,7 @@ final class AudioRendererContext {
let canonicalStream = outputAudioFormat.basicStreamDescription
framesRequiredToStartPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlaying)
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderun)
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
framesRequiredForDataAfterSeekPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.gracePeriodAfterSeekInSeconds)
let dataByteSize = Int(canonicalStream.mSampleRate * configuration.bufferSizeInSeconds) * Int(canonicalStream.mBytesPerFrame)
@@ -75,8 +75,8 @@ private func allocateBufferList(dataByteSize: Int) -> UnsafeMutablePointer<Audio
let _bufferList = AudioBufferList.allocate(maximumBuffers: 1)
_bufferList[0].mDataByteSize = UInt32(dataByteSize)
let alingment = MemoryLayout<UInt8>.alignment
let mData = UnsafeMutableRawPointer.allocate(byteCount: dataByteSize, alignment: alingment)
let alignment = MemoryLayout<UInt8>.alignment
let mData = UnsafeMutableRawPointer.allocate(byteCount: dataByteSize, alignment: alignment)
_bufferList[0].mData = mData
_bufferList[0].mNumberChannels = 2
@@ -195,7 +195,7 @@ final class AudioFileStreamProcessor {
}
guard let converter = audioConverter else {
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
return
return
}
guard AudioConverterSetProperty(converter, kAudioConverterDecompressionMagicCookie, cookieSize, cookie) == noErr else {
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
@@ -231,9 +231,9 @@ final class AudioFileStreamProcessor {
case kAudioFileStreamProperty_AudioDataByteCount:
processDataByteCount(fileStream: fileStream)
case kAudioFileStreamProperty_AudioDataPacketCount:
proccessAudioDataPacketCount(fileStream: fileStream)
processAudioDataPacketCount(fileStream: fileStream)
case kAudioFileStreamProperty_ReadyToProducePackets:
// check converter for discontious stream
// check converter for discontinuous stream
processReadyToProducePackets(fileStream: fileStream)
case kAudioFileStreamProperty_FormatList:
processFormatList(fileStream: fileStream)
@@ -241,7 +241,7 @@ final class AudioFileStreamProcessor {
}
}
// MARK: AudioFileStream properties Proccessing
// MARK: AudioFileStream properties Processing
private func processDataOffset(fileStream: AudioFileStreamID) {
var offset: UInt64 = 0
@@ -265,7 +265,7 @@ final class AudioFileStreamProcessor {
var size = UInt32(4)
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_FileFormat, &size, &fileFormat)
if let stringFileFormat = String(data: Data(fileFormat), encoding: .utf8) {
self.currentFileFormat = stringFileFormat
currentFileFormat = stringFileFormat
}
}
@@ -308,7 +308,7 @@ final class AudioFileStreamProcessor {
entry.audioStreamState.dataByteCount = audioDataByteCount
}
private func proccessAudioDataPacketCount(fileStream: AudioFileStreamID) {
private func processAudioDataPacketCount(fileStream: AudioFileStreamID) {
guard let entry = playerContext.audioReadingEntry else { return }
var audioDataPacketCount: UInt64 = 0
fileStreamGetProperty(value: &audioDataPacketCount, fileStream: fileStream, propertyId: kAudioFileStreamProperty_AudioDataPacketCount)
@@ -378,7 +378,7 @@ final class AudioFileStreamProcessor {
}
updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
inNumberPackets: inNumberPackets)
inNumberPackets: inNumberPackets)
var status: OSStatus = noErr
packetProcess: while status == noErr {
@@ -33,7 +33,6 @@ public struct FilterEntry: Equatable {
}
public protocol FrameFiltering {
/// A Boolean value indicating whether there are filter entries
var hasEntries: Bool { get }
@@ -50,21 +49,21 @@ public protocol FrameFiltering {
/// Adds a filter entry with the given parameters
/// - Parameters:
/// - named: The name of the entry to be added
/// - filter: The block for the filter hanlding
/// - filter: The block for the filter handling
func add(entry named: String, filter: @escaping FilterCallback)
/// Adds a filter entry with the given parameters
/// - Parameters:
/// - name: The name for the new entry
/// - filterName: The name of a previously added filters
/// - filter: The block for the filter hanlding
/// - filter: The block for the filter handling
func add(entry named: String, after filterName: String, filter: @escaping FilterCallback)
/// Removes a filter entry
/// - Parameter entry: An instance of `FilterEntry` to be removed
func remove(entry: FilterEntry)
/// Attemps to remove a filter entry by its name
/// Attempts to remove a filter entry by its name
/// - Parameter named: A `String` representing the name of the filter entry
func remove(entry named: String)
@@ -73,7 +72,6 @@ public protocol FrameFiltering {
}
final class FrameFilterProcessor: NSObject, FrameFiltering {
public var hasEntries: Bool {
lock.lock(); defer { lock.unlock() }
return !entries.isEmpty
@@ -26,7 +26,6 @@ import Foundation
/// ```
final class IcycastHeadersProcessor {
private var icecastHeaders = Data(capacity: 1024)
private var searchComplete = false
private var iceHeaderAvailable = false
@@ -72,8 +71,9 @@ final class IcycastHeadersProcessor {
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 {
if icecastHeaders[..<icyPrefix.count].elementsEqual(icyPrefix) == false,
icecastHeaders[..<httpPrefix.count].elementsEqual(httpPrefix) == false
{
iceHeaderAvailable = false
searchComplete = true
}
@@ -12,7 +12,7 @@ protocol MetadataStreamSourceDelegate: AnyObject {
protocol MetadataStreamSource {
var delegate: MetadataStreamSourceDelegate? { get set }
/// Returns `true` when the stream header has indicated that we can proccess metadata, otherwise `false`.
/// Returns `true` when the stream header has indicated that we can process metadata, otherwise `false`.
var canProcessMetadata: Bool { get }
/// Assigns the metadata step of the metadata
@@ -9,9 +9,7 @@
import Foundation
struct IcycastHeaderParser: Parser {
func parse(input: Data) -> HTTPHeaderParserOutput? {
guard let icecastValue = String(data: input, encoding: .utf8) else {
return nil
}
@@ -20,11 +20,15 @@ struct MetadataParser: Parser {
guard let string = String(data: input, encoding: .utf8) else { return .failure(.unableToParse) }
// remove added bytes (zeros) and separate the string on every ';' char
let pairs = string.trimmingCharacters(in: CharacterSet(charactersIn: "\0")).components(separatedBy: ";")
let temp: [String: String] = [:]
let metadata = pairs.reduce(into: temp) { result, next in
let paired = next.components(separatedBy: "=")
if let key = paired.first,
let value = paired.last?.replacingOccurrences(of: "'", with: ""), !key.isEmpty
let metadata = pairs.reduce(into: [String: String]()) { result, next in
let split = next.split(
separator: "=",
maxSplits: 1,
omittingEmptySubsequences: true
)
.map(String.init)
if let key = split.first,
let value = split.last?.replacingOccurrences(of: "'", with: ""), !key.isEmpty
{
result[key] = value
}
@@ -50,6 +50,26 @@ class MetadataParserTests: XCTestCase {
}
}
func testParserOutputsCorrectResultWhenEntryContainsEqualSign() throws {
let string = "StreamTitle=\'Gramatik - In This Whole World (Original Mix)\';StreamUrl=\'\';track_info=\'k4Smc3RhdHVzoUihQNJiGp6BpHR5cGWhVKJpZKhNWDUxMTYzNISmc3RhdHVzoUOhQNJiGp9cpHR5cGWhVKJpZKhNWDUxMDM3MoSmc3RhdHVzoUOhQNJiGqAqpHR5cGWhVKJpZKhNWDUxMjA5Ng==\';UTC=\'20220226T214447.206\';\0\0\0\0\0\0\0\0\0"
let data = string.data(using: .utf8)!
let parser = MetadataParser()
let output = parser.parse(input: data)
switch output {
case let .success(values):
XCTAssertFalse(values.isEmpty)
XCTAssertEqual(values["StreamTitle"], "Gramatik - In This Whole World (Original Mix)")
XCTAssertEqual(values["StreamUrl"], "")
XCTAssertEqual(values["track_info"], "k4Smc3RhdHVzoUihQNJiGp6BpHR5cGWhVKJpZKhNWDUxMTYzNISmc3RhdHVzoUOhQNJiGp9cpHR5cGWhVKJpZKhNWDUxMDM3MoSmc3RhdHVzoUOhQNJiGqAqpHR5cGWhVKJpZKhNWDUxMjA5Ng==")
XCTAssertEqual(values["UTC"], "20220226T214447.206")
case .failure:
XCTFail()
}
}
func testParserOutputsFailureOnEmptyStringData() throws {
let data = "".data(using: .utf8)!
let parser = MetadataParser()
+1 -1
View File
@@ -17,7 +17,7 @@ let package = Package(
.target(
name: "AudioStreaming",
path: "AudioStreaming"
)
),
],
swiftLanguageVersions: [.v5]
)
+17 -17
View File
@@ -6,12 +6,12 @@ An AudioPlayer/Streaming library for iOS written in Swift, allows playback of on
Under the hood `AudioStreaming` uses `AVAudioEngine` and `CoreAudio` for playback and provides an easy way of applying real-time [audio enhancements](https://developer.apple.com/documentation/avfaudio/audio_engine/audio_units).
#### Supported audio
- Online streaming (Shoutcast/ICY streams) with metadata parsing
- Online streaming (Shoutcast/ICY streams) with metadata parsing
- AIFF, AIFC, WAVE, CAF, NeXT, ADTS, MPEG Audio Layer 3, AAC audio formats
- M4A (_Optimized files only_)
Known limitations:
- As described above non-optimised M4A files are not supported this is a limitation of [AudioFileStream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services?language=swift)
- As described above non-optimised M4A files are not supported this is a limitation of [AudioFileStream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services?language=swift)
# Requirements
@@ -22,18 +22,18 @@ Known limitations:
### Playing an audio source over HTTP
Note: You need to keep a reference to the `AudioPlayer` object
```
```swift
let player = AudioPlayer()
player.play(url: URL(string: "https://your-remote-url/to/audio-file.mp3")!)
```
### Playing a local file
```
```swift
let player = AudioPlayer()
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
```
### Queueing audio files
```
```swift
let player = AudioPlayer()
// when you want to queue a single url
player.queue(url: URL(string: "https://your-remote-url/to/audio-file.mp3")!)
@@ -46,7 +46,7 @@ player.queue(urls: [
```
### Adjusting playback properties
```
```swift
let player = AudioPlayer()
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
// adjust the playback rate
@@ -72,7 +72,7 @@ player.seek(to: 10)
```
### Audio playback properties
```
```swift
let player = AudioPlayer()
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
@@ -93,7 +93,7 @@ let state = player.stopReason
You can inspect various callbacks by using the `delegate` property of the `AudioPlayer` to get informed about the player state, errors etc.
View the [AudioPlayerDelegate](AudioStreaming/Streaming/AudioPlayer/AudioPlayerDelegate.swift) for more details
```
```swift
let player = AudioPlayer()
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
@@ -105,9 +105,9 @@ func audioPlayerStateChanged(player: AudioPlayer, with newState: AudioPlayerStat
### Adding custom audio nodes to AudioPlayer
`AudioStreaming` provides an easy way to attach/remove `AVAudioNode`(s).
This provides a powerful way of adjusting the playback audio with various enchncements
This provides a powerful way of adjusting the playback audio with various enhancements
```
```swift
let reverbNode = AVAudioUnitReverb()
reverbNode.wetDryMix = 50
@@ -126,11 +126,11 @@ The example project shows an example of adding a custom `AVAudioUnitEQ` node for
### Adding custom frame filter for recording and observation of audio data
`AudioStreaming` allow for custom frame fliters to be added so that recording or other observation for audio that's playing.
`AudioStreaming` allow for custom frame filters to be added so that recording or other observation for audio that's playing.
You add a frame filter by using the `AudioPlayer`'s property `frameFiltering`.
```
```swift
let player = AudioPlayer()
let format = player.mainMixerNode.outputFormat(forBus: 0)
@@ -152,12 +152,12 @@ let record = FilterEntry(name: "record") { buffer, when in
player.frameFiltering.add(entry: record)
```
See the `FrameFiltering` protocol for more ways of adding and removing frame filters.
The callback in which you observe a filter will be run on a thread other than the main thread.
See the `FrameFiltering` protocol for more ways of adding and removing frame filters.
The callback in which you observe a filter will be run on a thread other than the main thread.
Under the hood the concrete class for frame filters, `FrameFilterProcessor` installs a tap on the `mainMixerNode` of `AVAudioEngine` in which all the added fitler will be called from.
Under the hood the concrete class for frame filters, `FrameFilterProcessor` installs a tap on the `mainMixerNode` of `AVAudioEngine` in which all the added filter will be called from.
**Note** since the `mainMixerNode` is publicly exposed extra care should be taken to not install a tap directly and also use frame filters, this result in an exception because only one tap can be installed on an output node, as per Apple's documention.
**Note** since the `mainMixerNode` is publicly exposed extra care should be taken to not install a tap directly and also use frame filters, this result in an exception because only one tap can be installed on an output node, as per Apple's documentation.
# Installation
@@ -199,5 +199,5 @@ Visit [installation instructions](https://github.com/Carthage/Carthage#adding-fr
AudioStreaming is available under the MIT license. See the LICENSE file for more info.
# Attributions
This librabry takes inspiration on the already battled-tested streaming library, [StreamingKit](https://github.com/tumtumtum/StreamingKit).
This library takes inspiration on the already battled-tested streaming library, [StreamingKit](https://github.com/tumtumtum/StreamingKit).
Big 🙏 to Thong Nguyen (@tumtumtum) and Matt Gallagher (@mattgallagher) for [AudioStreamer](https://github.com/mattgallagher/AudioStreamer)