Compare commits

..

1 Commits

Author SHA1 Message Date
dimitris-c 5ab306e528 Adds macOS support, updates example project
Also fixes a thread safety issue
2024-05-17 22:49:56 +03:00
30 changed files with 230 additions and 659 deletions
@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
42BE42F52C9322AA00C0E448 /* CustomStreamSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42BE42F42C9322AA00C0E448 /* CustomStreamSource.swift */; };
9806E8182BC5D12500757370 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8172BC5D12500757370 /* App.swift */; };
9806E81A2BC5D12500757370 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8192BC5D12500757370 /* ContentView.swift */; };
9806E81C2BC5D12700757370 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9806E81B2BC5D12700757370 /* Assets.xcassets */; };
@@ -48,7 +47,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
42BE42F42C9322AA00C0E448 /* CustomStreamSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomStreamSource.swift; sourceTree = "<group>"; };
9806E8142BC5D12500757370 /* AudioPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AudioPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
9806E8172BC5D12500757370 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
9806E8192BC5D12500757370 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -197,7 +195,6 @@
98E3921C2BD845E100B586E9 /* AudioPlayer */ = {
isa = PBXGroup;
children = (
42BE42F42C9322AA00C0E448 /* CustomStreamSource.swift */,
9806E8302BC6927D00757370 /* AudioPlayerModel.swift */,
9806E8292BC68F8700757370 /* AudioPlayerView.swift */,
98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */,
@@ -295,7 +292,6 @@
9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */,
984DE9572BDAFC7E004B427A /* AudioPlayerControlsView.swift in Sources */,
9806E8182BC5D12500757370 /* App.swift in Sources */,
42BE42F52C9322AA00C0E448 /* CustomStreamSource.swift in Sources */,
989E08E72BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -19,7 +19,6 @@ enum AudioContent {
case remoteWave
case local
case localWave
case loopBeatFlac
case custom(String)
var title: String {
@@ -50,8 +49,6 @@ enum AudioContent {
return "Jazzy Frenchy"
case .nonOptimized:
return "Jazzy Frenchy"
case .loopBeatFlac:
return "Beat loop"
case .custom(let url):
return url
}
@@ -85,8 +82,6 @@ enum AudioContent {
return "Music by: bensound.com - m4a optimized"
case .nonOptimized:
return "Music by: bensound.com - m4a non-optimized"
case .loopBeatFlac:
return "Remote flac"
case .custom:
return ""
}
@@ -122,8 +117,6 @@ enum AudioContent {
return URL(fileURLWithPath: path)
case .remoteWave:
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/5-MB-WAV.wav")!
case .loopBeatFlac:
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/drumbeat-loop.flac")!
case .custom(let url):
return URL(string: url)!
}
@@ -4,7 +4,6 @@
import AVFoundation
import SwiftUI
import AudioStreaming
struct AudioPlayerControls: View {
@State var model: Model
@@ -248,23 +247,11 @@ extension AudioPlayerControls {
func play(_ track: AudioTrack) {
if track != currentTrack {
currentTrack?.status = .idle
if track.url.scheme == "custom" {
let source = createStreamSource()
let audioFormat = AVAudioFormat(
commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false
)!
audioPlayerService.play(source: source, entryId: track.url.absoluteString, format: audioFormat)
currentTrack = track
} else {
audioPlayerService.play(url: track.url)
}
audioPlayerService.play(url: track.url)
currentTrack = track
}
}
func createStreamSource() -> CoreAudioStreamSource {
return CustomStreamAudioSource(underlyingQueue: audioPlayerService.player.sourceQueue)
}
func onTick() {
let duration = audioPlayerService.duration
let progress = audioPlayerService.progress
@@ -58,14 +58,12 @@ public class AudioPlayerModel {
}
private let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .kosmosJazz, .radiox]
private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave, .loopBeatFlac]
private let customStreams: [AudioContent] = [.custom("custom://sinwave")]
private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave]
func audioTracksProvider() -> [AudioPlaylist] {
[
AudioPlaylist(title: "Radio", tracks: radioTracks.map { AudioTrack.init(from: $0) }),
AudioPlaylist(title: "Tracks", tracks: audioTracks.map { AudioTrack.init(from:$0) }),
AudioPlaylist(title: "Generated", tracks: customStreams.map { AudioTrack.init(from:$0) })
AudioPlaylist(title: "Tracks", tracks: audioTracks.map { AudioTrack.init(from:$0) })
]
}
@@ -1,139 +0,0 @@
//
// CustomStreamSource.swift
// AudioPlayer
//
// Created by Jackson Harper on 12/9/24.
//
import AVFoundation
import Foundation
import AudioStreaming
// This is a basic example of playing a custom audio stream. We generate
// a small audio data on load and then pass it off to AudioStreaming.
final class CustomStreamAudioSource: NSObject, CoreAudioStreamSource {
weak var delegate: AudioStreamSourceDelegate?
var underlyingQueue: DispatchQueue
var position = 0
var length = 0
var audioFileHint: AudioFileTypeID {
kAudioFileWAVEType
}
init(underlyingQueue: DispatchQueue) {
self.underlyingQueue = underlyingQueue
}
// no-op
func close() {}
// no-op
func suspend() {}
func resume() {}
func seek(at _: Int) {
// The streaming process is started by a seek(0) call from AudioStreaming
generateData()
}
private func generateData() {
let frequency = 440.0
let sampleRate = 44100
let duration = 20.0
let lpcmData = generateSineWave(frequency: frequency, sampleRate: sampleRate, duration: duration)
let waveFile = createWavFile(using: lpcmData)
// We enqueue this because during startup the seek call will be made, but the player
// is not completely setup and ready to handle data yet, as its expected to be
// generated asyncronously.
underlyingQueue.asyncAfter(deadline: .now().advanced(by: .milliseconds(100))) {
self.delegate?.dataAvailable(source: self, data: waveFile)
}
}
}
// Functions for generating some sample data
// Function to generate a sine wave as Data
func generateSineWave(frequency: Double, sampleRate: Int, duration: Double, amplitude: Double = 0.5) -> Data {
let numberOfSamples = Int(Double(sampleRate) * duration)
let twoPi = 2.0 * Double.pi
var lpcmData = Data()
for sampleIndex in 0 ..< numberOfSamples {
let time = Double(sampleIndex) / Double(sampleRate)
let sampleValue = amplitude * sin(twoPi * frequency * time)
let pcmValue = Int16(sampleValue * Double(Int16.max))
withUnsafeBytes(of: pcmValue.littleEndian) { lpcmData.append(contentsOf: $0) }
}
return lpcmData
}
func createWavFile(using rawData: Data) -> Data {
let waveHeaderFormate = createWaveHeader(data: rawData) as Data
let waveFileData = waveHeaderFormate + rawData
return waveFileData
}
// from: https://stackoverflow.com/questions/49399823/in-ios-how-to-create-audio-file-wav-mp3-file-from-data
private func createWaveHeader(data: Data) -> NSData {
let sampleRate: Int32 = 44100
let chunkSize: Int32 = 36 + Int32(data.count)
let subChunkSize: Int32 = 16
let format: Int16 = 1
let channels: Int16 = 2
let bitsPerSample: Int16 = 16
let byteRate: Int32 = sampleRate * Int32(channels * bitsPerSample / 8)
let blockAlign: Int16 = channels * bitsPerSample / 8
let dataSize = Int32(data.count)
let header = NSMutableData()
header.append([UInt8]("RIFF".utf8), length: 4)
header.append(intToByteArray(chunkSize), length: 4)
// WAVE
header.append([UInt8]("WAVE".utf8), length: 4)
// FMT
header.append([UInt8]("fmt ".utf8), length: 4)
header.append(intToByteArray(subChunkSize), length: 4)
header.append(shortToByteArray(format), length: 2)
header.append(shortToByteArray(channels), length: 2)
header.append(intToByteArray(sampleRate), length: 4)
header.append(intToByteArray(byteRate), length: 4)
header.append(shortToByteArray(blockAlign), length: 2)
header.append(shortToByteArray(bitsPerSample), length: 2)
header.append([UInt8]("data".utf8), length: 4)
header.append(intToByteArray(dataSize), length: 4)
return header
}
private func intToByteArray(_ i: Int32) -> [UInt8] {
return [
// little endian
UInt8(truncatingIfNeeded: i & 0xFF),
UInt8(truncatingIfNeeded: (i >> 8) & 0xFF),
UInt8(truncatingIfNeeded: (i >> 16) & 0xFF),
UInt8(truncatingIfNeeded: (i >> 24) & 0xFF),
]
}
private func shortToByteArray(_ i: Int16) -> [UInt8] {
return [
// little endian
UInt8(truncatingIfNeeded: i & 0xFF),
UInt8(truncatingIfNeeded: (i >> 8) & 0xFF),
]
}
@@ -17,7 +17,7 @@ protocol AudioPlayerServiceDelegate: AnyObject {
final class AudioPlayerService {
weak var delegate: AudioPlayerServiceDelegate?
var player: AudioPlayer
private var player: AudioPlayer
private var audioSystemResetObserver: Any?
var duration: Double {
@@ -60,11 +60,6 @@ final class AudioPlayerService {
player.play(url: url)
}
func play(source: CoreAudioStreamSource, entryId: String, format: AVAudioFormat) {
activateAudioSession()
player.play(source: source, entryId: entryId, format: format)
}
func queue(url: URL) {
activateAudioSession()
player.queue(url: url)
+19
View File
@@ -0,0 +1,19 @@
Pod::Spec.new do |s|
s.name = 'AudioStreaming'
s.version = '1.2.2'
s.license = 'MIT'
s.summary = 'An AudioPlayer/Streaming library for iOS written in Swift using AVAudioEngine.'
s.homepage = 'https://github.com/dimitris-c/AudioStreaming'
s.authors = { 'Dimitris C.' => 'dimmdesign@gmail.com' }
s.source = { :git => 'https://github.com/dimitris-c/AudioStreaming.git', :tag => s.version }
s.ios.deployment_target = '13.0'
s.swift_versions = ['5.1', '5.2', '5.3']
s.source_files = 'AudioStreaming/**/*.swift'
s.pod_target_xcconfig = {
'SWIFT_INSTALL_OBJC_HEADER' => 'NO'
}
end
+15 -31
View File
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
@@ -54,7 +54,7 @@
B59D0B6F255C904900D6CCE5 /* FileAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */; };
B59DF10424916FD50043C498 /* DispatchQueue+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59DF10324916FD50043C498 /* DispatchQueue+Helpers.swift */; };
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59DF1A22493E90C0043C498 /* AudioFileStream+Helpers.swift */; };
B5AEDBB824744153007D8101 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AEDBAE24744153007D8101 /* AudioStreaming.framework */; platformFilters = (ios, tvos, ); };
B5AEDBB824744153007D8101 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AEDBAE24744153007D8101 /* AudioStreaming.framework */; };
B5AEDBBF24744153007D8101 /* AudioStreaming.h in Headers */ = {isa = PBXBuildFile; fileRef = B5AEDBB124744153007D8101 /* AudioStreaming.h */; settings = {ATTRIBUTES = (Public, ); }; };
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */; };
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */; };
@@ -528,9 +528,8 @@
B5AEDBA524744153007D8101 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 1620;
LastUpgradeCheck = 1200;
ORGANIZATIONNAME = Decimal;
TargetAttributes = {
B5AEDBAD24744153007D8101 = {
@@ -588,7 +587,6 @@
/* Begin PBXShellScriptBuildPhase section */
B583864B2545858E0087A712 /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -685,10 +683,6 @@
/* Begin PBXTargetDependency section */
B5AEDBBA24744153007D8101 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilters = (
ios,
tvos,
);
target = B5AEDBAD24744153007D8101 /* AudioStreaming */;
targetProxy = B5AEDBB924744153007D8101 /* PBXContainerItemProxy */;
};
@@ -733,7 +727,6 @@
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -750,7 +743,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.2.8;
MARKETING_VERSION = 1.1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -800,7 +793,6 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -811,7 +803,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.2.8;
MARKETING_VERSION = 1.1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -827,14 +819,12 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
INFOPLIST_FILE = AudioStreaming/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
@@ -843,18 +833,17 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.2.8;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
MARKETING_VERSION = 1.2.2;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -862,14 +851,12 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
INFOPLIST_FILE = AudioStreaming/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
@@ -878,23 +865,23 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.2.8;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
MARKETING_VERSION = 1.2.2;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
B5AEDBC624744153007D8101 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
@@ -906,17 +893,16 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreamingTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
B5AEDBC724744153007D8101 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
@@ -928,10 +914,8 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreamingTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -5,7 +5,7 @@
import AVFoundation
public enum AudioConverterError: CustomDebugStringConvertible, Sendable {
public enum AudioConverterError: CustomDebugStringConvertible {
case badPropertySizeError
case formatNotSupported
case inputSampleRateOutOfRange
@@ -29,7 +29,7 @@ func fileStreamGetPropertyInfo(fileStream streamId: AudioFileStreamID, propertyI
///
/// Reference:
/// [Audio File Stream Errors](https://developer.apple.com/documentation/audiotoolbox/1391572-audio_file_stream_errors?language=objc)
public enum AudioFileStreamError: CustomDebugStringConvertible, Sendable {
public enum AudioFileStreamError: CustomDebugStringConvertible {
case badPropertySize
case dataUnavailable
case discontinuityCantRecover
+6 -76
View File
@@ -4,7 +4,6 @@
//
import Foundation
import os
protocol Lock {
func lock()
@@ -15,96 +14,24 @@ protocol Lock {
// Execute a closure while acquiring a lock
func withLock(body: () -> Void)
func deallocate()
}
/// A wrapper for `os_unfair_lock`
/// - Tag: UnfairLock
final class UnfairLock: Lock {
var unfairLock: Lock
init() {
if #available(iOS 16.0, *), #available(macOS 13.0, *) {
unfairLock = OSStorageLock()
} else {
unfairLock = UnfairStorageLock()
}
}
deinit {
deallocate()
}
func deallocate() {
unfairLock.deallocate()
}
@inlinable
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
try unfairLock.withLock(body: body)
}
@inlinable
func withLock(body: () -> Void) {
unfairLock.withLock(body: body)
}
@inlinable
func lock() {
unfairLock.lock()
}
@inlinable
func unlock() {
unfairLock.unlock()
}
}
@available(iOS 16.0, *)
@available(macOS 13, *)
private class OSStorageLock: Lock {
@usableFromInline
let osLock = OSAllocatedUnfairLock()
@inlinable
func lock() {
osLock.lock()
}
@inlinable
func unlock() {
osLock.unlock()
}
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
try osLock.withLockUnchecked(body)
}
func withLock(body: () -> Void) {
osLock.withLockUnchecked(body)
}
func deallocate() {} // no-op
}
private class UnfairStorageLock: Lock {
@usableFromInline
let unfairLock: UnsafeMutablePointer<os_unfair_lock>
@usableFromInline let unfairLock: UnsafeMutablePointer<os_unfair_lock>
init() {
unfairLock = .allocate(capacity: 1)
unfairLock.initialize(to: os_unfair_lock())
}
func deallocate() {
unfairLock.deinitialize(count: 1)
deinit {
unfairLock.deallocate()
}
@inlinable
@inline(__always)
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
os_unfair_lock_lock(unfairLock)
defer { os_unfair_lock_unlock(unfairLock) }
@@ -112,6 +39,7 @@ private class UnfairStorageLock: Lock {
}
@inlinable
@inline(__always)
func withLock(body: () -> Void) {
os_unfair_lock_lock(unfairLock)
defer { os_unfair_lock_unlock(unfairLock) }
@@ -119,11 +47,13 @@ private class UnfairStorageLock: Lock {
}
@inlinable
@inline(__always)
func lock() {
os_unfair_lock_lock(unfairLock)
}
@inlinable
@inline(__always)
func unlock() {
os_unfair_lock_unlock(unfairLock)
}
@@ -37,11 +37,6 @@ class AudioEntry {
return seekTime + (Double(framesState.played) / outputAudioFormat.sampleRate)
}
var framesPlayed: Int {
lock.lock(); defer { lock.unlock() }
return framesState.played
}
var audioStreamFormat = AudioStreamBasicDescription()
/// Hold the seek time, if a seek was requested
@@ -108,9 +103,6 @@ class AudioEntry {
func calculatedBitrate() -> Double {
lock.lock(); defer { lock.unlock() }
if let explicitBitRate = audioStreamState.bitRate, explicitBitRate > 0 {
return explicitBitRate
}
let packets = processedPacketsState
if packetDuration > 0 {
let packetsCount = packets.count
@@ -6,7 +6,6 @@
import AVFoundation
protocol AudioEntryProviding {
func provideAudioEntry(url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) -> AudioEntry
func provideAudioEntry(url: URL, headers: [String: String]) -> AudioEntry
func provideAudioEntry(url: URL) -> AudioEntry
}
@@ -26,14 +25,7 @@ final class AudioEntryProvider: AudioEntryProviding {
}
func provideAudioEntry(url: URL, headers: [String: String]) -> AudioEntry {
let source = self.source(for: url, httpMethod: nil, httpBody: nil, headers: headers)
return AudioEntry(source: source,
entryId: AudioEntryId(id: url.absoluteString),
outputAudioFormat: outputAudioFormat)
}
func provideAudioEntry(url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) -> AudioEntry {
let source = self.source(for: url, httpMethod: httpMethod, httpBody: httpBody, headers: headers)
let source = self.source(for: url, headers: headers)
return AudioEntry(source: source,
entryId: AudioEntryId(id: url.absoluteString),
outputAudioFormat: outputAudioFormat)
@@ -42,12 +34,10 @@ final class AudioEntryProvider: AudioEntryProviding {
func provideAudioEntry(url: URL) -> AudioEntry {
provideAudioEntry(url: url, headers: [:])
}
func provideAudioSource(url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) -> AudioStreamSource {
func provideAudioSource(url: URL, headers: [String: String]) -> AudioStreamSource {
RemoteAudioSource(networking: networkingClient,
url: url,
httpMethod: httpMethod,
httpBody: httpBody,
underlyingQueue: underlyingQueue,
httpHeaders: headers)
}
@@ -56,10 +46,10 @@ final class AudioEntryProvider: AudioEntryProviding {
FileAudioSource(url: url, underlyingQueue: underlyingQueue)
}
func source(for url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) -> CoreAudioStreamSource {
func source(for url: URL, headers: [String: String]) -> CoreAudioStreamSource {
guard !url.isFileURL else {
return provideFileAudioSource(url: url)
}
return provideAudioSource(url: url, httpMethod: httpMethod, httpBody: httpBody, headers: headers)
return provideAudioSource(url: url, headers: headers)
}
}
@@ -12,5 +12,4 @@ final class AudioStreamState {
var dataPacketOffset: UInt64?
var dataPacketCount: Double = 0
var streamFormat = AudioStreamBasicDescription()
var bitRate: Double?
}
@@ -6,7 +6,7 @@
import AudioToolbox
import Foundation
public protocol AudioStreamSourceDelegate: AnyObject {
protocol AudioStreamSourceDelegate: AnyObject {
/// Indicates that there's data available
func dataAvailable(source: CoreAudioStreamSource, data: Data)
/// Indicates an error occurred
@@ -17,7 +17,7 @@ public protocol AudioStreamSourceDelegate: AnyObject {
func metadataReceived(data: [String: String])
}
public protocol CoreAudioStreamSource: AnyObject {
protocol CoreAudioStreamSource: AnyObject {
/// An `Int` that represents the position of the audio
var position: Int { get }
/// The length of the audio in bytes
@@ -120,15 +120,11 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
if isMp4, !mp4IsAlreadyOptimized {
if !mp4Restructure.dataOptimized {
do {
switch try mp4Restructure.checkIsOptimized(data: data) {
case .undetermined:
// Not enough bytes yet; wait for more data before deciding
break
case .optimized:
if let mp4OptimizeInfo = try mp4Restructure.checkIsOptimized(data: data) {
try performMp4Restructure(inputStream: inputStream, mp4OptimizeInfo: mp4OptimizeInfo)
} else {
mp4IsAlreadyOptimized = true
delegate?.dataAvailable(source: self, data: data)
case let .needsRestructure(moovOffset):
try performMp4Restructure(inputStream: inputStream, moovOffset: moovOffset)
}
} catch {
delegate?.errorOccurred(source: self, error: error)
@@ -145,71 +141,24 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
}
func performMp4Restructure(inputStream: InputStream, moovOffset: Int) throws {
let offsetAccepted = inputStream.setProperty(moovOffset, forKey: .fileCurrentOffsetKey)
if !offsetAccepted {
delegate?.errorOccurred(source: self, error: inputStream.streamError ?? AudioSystemError.playerStartError)
return
}
// Read moov header (8 bytes)
var header = [UInt8](repeating: 0, count: 8)
let headerRead = inputStream.read(&header, maxLength: 8)
guard headerRead == 8 else {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
return
}
// Parse size and type (big endian)
let size32 = Data(header[0 ..< 4]).withUnsafeBytes { $0.load(as: UInt32.self) }.bigEndian
let type32 = Data(header[4 ..< 8]).withUnsafeBytes { $0.load(as: UInt32.self) }.bigEndian
guard Int(type32) == Atoms.moov else {
delegate?.errorOccurred(source: self, error: Mp4RestructureError.missingMoovAtom)
return
}
var moovSize = Int(size32)
var moovData = Data(header)
// Extended size (64-bit)
if moovSize == 1 {
var ext = [UInt8](repeating: 0, count: 8)
let extRead = inputStream.read(&ext, maxLength: 8)
guard extRead == 8 else {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
return
}
let ext64 = Data(ext).withUnsafeBytes { $0.load(as: UInt64.self) }.bigEndian
moovSize = Int(ext64)
moovData.append(contentsOf: ext)
}
let remaining = moovSize - moovData.count
if remaining < 0 {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
return
}
if remaining > 0 {
var buffer = [UInt8](repeating: 0, count: remaining)
var total = 0
while total < remaining {
let readBytes = buffer.withUnsafeMutableBytes { ptr -> Int in
let base = ptr.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: total)
return inputStream.read(base, maxLength: remaining - total)
}
guard readBytes > 0 else {
func performMp4Restructure(inputStream: InputStream, mp4OptimizeInfo: Mp4OptimizeInfo) throws {
let offsetAccepted = inputStream.setProperty(mp4OptimizeInfo.moovOffset, forKey: .fileCurrentOffsetKey)
if offsetAccepted {
let moovDataBuffer = UnsafeMutablePointer.uint8pointer(of: mp4OptimizeInfo.moovSize)
defer { moovDataBuffer.deallocate() }
let moovRead = inputStream.read(moovDataBuffer, maxLength: mp4OptimizeInfo.moovSize)
if moovRead > 0 {
let data = Data(bytes: moovDataBuffer, count: moovRead)
let moovData = try mp4Restructure.restructureMoov(data: data)
delegate?.dataAvailable(source: self, data: moovData.initialData)
if !inputStream.setProperty(moovData.mdatOffset, forKey: .fileCurrentOffsetKey) {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
return
}
total += readBytes
} else {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
}
moovData.append(contentsOf: buffer)
}
let moovResult = try mp4Restructure.restructureMoov(data: moovData)
delegate?.dataAvailable(source: self, data: moovResult.initialData)
if !inputStream.setProperty(moovResult.mdatOffset, forKey: .fileCurrentOffsetKey) {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
} else {
delegate?.errorOccurred(source: self, error: inputStream.streamError ?? AudioSystemError.playerStartError)
}
}
@@ -36,7 +36,7 @@ enum Atoms {
static var cmov: Int { fourCcToInt("cmov") }
static var stco: Int { fourCcToInt("stco") }
static var co64: Int { fourCcToInt("co64") }
static var co64: Int { fourCcToInt("c064") }
static var atomPreampleSize: Int = 8
@@ -75,12 +75,6 @@ enum Mp4RestructureError: Error {
case networkError(Error)
}
enum OptimizeCheckResult: Equatable {
case optimized
case needsRestructure(moovOffset: Int)
case undetermined
}
final class Mp4Restructure {
private var atomOffset: Int = 0
@@ -135,36 +129,24 @@ final class Mp4Restructure {
return (initialData, mdatOffset)
}
/// Incrementally checks if the MP4 is optimized. Returns tri-state result.
func checkIsOptimized(data: Data) throws -> OptimizeCheckResult {
while atomOffset + 8 <= data.count {
var atomSize: Int = try Int(getInteger(data: data, offset: atomOffset) as UInt32)
let atomType: Int = try Int(getInteger(data: data, offset: atomOffset + 4) as UInt32)
var headerSize = 8
// Handle extended size (64-bit)
if atomSize == 1 {
if atomOffset + 16 > data.count { break }
let ext: UInt64 = try getInteger(data: data, offset: atomOffset + 8)
atomSize = Int(ext)
headerSize = 16
} else if atomSize == 0 {
// Size extends to EOF; with partial data we can't determine full box
break
}
// Bounds and sanity checks
if atomSize < headerSize || atomOffset + atomSize > data.count { break }
/// Returns `nil` if the data is optimized otherwise `Mp4OptimizeInfo`
func checkIsOptimized(data: Data) throws -> Mp4OptimizeInfo? {
while atomOffset < UInt64(data.count) {
var atomSize = try Int(getInteger(data: data, offset: atomOffset) as UInt32)
let atomType = try Int(getInteger(data: data, offset: atomOffset + 4) as UInt32)
switch atomType {
case Atoms.ftyp:
let start = atomOffset
let end = atomOffset + atomSize
let ftypData = data[start ..< end]
let ftypData = data[Int(atomOffset) ..< atomSize]
let ftyp = MP4Atom(type: atomType, size: atomSize, offset: atomOffset, data: ftypData)
self.ftyp = ftyp
atoms.append(ftyp)
case Atoms.mdat:
// ref: https://developer.apple.com/documentation/quicktime-file-format/movie_data_atom
// This atom can be quite large, and may exceed 2^32 bytes, in which case the size field will be set to 1,
// and the header will contain a 64-bit extended size field.
if atomSize == 1 {
atomSize = Int(try getInteger(data: data, offset: atomOffset + 8) as UInt64)
}
let mdat = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
atoms.append(mdat)
foundMdat = true
@@ -176,21 +158,19 @@ final class Mp4Restructure {
let atom = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
atoms.append(atom)
}
if ftyp != nil {
if foundMoov && !foundMdat {
Logger.debug("🕵️ detected an optimized mp4", category: .generic)
return .optimized
return nil
} else if !foundMoov && foundMdat {
Logger.debug("🕵️ detected a non-optimized mp4", category: .generic)
let possibleMoovOffset = atomOffset + atomSize
return .needsRestructure(moovOffset: possibleMoovOffset)
Logger.debug("🕵️ detected an non-optimized mp4", category: .generic)
let possibleMoovOffset = Int(atomOffset) + atomSize
return Mp4OptimizeInfo(moovOffset: possibleMoovOffset, moovSize: atomSize)
}
}
atomOffset += atomSize
}
return .undetermined
return nil
}
/// logic taken from qt-faststart.c over at ffmpeg
@@ -256,8 +236,6 @@ final class Mp4Restructure {
// the next integer determines the `Number of entries`
// https://developer.apple.com/documentation/quicktime-file-format/chunk_offset_atom/number_of_entries
let numberOfOffsetEntries = try Int(moovAtom.getInteger() as UInt32)
// Adjust by moov size
let adjustDelta = moovAtomSize
if atomType == Atoms.stco {
Logger.debug("🏗️ patching stco atom...", category: .generic)
if moovAtom.bytesAvailable < numberOfOffsetEntries * 4 {
@@ -268,7 +246,7 @@ final class Mp4Restructure {
for _ in 0 ..< numberOfOffsetEntries {
let currentOffset = try Int(moovAtom.getInteger(moovAtom.offset) as UInt32)
// adjust the offset by adding the size of moov atom
let adjustOffset = currentOffset + adjustDelta
let adjustOffset = currentOffset + moovAtomSize
if currentOffset < 0, adjustOffset >= 0 {
throw Mp4RestructureError.unableToRestructureData
@@ -283,8 +261,8 @@ final class Mp4Restructure {
}
for _ in 0 ..< numberOfOffsetEntries {
let currentOffset: Int = try moovAtom.getInteger(moovAtom.offset)
// adjust the offset by adding the size of moov atom (write as big-endian 64-bit)
moovAtom.put(UInt64(currentOffset + adjustDelta).bigEndian)
// adjust the offset by adding the size of moov atom
moovAtom.put(currentOffset + moovAtomSize)
}
}
}
@@ -293,10 +271,10 @@ final class Mp4Restructure {
func getInteger<T: FixedWidthInteger>(data: Data, offset: Int) throws -> T {
let sizeOfInteger = MemoryLayout<T>.size
guard offset >= 0, offset + sizeOfInteger <= data.count else {
guard sizeOfInteger <= data.count else {
throw ByteBuffer.Error.eof
}
let end = offset + sizeOfInteger
return T(data: data[offset ..< end]).bigEndian
let _offset = offset + sizeOfInteger
return T(data: data[_offset - sizeOfInteger ..< _offset]).bigEndian
}
}
@@ -75,15 +75,8 @@ final class RemoteMp4Restructure {
}
self.audioData.append(data)
do {
switch try self.mp4Restructure.checkIsOptimized(data: self.audioData) {
case .undetermined:
break // keep streaming until decision can be made
case .optimized:
self.audioData = Data()
self.task?.cancel()
self.task = nil
completion(.success(nil))
case let .needsRestructure(moovOffset):
let value = try self.mp4Restructure.checkIsOptimized(data: self.audioData)
if let value {
guard response.response?.statusCode == 206 else {
Logger.error("⛔️ mp4 error: no moov before mdat and the stream is not seekable", category: .networking)
completion(.failure(Mp4RestructureError.nonOptimizedMp4AndServerCannotSeek))
@@ -93,15 +86,22 @@ final class RemoteMp4Restructure {
self.audioData = Data()
self.task?.cancel()
self.task = nil
self.fetchAndRestructureMoovAtom(offset: moovOffset) { result in
self.fetchAndRestructureMoovAtom(offset: value.moovOffset) { result in
switch result {
case let .success(value):
let data = value.data
let offset = value.offset
self.dataOptimized = true
completion(.success(RestructuredData(initialData: value.data, mdatOffset: value.offset)))
completion(.success(RestructuredData(initialData: data, mdatOffset: offset)))
case let .failure(error):
completion(.failure(Mp4RestructureError.networkError(error)))
}
}
} else {
self.audioData = Data()
self.task?.cancel()
self.task = nil
completion(.success(nil))
}
} catch {
completion(.failure(Mp4RestructureError.invalidAtomSize))
@@ -132,8 +132,6 @@ final class RemoteMp4Restructure {
}
}
// removed warmup range helper
private func urlForPartialContent(with url: URL, offset: Int) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.networkServiceType = .avStreaming
@@ -13,20 +13,18 @@ enum RemoteAudioSourceError: Error {
}
public class RemoteAudioSource: AudioStreamSource {
public weak var delegate: AudioStreamSourceDelegate?
weak var delegate: AudioStreamSourceDelegate?
public var position: Int {
var position: Int {
return seekOffset + relativePosition
}
public var length: Int {
var length: Int {
guard let parsedHeader = parsedHeaderOutput else { return 0 }
return parsedHeader.fileLength
}
private let url: URL
private let httpMethod: String?
private let httpBody: Data?
private let networkingClient: NetworkingClient
private var streamRequest: NetworkDataStream?
@@ -42,7 +40,7 @@ public class RemoteAudioSource: AudioStreamSource {
private var shouldTryParsingIcycastHeaders: Bool = false
private let icycastHeadersProcessor: IcycastHeadersProcessor
public var audioFileHint: AudioFileTypeID {
var audioFileHint: AudioFileTypeID {
guard let output = parsedHeaderOutput, output.typeId != 0 else {
return audioFileType(fileExtension: url.pathExtension)
}
@@ -51,7 +49,7 @@ public class RemoteAudioSource: AudioStreamSource {
private let mp4Restructure: RemoteMp4Restructure
public let underlyingQueue: DispatchQueue
let underlyingQueue: DispatchQueue
let streamOperationQueue: OperationQueue
let netStatusService: NetStatusProvider
var waitingForNetwork = false
@@ -63,16 +61,12 @@ public class RemoteAudioSource: AudioStreamSource {
netStatusProvider: NetStatusProvider,
retrier: Retrier,
url: URL,
httpMethod: String?,
httpBody: Data?,
underlyingQueue: DispatchQueue,
httpHeaders: [String: String])
{
networkingClient = networking
metadataStreamProcessor = metadataStreamSource
self.url = url
self.httpMethod = httpMethod
self.httpBody = httpBody
additionalRequestHeaders = httpHeaders
relativePosition = 0
seekOffset = 0
@@ -89,11 +83,9 @@ public class RemoteAudioSource: AudioStreamSource {
mp4Restructure = RemoteMp4Restructure(url: url, networking: networkingClient)
startNetworkService()
}
convenience init(networking: NetworkingClient,
url: URL,
httpMethod: String?,
httpBody: Data?,
underlyingQueue: DispatchQueue,
httpHeaders: [String: String])
{
@@ -108,21 +100,6 @@ public class RemoteAudioSource: AudioStreamSource {
netStatusProvider: netStatusProvider,
retrier: retrierTimeout,
url: url,
httpMethod: httpMethod,
httpBody: httpBody,
underlyingQueue: underlyingQueue,
httpHeaders: httpHeaders)
}
convenience init(networking: NetworkingClient,
url: URL,
underlyingQueue: DispatchQueue,
httpHeaders: [String: String])
{
self.init(networking: networking,
url: url,
httpMethod: nil,
httpBody: nil,
underlyingQueue: underlyingQueue,
httpHeaders: httpHeaders)
}
@@ -137,7 +114,7 @@ public class RemoteAudioSource: AudioStreamSource {
httpHeaders: [:])
}
public func close() {
func close() {
retrierTimeout.cancel()
streamOperationQueue.isSuspended = false
streamOperationQueue.cancelAllOperations()
@@ -148,7 +125,7 @@ public class RemoteAudioSource: AudioStreamSource {
streamRequest = nil
}
public func seek(at offset: Int) {
func seek(at offset: Int) {
close()
relativePosition = 0
@@ -167,11 +144,11 @@ public class RemoteAudioSource: AudioStreamSource {
performOpen(seek: offset)
}
public func suspend() {
func suspend() {
streamOperationQueue.isSuspended = true
}
public func resume() {
func resume() {
streamOperationQueue.isSuspended = false
}
@@ -370,8 +347,6 @@ public class RemoteAudioSource: AudioStreamSource {
urlRequest.networkServiceType = .avStreaming
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
urlRequest.timeoutInterval = 60
urlRequest.httpMethod = httpMethod
urlRequest.httpBody = httpBody
for header in additionalRequestHeaders {
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
@@ -391,8 +366,6 @@ public class RemoteAudioSource: AudioStreamSource {
urlRequest.networkServiceType = .avStreaming
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
urlRequest.timeoutInterval = 60
urlRequest.httpMethod = httpMethod
urlRequest.httpBody = httpBody
for header in additionalRequestHeaders {
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
@@ -81,16 +81,6 @@ open class AudioPlayer {
return entry.progress
}
/// The number of audio frames that have been played
public var framesPlayed: Int {
guard playerContext.internalState != .pendingNext else { return 0 }
playerContext.entriesLock.lock()
let playingEntry = playerContext.audioPlayingEntry
playerContext.entriesLock.unlock()
guard let entry = playingEntry else { return 0 }
return entry.framesPlayed
}
public private(set) var customAttachedNodes = [AVAudioNode]()
/// The current configuration of the player.
@@ -134,7 +124,7 @@ open class AudioPlayer {
private let frameFilterProcessor: FrameFilterProcessor
private let serializationQueue: DispatchQueue
public let sourceQueue: DispatchQueue
private let sourceQueue: DispatchQueue
private let entryProvider: AudioEntryProviding
@@ -200,31 +190,6 @@ open class AudioPlayer {
/// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request.
public func play(url: URL, headers: [String: String]) {
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
play(audioEntry: audioEntry)
}
/// Starts the audio playback for the given URL
///
/// - parameter url: A `URL` specifying the audio context to be played.
/// - parameter httpMethod: A `String` specifying the HTTP method to use (e.g. "GET", "POST").
/// - parameter httpBody: A "Data" specifying the HTTP request body, if any.
/// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request.
public func play(url: URL, httpMethod: String?, httpBody: Data?, headers: [String: String]) {
let audioEntry = entryProvider.provideAudioEntry(url: url, httpMethod: httpMethod, httpBody: httpBody, headers: headers)
play(audioEntry: audioEntry)
}
/// Starts the audio playback for the supplied stream
///
/// - parameter source: A `CoreAudioStreamSource` that will providing streaming data
/// - parameter entryId: A `String` that provides a unique id for this item
/// - parameter format: An `AVAudioFormat` the format of this audio source
public func play(source: CoreAudioStreamSource, entryId: String, format: AVAudioFormat) {
let audioEntry = AudioEntry(source: source, entryId: AudioEntryId(id: entryId), outputAudioFormat: format)
play(audioEntry: audioEntry)
}
private func play(audioEntry: AudioEntry) {
audioEntry.delegate = self
checkRenderWaitingAndNotifyIfNeeded()
@@ -282,16 +247,6 @@ open class AudioPlayer {
queue(url: url, headers: [:], after: afterUrl)
}
/// Queues the specified audio stream
///
/// - parameter source: A `CoreAudioStreamSource` that will providing streaming data
/// - parameter entryId: A `String` that provides a unique id for this item
/// - parameter format: An `AVAudioFormat` the format of this audio source
public func queue(source: CoreAudioStreamSource, entryId: String, format: AVAudioFormat) {
let audioEntry = AudioEntry(source: source, entryId: AudioEntryId(id: entryId), outputAudioFormat: format)
queue(audioEntry: audioEntry)
}
public func removeFromQueue(url: URL) {
serializationQueue.sync {
if let item = entriesQueue.items(type: .upcoming).first(where: { $0.id.id == url.absoluteString }) {
@@ -313,8 +268,21 @@ open class AudioPlayer {
/// - 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], after afterUrl: URL? = nil) {
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
queue(audioEntry: audioEntry, after: afterUrl)
serializationQueue.sync {
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
audioEntry.delegate = self
if let afterUrl = afterUrl {
if let afterUrlEntry = entriesQueue.items(type: .upcoming).first(where: { $0.id.id == afterUrl.absoluteString }) {
entriesQueue.insert(item: audioEntry, type: .upcoming, after: afterUrlEntry)
}
} else {
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
}
}
checkRenderWaitingAndNotifyIfNeeded()
sourceQueue.async { [weak self] in
self?.processSource()
}
}
/// Queues the specified URLs
@@ -335,23 +303,6 @@ open class AudioPlayer {
}
}
private func queue(audioEntry: AudioEntry, after afterUrl: URL? = nil) {
serializationQueue.sync {
audioEntry.delegate = self
if let afterUrl = afterUrl {
if let afterUrlEntry = entriesQueue.items(type: .upcoming).first(where: { $0.id.id == afterUrl.absoluteString }) {
entriesQueue.insert(item: audioEntry, type: .upcoming, after: afterUrlEntry)
}
} else {
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
}
}
checkRenderWaitingAndNotifyIfNeeded()
sourceQueue.async { [weak self] in
self?.processSource()
}
}
/// Stops the audio playback
public func stop(clearQueue: Bool = true) {
guard playerContext.internalState != .stopped else { return }
@@ -651,22 +602,18 @@ open class AudioPlayer {
guard playerContext.internalState != .paused else { return }
let snapshot = playerContext.entriesLock.withLock {
(reading: playerContext.audioReadingEntry, playing: playerContext.audioPlayingEntry)
}
if playerContext.internalState == .pendingNext {
let entry = entriesQueue.dequeue(type: .upcoming)
playerContext.setInternalState(to: .waitingForData)
setCurrentReading(entry: entry, startPlaying: true, shouldClearQueue: true)
rendererContext.resetBuffers()
} else if let playingEntry = snapshot.playing,
} else if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested,
playingEntry != snapshot.reading
playingEntry != playerContext.audioReadingEntry
{
playingEntry.audioStreamState.processedDataFormat = false
playingEntry.reset()
if let readingEntry = snapshot.reading {
if let readingEntry = playerContext.audioReadingEntry {
readingEntry.delegate = nil
readingEntry.close()
}
@@ -681,20 +628,20 @@ open class AudioPlayer {
setCurrentReading(entry: playingEntry, startPlaying: true, shouldClearQueue: false)
}
} else if snapshot.reading == nil {
} else if playerContext.audioReadingEntry == nil {
if entriesQueue.count(for: .upcoming) > 0 {
let entry = entriesQueue.dequeue(type: .upcoming)
let shouldStartPlaying = snapshot.playing == nil
let shouldStartPlaying = playerContext.audioPlayingEntry == nil
playerContext.setInternalState(to: .waitingForData)
setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: false)
} else if snapshot.playing == nil {
} else if playerContext.audioPlayingEntry == nil {
if playerContext.internalState != .stopped {
stopEngine(reason: .eof)
}
}
}
if let playingEntry = snapshot.playing,
if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.audioStreamState.processedDataFormat,
playingEntry.calculatedBitrate() > 0.0
{
@@ -858,7 +805,7 @@ open class AudioPlayer {
}
extension AudioPlayer: AudioStreamSourceDelegate {
public func dataAvailable(source: CoreAudioStreamSource, data: Data) {
func dataAvailable(source: CoreAudioStreamSource, data: Data) {
guard let readingEntry = playerContext.audioReadingEntry, readingEntry.has(same: source) else {
return
}
@@ -888,12 +835,12 @@ extension AudioPlayer: AudioStreamSourceDelegate {
}
}
public func errorOccurred(source: CoreAudioStreamSource, error: Error) {
func errorOccurred(source: CoreAudioStreamSource, error: Error) {
guard let entry = playerContext.audioReadingEntry, entry.has(same: source) else { return }
raiseUnexpected(error: .networkError(.failure(error)))
}
public func endOfFileOccurred(source: CoreAudioStreamSource) {
func endOfFileOccurred(source: CoreAudioStreamSource) {
let hasSameSource = playerContext.audioReadingEntry?.has(same: source) ?? false
guard playerContext.audioReadingEntry == nil || hasSameSource else {
source.delegate = nil
@@ -930,7 +877,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
}
}
public func metadataReceived(data: [String: String]) {
func metadataReceived(data: [String: String]) {
asyncOnMain { [weak self] in
guard let self = self else { return }
self.delegate?.audioPlayerDidReadMetadata(player: self, metadata: data)
@@ -26,7 +26,7 @@ public struct AudioPlayerConfiguration: Equatable {
bufferSizeInSeconds: 10,
secondsRequiredToStartPlaying: 1,
gracePeriodAfterSeekInSeconds: 0.5,
secondsRequiredToStartPlayingAfterBufferUnderrun: 7,
secondsRequiredToStartPlayingAfterBufferUnderrun: 1,
enableLogs: false)
/// Initializes the configuration for the `AudioPlayer`
///
@@ -13,11 +13,11 @@ extension AudioPlayer {
static let initial = InternalState([])
static let running = InternalState(rawValue: 1)
static let playing = InternalState(rawValue: (1 << 1) | InternalState.running.rawValue)
static let rebuffering = InternalState(rawValue: (1 << 2) | InternalState.running.rawValue)
static let waitingForData = InternalState(rawValue: (1 << 3) | InternalState.running.rawValue)
static let waitingForDataAfterSeek = InternalState(rawValue: (1 << 4) | InternalState.running.rawValue)
static let paused = InternalState(rawValue: (1 << 5) | InternalState.running.rawValue)
static let playing = InternalState(rawValue: 1 << 1 | InternalState.running.rawValue)
static let rebuffering = InternalState(rawValue: 1 << 2 | InternalState.running.rawValue)
static let waitingForData = InternalState(rawValue: 1 << 3 | InternalState.running.rawValue)
static let waitingForDataAfterSeek = InternalState(rawValue: 1 << 4 | InternalState.running.rawValue)
static let paused = InternalState(rawValue: 1 << 5 | InternalState.running.rawValue)
static let stopped = InternalState(rawValue: 1 << 9)
static let pendingNext = InternalState(rawValue: 1 << 10)
static let disposed = InternalState(rawValue: 1 << 30)
@@ -55,7 +55,7 @@ func playerStateAndStopReason(
// MARK: Public States
public enum AudioPlayerState: Equatable, Sendable {
public enum AudioPlayerState: Equatable {
case ready
case running
case playing
@@ -66,7 +66,7 @@ public enum AudioPlayerState: Equatable, Sendable {
case disposed
}
public enum AudioPlayerStopReason: Equatable, Sendable {
public enum AudioPlayerStopReason: Equatable {
case none
case eof
case userAction
@@ -74,7 +74,7 @@ public enum AudioPlayerStopReason: Equatable, Sendable {
case disposed
}
public enum AudioPlayerError: LocalizedError, Equatable, Sendable {
public enum AudioPlayerError: LocalizedError, Equatable {
case streamParseBytesFailure(AudioFileStreamError)
case audioSystemError(AudioSystemError)
case codecError
@@ -100,7 +100,7 @@ public enum AudioPlayerError: LocalizedError, Equatable, Sendable {
}
}
public enum AudioSystemError: LocalizedError, Equatable, Sendable {
public enum AudioSystemError: LocalizedError, Equatable {
case engineFailure
case playerNotFound
case playerStartError
@@ -20,9 +20,9 @@ final class AudioRendererContext {
let packetsSemaphore = DispatchSemaphore(value: 0)
let framesRequiredToStartPlaying: Double
let framesRequiredAfterRebuffering: Double
let framesRequiredForDataAfterSeekPlaying: Double
let framesRequiredToStartPlaying: UInt32
let framesRequiredAfterRebuffering: UInt32
let framesRequiredForDataAfterSeekPlaying: UInt32
let waitingForDataAfterSeekFrameCount = Atomic<Int32>(0)
@@ -33,9 +33,9 @@ final class AudioRendererContext {
let canonicalStream = outputAudioFormat.basicStreamDescription
framesRequiredToStartPlaying = Double(canonicalStream.mSampleRate) * Double(configuration.secondsRequiredToStartPlaying)
framesRequiredAfterRebuffering = Double(canonicalStream.mSampleRate) * Double(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
framesRequiredForDataAfterSeekPlaying = Double(canonicalStream.mSampleRate) * Double(configuration.gracePeriodAfterSeekInSeconds)
framesRequiredToStartPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlaying)
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
framesRequiredForDataAfterSeekPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.gracePeriodAfterSeekInSeconds)
let dataByteSize = Int(canonicalStream.mSampleRate * configuration.bufferSizeInSeconds) * Int(canonicalStream.mBytesPerFrame)
inOutAudioBufferList = allocateBufferList(dataByteSize: dataByteSize)
@@ -6,7 +6,6 @@
//
import AVFoundation
import CoreAudio
enum AudioConvertStatus: Int32 {
case done = 100
@@ -105,8 +104,6 @@ final class AudioFileStreamProcessor {
let dataLengthInBytes = Double(readingEntry.audioDataLengthBytes())
let entryDuration = readingEntry.duration()
let duration = entryDuration < readingEntry.progress && entryDuration > 0 ? readingEntry.progress : entryDuration
guard duration > 0.0 else { return }
var seekByteOffset = Int64(dataOffset + (readingEntry.seekRequest.time / duration) * dataLengthInBytes)
@@ -229,17 +226,12 @@ final class AudioFileStreamProcessor {
processDataByteCount(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_AudioDataPacketCount:
processAudioDataPacketCount(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_BitRate:
processBitRate(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_ReadyToProducePackets:
// check converter for discontinuous stream
assignMagicCookieToConverterIfNeeded()
processPacketUpperBoundAndMaxPacketSize(entry: entry, fileStream: fileStream)
processReadyToProducePackets(entry: entry, fileStream: fileStream)
processPacketUpperBoundAndMaxPacketSize(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_FormatList:
processFormatList(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_PacketTableInfo:
processPacketTableInfo(entry: entry, fileStream: fileStream)
default:
break
}
@@ -250,7 +242,7 @@ final class AudioFileStreamProcessor {
private func processDataOffset(entry: AudioEntry, fileStream: AudioFileStreamID) {
var offset: UInt64 = 0
fileStreamGetProperty(value: &offset, fileStream: fileStream, propertyId: kAudioFileStreamProperty_DataOffset)
entry.lock.lock(); defer { entry.lock.unlock() }
entry.lock.lock(); defer { playerContext.audioReadingEntry?.lock.unlock() }
entry.audioStreamState.processedDataFormat = true
entry.audioStreamState.dataOffset = offset
}
@@ -261,9 +253,7 @@ final class AudioFileStreamProcessor {
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_AudioDataPacketCount, &packetCountSize, &packetCount)
entry.lock.lock(); defer { entry.lock.unlock() }
entry.audioStreamState.dataPacketCount = Double(packetCount)
let entryFormatID = entry.audioStreamFormat.mFormatID
let isFLAC = entryFormatID == kAudioFormatFLAC
if entryFormatID != kAudioFormatLinearPCM && !isFLAC {
if entry.audioStreamFormat.mFormatID != kAudioFormatLinearPCM {
discontinuous = true
}
}
@@ -343,56 +333,28 @@ final class AudioFileStreamProcessor {
entry.audioStreamState.dataPacketOffset = audioDataPacketCount
}
private func processBitRate(entry: AudioEntry, fileStream: AudioFileStreamID) {
var bitRate: UInt32 = 0
let status = fileStreamGetProperty(value: &bitRate, fileStream: fileStream, propertyId: kAudioFileStreamProperty_BitRate)
guard status == noErr else { return }
entry.lock.lock(); defer { entry.lock.unlock() }
entry.audioStreamState.bitRate = Double(bitRate)
}
private func processPacketTableInfo(entry: AudioEntry, fileStream: AudioFileStreamID) {
var pti = AudioFilePacketTableInfo(mNumberValidFrames: 0,
mPrimingFrames: 0,
mRemainderFrames: 0)
let status = fileStreamGetProperty(value: &pti, fileStream: fileStream, propertyId: kAudioFileStreamProperty_PacketTableInfo)
guard status == noErr else { return }
// Use valid frames to refine duration if present
entry.lock.lock(); defer { entry.lock.unlock() }
if pti.mNumberValidFrames > 0 {
entry.audioStreamState.dataPacketCount = Double(pti.mNumberValidFrames) / Double(max(1, entry.audioStreamFormat.mFramesPerPacket))
}
}
private func processFormatList(entry: AudioEntry, fileStream: AudioFileStreamID) {
entry.lock.lock(); defer { entry.lock.unlock() }
let info = fileStreamGetPropertyInfo(fileStream: fileStream, propertyId: kAudioFileStreamProperty_FormatList)
guard info.status == noErr, info.size > 0 else { return }
let itemStride = MemoryLayout<AudioFormatListItem>.stride
let itemCount = Int(info.size) / itemStride
guard itemCount > 0 else { return }
var list = [AudioFormatListItem](repeating: AudioFormatListItem(), count: itemCount)
var size = UInt32(itemCount * itemStride)
guard info.status == noErr else { return }
var list: [AudioFormatListItem] = Array(repeating: AudioFormatListItem(), count: Int(info.size))
var size = UInt32(info.size)
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_FormatList, &size, &list)
var chosenASBD: AudioStreamBasicDescription?
for i in 0..<itemCount {
let step = MemoryLayout<AudioFormatListItem>.size
var i = 0
while i * step < size {
let asbd = list[i].mASBD
let formatId = asbd.mFormatID
if formatId == kAudioFormatMPEG4AAC_HE || formatId == kAudioFormatMPEG4AAC_HE_V2 {
chosenASBD = asbd
playerContext.audioReadingEntry?.audioStreamFormat = asbd
break
}
if chosenASBD == nil {
chosenASBD = asbd
}
i += step
}
if let asbd = chosenASBD {
entry.lock.withLock { entry.audioStreamFormat = asbd }
if fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
createAudioConverter(from: asbd, to: outputAudioFormat)
if fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
if let inputStreamFormat = playerContext.audioReadingEntry?.audioStreamFormat {
createAudioConverter(from: inputStreamFormat, to: outputAudioFormat)
}
}
}
@@ -408,11 +370,6 @@ final class AudioFileStreamProcessor {
guard let entry = playerContext.audioReadingEntry else { return }
guard entry.audioStreamState.processedDataFormat else { return }
guard let converter = audioConverter else {
Logger.error("Couldn't find audio converter", category: .audioRendering)
return
}
if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
{
@@ -423,24 +380,25 @@ final class AudioFileStreamProcessor {
return
}
guard let converter = audioConverter else {
Logger.error("Couldn't find audio converter", category: .audioRendering)
return
}
// reset discontinuity
discontinuous = false
var convertInfo = AudioConvertInfo(
done: false,
numberOfPackets: inNumberPackets,
packDescription: inPacketDescriptions
)
var convertInfo = AudioConvertInfo(done: false,
numberOfPackets: inNumberPackets,
packDescription: inPacketDescriptions)
convertInfo.audioBuffer.mData = UnsafeMutableRawPointer(mutating: inInputData)
convertInfo.audioBuffer.mDataByteSize = inNumberBytes
if let playingAudioStreamFormat = playerContext.audioPlayingEntry?.audioStreamFormat {
convertInfo.audioBuffer.mNumberChannels = playingAudioStreamFormat.mChannelsPerFrame
}
updateProcessedPackets(
inPacketDescriptions: inPacketDescriptions,
inNumberPackets: inNumberPackets
)
updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
inNumberPackets: inNumberPackets)
var status: OSStatus = noErr
packetProcess: while status == noErr {
@@ -448,7 +406,7 @@ final class AudioFileStreamProcessor {
let bufferContext = rendererContext.bufferContext
var used = bufferContext.frameUsedCount
var start = bufferContext.frameStartIndex
var end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount
var end = bufferContext.end
var framesLeftInBuffer = bufferContext.totalFrameCount - used
rendererContext.lock.unlock()
@@ -64,30 +64,29 @@ final class AudioPlayerRenderProcessor: NSObject {
let frameSizeInBytes = bufferContext.sizeInBytes
let used = bufferContext.frameUsedCount
let start = bufferContext.frameStartIndex
let end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount
let end = bufferContext.end
let signal = rendererContext.waiting.value && used < bufferContext.totalFrameCount / 2
if let playingEntry = playingEntry {
playingEntry.lock.lock()
let framesState = playingEntry.framesState
playingEntry.lock.unlock()
if state == .waitingForData {
var requiredFramesToStart = rendererContext.framesRequiredToStartPlaying
if framesState.lastFrameQueued >= 0 {
requiredFramesToStart = min(requiredFramesToStart, Double(playingEntry.framesState.lastFrameQueued))
requiredFramesToStart = min(requiredFramesToStart, UInt32(playingEntry.framesState.lastFrameQueued))
}
if readingEntry === playingEntry, framesState.queued < Int(requiredFramesToStart)
if let readingEntry = readingEntry, readingEntry === playingEntry,
framesState.queued < requiredFramesToStart
{
waitForBuffer = true
}
} else if state == .rebuffering {
var requiredFramesToStart = rendererContext.framesRequiredAfterRebuffering
if framesState.lastFrameQueued >= 0 {
requiredFramesToStart = min(requiredFramesToStart, Double(framesState.lastFrameQueued - framesState.queued))
requiredFramesToStart = min(requiredFramesToStart, UInt32(framesState.lastFrameQueued - framesState.queued))
}
if used < Int(requiredFramesToStart) {
if used < requiredFramesToStart {
waitForBuffer = true
}
} else if state == .waitingForDataAfterSeek {
@@ -103,7 +102,7 @@ final class AudioPlayerRenderProcessor: NSObject {
rendererContext.lock.unlock()
var totalFramesCopied: UInt32 = 0
if used > 0 && !waitForBuffer && playingEntry != nil && state.contains(.running) && state != .paused {
if used > 0 && !waitForBuffer && state.contains(.running) && state != .paused {
if end > start {
let framesToCopy = min(inNumberFrames, used)
bufferList.mBuffers.mNumberChannels = 2
@@ -163,7 +162,6 @@ final class AudioPlayerRenderProcessor: NSObject {
bufferContext.frameUsedCount -= totalFramesCopied
rendererContext.lock.unlock()
}
if playerContext.internalState != .playing {
playerContext.setInternalState(to: .playing, when: { state -> Bool in
state.contains(.running) && state != .paused
@@ -177,7 +175,7 @@ final class AudioPlayerRenderProcessor: NSObject {
memset(mData + Int(totalFramesCopied * frameSizeInBytes), 0, Int(delta * frameSizeInBytes))
}
if !(playingEntry == nil || state == .waitingForDataAfterSeek || state == .waitingForData || state == .rebuffering) {
if playingEntry != nil || AudioPlayer.InternalState.waiting.contains(state) {
if playerContext.internalState != .rebuffering {
playerContext.setInternalState(to: .rebuffering, when: { state -> Bool in
state.contains(.running) && state != .paused
@@ -186,7 +184,7 @@ final class AudioPlayerRenderProcessor: NSObject {
} else if state == .waitingForDataAfterSeek {
if totalFramesCopied == 0 {
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 += Int32(inNumberFrames - totalFramesCopied) }
if rendererContext.waitingForDataAfterSeekFrameCount.value > Int(rendererContext.framesRequiredForDataAfterSeekPlaying) {
if rendererContext.waitingForDataAfterSeekFrameCount.value > rendererContext.framesRequiredForDataAfterSeekPlaying {
if playerContext.internalState != .playing {
playerContext.setInternalState(to: .playing) { state -> Bool in
state.contains(.running) && state != .playing
@@ -5,11 +5,13 @@
import AVFoundation
private let outputChannels: UInt32 = 2
enum UnitDescriptions {
static let output: AudioComponentDescription = {
static var output: AudioComponentDescription = {
var desc = AudioComponentDescription()
desc.componentType = kAudioUnitType_Output
#if os(iOS) || os(tvOS)
#if os(iOS)
desc.componentSubType = kAudioUnitSubType_RemoteIO
#else
desc.componentSubType = kAudioUnitSubType_DefaultOutput
@@ -33,7 +33,6 @@ let fileTypesFromMimeType: [String: AudioFileTypeID] =
"video/3gpp": kAudioFile3GPType,
"audio/3gp2": kAudioFile3GP2Type,
"video/3gp2": kAudioFile3GP2Type,
"audio/flac": kAudioFileFLACType
]
/// Method that converts mime type to AudioFileTypeID
+1 -2
View File
@@ -6,8 +6,7 @@ let package = Package(
name: "AudioStreaming",
platforms: [
.iOS(.v12),
.macOS(.v13),
.tvOS(.v16)
.macOS(.v13)
],
products: [
.library(
+29 -3
View File
@@ -8,7 +8,7 @@ Under the hood `AudioStreaming` uses `AVAudioEngine` and `CoreAudio` for playbac
#### Supported audio
- Online streaming (Shoutcast/ICY streams) with metadata parsing
- AIFF, AIFC, WAVE, CAF, NeXT, ADTS, MPEG Audio Layer 3, AAC audio formats
- M4A
- M4A (_Optimized files only_)
As of 1.2.0 version, there's support for non-optimized M4A, please report any issues
@@ -18,8 +18,6 @@ Known limitations:
# Requirements
- iOS 13.0+
- macOS 13.0+
- tvOS 16.0+
- Swift 5.x
# Using AudioStreaming
@@ -165,11 +163,39 @@ Under the hood the concrete class for frame filters, `FrameFilterProcessor` inst
# Installation
### Cocoapods
[Cocoapods](https://cocoapods.org/) is a dependency manager for Cocoa projects. You can install it with the following command:
```
$ gem install cocoapods
```
To intergrate AudioStreaming with [Cocoapods](https://cocoapods.org/) to your Xcode project add the following to your `Podfile`:
```
pod 'AudioStreaming'
```
### Swift Package Manager
On Xcode 11.0+ you can add a new dependency by going to **File / Swift Packages / Add Package Dependency...**
and enter package repository URL https://github.com/dimitris-c/AudioStreaming.git, then follow the instructions.
### Carthage
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with frameworks.
You can install Carthage with Homebrew using the following command:
```
$ brew update
$ brew install carthage
```
To integrate AudioStreaming into your Xcode project using Carthage, add the following to your `Cartfile`:
```
github "dimitris-c/AudioStreaming"
```
Visit [installation instructions](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) on Carthage to install the framework
# Licence
AudioStreaming is available under the MIT license. See the LICENSE file for more info.