Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f3c7912e8 | |||
| 548d599628 | |||
| 17f532556a | |||
| 00bd6cd81b | |||
| ce2b88ac03 | |||
| 624e575980 |
@@ -7,6 +7,7 @@
|
||||
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 */; };
|
||||
@@ -47,6 +48,7 @@
|
||||
/* 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>"; };
|
||||
@@ -195,6 +197,7 @@
|
||||
98E3921C2BD845E100B586E9 /* AudioPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
42BE42F42C9322AA00C0E448 /* CustomStreamSource.swift */,
|
||||
9806E8302BC6927D00757370 /* AudioPlayerModel.swift */,
|
||||
9806E8292BC68F8700757370 /* AudioPlayerView.swift */,
|
||||
98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */,
|
||||
@@ -292,6 +295,7 @@
|
||||
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;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import AVFoundation
|
||||
import SwiftUI
|
||||
import AudioStreaming
|
||||
|
||||
struct AudioPlayerControls: View {
|
||||
@State var model: Model
|
||||
@@ -247,11 +248,23 @@ extension AudioPlayerControls {
|
||||
func play(_ track: AudioTrack) {
|
||||
if track != currentTrack {
|
||||
currentTrack?.status = .idle
|
||||
audioPlayerService.play(url: track.url)
|
||||
currentTrack = track
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createStreamSource() -> CoreAudioStreamSource {
|
||||
return CustomStreamAudioSource(underlyingQueue: audioPlayerService.player.sourceQueue)
|
||||
}
|
||||
|
||||
func onTick() {
|
||||
let duration = audioPlayerService.duration
|
||||
let progress = audioPlayerService.progress
|
||||
|
||||
@@ -59,11 +59,13 @@ 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")]
|
||||
|
||||
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: "Tracks", tracks: audioTracks.map { AudioTrack.init(from:$0) }),
|
||||
AudioPlaylist(title: "Generated", tracks: customStreams.map { AudioTrack.init(from:$0) })
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// 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?
|
||||
|
||||
private var player: AudioPlayer
|
||||
var player: AudioPlayer
|
||||
private var audioSystemResetObserver: Any?
|
||||
|
||||
var duration: Double {
|
||||
@@ -60,6 +60,11 @@ 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)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '1.2.5'
|
||||
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
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 55;
|
||||
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 */; };
|
||||
B5AEDBB824744153007D8101 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AEDBAE24744153007D8101 /* AudioStreaming.framework */; platformFilters = (ios, tvos, ); };
|
||||
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,8 +528,9 @@
|
||||
B5AEDBA524744153007D8101 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1140;
|
||||
LastUpgradeCheck = 1200;
|
||||
LastUpgradeCheck = 1620;
|
||||
ORGANIZATIONNAME = Decimal;
|
||||
TargetAttributes = {
|
||||
B5AEDBAD24744153007D8101 = {
|
||||
@@ -587,6 +588,7 @@
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
B583864B2545858E0087A712 /* SwiftLint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
@@ -683,6 +685,10 @@
|
||||
/* Begin PBXTargetDependency section */
|
||||
B5AEDBBA24744153007D8101 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
platformFilters = (
|
||||
ios,
|
||||
tvos,
|
||||
);
|
||||
target = B5AEDBAD24744153007D8101 /* AudioStreaming */;
|
||||
targetProxy = B5AEDBB924744153007D8101 /* PBXContainerItemProxy */;
|
||||
};
|
||||
@@ -727,6 +733,7 @@
|
||||
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;
|
||||
@@ -743,7 +750,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -793,6 +800,7 @@
|
||||
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;
|
||||
@@ -803,7 +811,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -819,12 +827,14 @@
|
||||
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;
|
||||
@@ -833,17 +843,18 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.5;
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = "1,2,3";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -851,12 +862,14 @@
|
||||
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;
|
||||
@@ -865,23 +878,23 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.5;
|
||||
MARKETING_VERSION = 1.2.7;
|
||||
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = "1,2,3";
|
||||
};
|
||||
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;
|
||||
@@ -893,16 +906,17 @@
|
||||
);
|
||||
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";
|
||||
TARGETED_DEVICE_FAMILY = "1,2,3";
|
||||
};
|
||||
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;
|
||||
@@ -914,8 +928,10 @@
|
||||
);
|
||||
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";
|
||||
TARGETED_DEVICE_FAMILY = "1,2,3";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1200"
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import AudioToolbox
|
||||
import Foundation
|
||||
|
||||
protocol AudioStreamSourceDelegate: AnyObject {
|
||||
public protocol AudioStreamSourceDelegate: AnyObject {
|
||||
/// Indicates that there's data available
|
||||
func dataAvailable(source: CoreAudioStreamSource, data: Data)
|
||||
/// Indicates an error occurred
|
||||
@@ -17,7 +17,7 @@ protocol AudioStreamSourceDelegate: AnyObject {
|
||||
func metadataReceived(data: [String: String])
|
||||
}
|
||||
|
||||
protocol CoreAudioStreamSource: AnyObject {
|
||||
public protocol CoreAudioStreamSource: AnyObject {
|
||||
/// An `Int` that represents the position of the audio
|
||||
var position: Int { get }
|
||||
/// The length of the audio in bytes
|
||||
|
||||
@@ -13,13 +13,13 @@ enum RemoteAudioSourceError: Error {
|
||||
}
|
||||
|
||||
public class RemoteAudioSource: AudioStreamSource {
|
||||
weak var delegate: AudioStreamSourceDelegate?
|
||||
public weak var delegate: AudioStreamSourceDelegate?
|
||||
|
||||
var position: Int {
|
||||
public var position: Int {
|
||||
return seekOffset + relativePosition
|
||||
}
|
||||
|
||||
var length: Int {
|
||||
public var length: Int {
|
||||
guard let parsedHeader = parsedHeaderOutput else { return 0 }
|
||||
return parsedHeader.fileLength
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
private var shouldTryParsingIcycastHeaders: Bool = false
|
||||
private let icycastHeadersProcessor: IcycastHeadersProcessor
|
||||
|
||||
var audioFileHint: AudioFileTypeID {
|
||||
public var audioFileHint: AudioFileTypeID {
|
||||
guard let output = parsedHeaderOutput, output.typeId != 0 else {
|
||||
return audioFileType(fileExtension: url.pathExtension)
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
|
||||
private let mp4Restructure: RemoteMp4Restructure
|
||||
|
||||
let underlyingQueue: DispatchQueue
|
||||
public let underlyingQueue: DispatchQueue
|
||||
let streamOperationQueue: OperationQueue
|
||||
let netStatusService: NetStatusProvider
|
||||
var waitingForNetwork = false
|
||||
@@ -114,7 +114,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
httpHeaders: [:])
|
||||
}
|
||||
|
||||
func close() {
|
||||
public func close() {
|
||||
retrierTimeout.cancel()
|
||||
streamOperationQueue.isSuspended = false
|
||||
streamOperationQueue.cancelAllOperations()
|
||||
@@ -125,7 +125,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
streamRequest = nil
|
||||
}
|
||||
|
||||
func seek(at offset: Int) {
|
||||
public func seek(at offset: Int) {
|
||||
close()
|
||||
|
||||
relativePosition = 0
|
||||
@@ -144,11 +144,11 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
performOpen(seek: offset)
|
||||
}
|
||||
|
||||
func suspend() {
|
||||
public func suspend() {
|
||||
streamOperationQueue.isSuspended = true
|
||||
}
|
||||
|
||||
func resume() {
|
||||
public func resume() {
|
||||
streamOperationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ open class AudioPlayer {
|
||||
private let frameFilterProcessor: FrameFilterProcessor
|
||||
|
||||
private let serializationQueue: DispatchQueue
|
||||
private let sourceQueue: DispatchQueue
|
||||
public let sourceQueue: DispatchQueue
|
||||
|
||||
private let entryProvider: AudioEntryProviding
|
||||
|
||||
@@ -190,6 +190,20 @@ 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 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()
|
||||
@@ -247,6 +261,16 @@ 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 }) {
|
||||
@@ -268,21 +292,8 @@ 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) {
|
||||
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()
|
||||
}
|
||||
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
|
||||
queue(audioEntry: audioEntry, after: afterUrl)
|
||||
}
|
||||
|
||||
/// Queues the specified URLs
|
||||
@@ -303,6 +314,23 @@ 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 }
|
||||
@@ -805,7 +833,7 @@ open class AudioPlayer {
|
||||
}
|
||||
|
||||
extension AudioPlayer: AudioStreamSourceDelegate {
|
||||
func dataAvailable(source: CoreAudioStreamSource, data: Data) {
|
||||
public func dataAvailable(source: CoreAudioStreamSource, data: Data) {
|
||||
guard let readingEntry = playerContext.audioReadingEntry, readingEntry.has(same: source) else {
|
||||
return
|
||||
}
|
||||
@@ -835,12 +863,12 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func errorOccurred(source: CoreAudioStreamSource, error: Error) {
|
||||
public func errorOccurred(source: CoreAudioStreamSource, error: Error) {
|
||||
guard let entry = playerContext.audioReadingEntry, entry.has(same: source) else { return }
|
||||
raiseUnexpected(error: .networkError(.failure(error)))
|
||||
}
|
||||
|
||||
func endOfFileOccurred(source: CoreAudioStreamSource) {
|
||||
public func endOfFileOccurred(source: CoreAudioStreamSource) {
|
||||
let hasSameSource = playerContext.audioReadingEntry?.has(same: source) ?? false
|
||||
guard playerContext.audioReadingEntry == nil || hasSameSource else {
|
||||
source.delegate = nil
|
||||
@@ -877,7 +905,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func metadataReceived(data: [String: String]) {
|
||||
public func metadataReceived(data: [String: String]) {
|
||||
asyncOnMain { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.delegate?.audioPlayerDidReadMetadata(player: self, metadata: data)
|
||||
|
||||
@@ -9,7 +9,7 @@ enum UnitDescriptions {
|
||||
static let output: AudioComponentDescription = {
|
||||
var desc = AudioComponentDescription()
|
||||
desc.componentType = kAudioUnitType_Output
|
||||
#if os(iOS)
|
||||
#if os(iOS) || os(tvOS)
|
||||
desc.componentSubType = kAudioUnitSubType_RemoteIO
|
||||
#else
|
||||
desc.componentSubType = kAudioUnitSubType_DefaultOutput
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ let package = Package(
|
||||
name: "AudioStreaming",
|
||||
platforms: [
|
||||
.iOS(.v12),
|
||||
.macOS(.v13)
|
||||
.macOS(.v13),
|
||||
.tvOS(.v16)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
||||
@@ -18,6 +18,8 @@ Known limitations:
|
||||
|
||||
# Requirements
|
||||
- iOS 13.0+
|
||||
- macOS 13.0+
|
||||
- tvOS 16.0+
|
||||
- Swift 5.x
|
||||
|
||||
# Using AudioStreaming
|
||||
@@ -163,39 +165,11 @@ 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.
|
||||
|
||||
Reference in New Issue
Block a user