Compare commits

...

18 Commits

Author SHA1 Message Date
dimitris-c 738397c637 version bump 1.2.1 2024-05-15 00:33:47 +03:00
Dimitris C 1f70860473 feature(Mp4): Adds support for non-optimised mp4 audio for local files (#79)
* adds handling for non-optimized m4a for local files

* seek improvements

* small improvement in render processor

* improvements on seeking on local files

* improvements on seeking

* nit
2024-05-15 00:30:01 +03:00
Dimitris C a8865bb4d8 Mp4 Restructure account for large mdat box size (#78) 2024-05-09 14:45:02 +03:00
Dimitris C dd2e790ca6 Update README.md 2024-04-01 17:51:23 +03:00
Dimitris C c5bdbdd692 update readme.md (#71) 2024-04-01 16:46:23 +03:00
dimitris-c ffa5bf8f2c version bump to 1.2.0 2024-04-01 16:42:04 +03:00
dimitris-c 9d8973e971 update gitignore 2024-04-01 16:06:42 +03:00
Dimitris C cb72197f8e feature(Mp4): Support for non-optimised mp4 (#67)
* initial work for supporting non-optimised mp4

* Update AppCoordinator.swift

* some refactor and fixed seek for a restructured mp4

* nit

* nit

* nit

* runs swiftlint

* improvements

* improvements

* handles case where we the stream is not seekable for an mp4 file

* better check for mp4, seekable and moov atom

* nit

* fix an issue with seek

* some refactoring
2024-04-01 16:02:51 +03:00
Dimitris C 374da9bc22 removes measure file 2024-03-28 15:38:08 +02:00
Dimitris C 38d0bdb5d9 fix a glitch sound on pause and play (#69) 2024-03-10 19:23:58 +02:00
Dimitris C decb12641d fix incorrect stopReason on finish delegate method (#66) 2024-02-29 14:49:51 +02:00
Dimitris C 4e485f924a Fixes an issue with seek on FileAudioSource (#65) 2024-02-27 19:03:57 +02:00
junyaninflection 7e770197e6 version bump to 1.1.0 (#59) 2023-08-15 17:22:17 +03:00
junyaninflection 6f552e60c0 Lazy initialize singleton mixer node (#58) 2023-08-14 19:22:24 +03:00
dimitris-c 0f2a1f7b8a Version Bump
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-09-01 18:16:24 +03:00
dimitris-c 0c2c7ba685 Fixes an issue with seek functionality
Lowers the sourceQueue qos to default

Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-09-01 17:46:13 +03:00
dimitris-c 50174a7f4a Fix wrong next entry on audioPlayerDidStartPlaying
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-08-30 12:59:02 +03:00
dimitris-c cc82e79d50 Updates UnfairLock
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-08-30 01:47:26 +03:00
50 changed files with 1324 additions and 329 deletions
+3
View File
@@ -10,6 +10,7 @@ xcuserdata/
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
**/.DS_Store
build/
DerivedData/
*.moved-aside
@@ -88,3 +89,5 @@ fastlane/test_output
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
/.DS_Store
/AudioExample/AudioExample/.DS_Store
BIN
View File
Binary file not shown.
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
984808A028C0F549001160E6 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9848089F28C0F549001160E6 /* hipjazz.wav */; };
98C82AE22B8CA16A00AED485 /* bensound-jazzyfrenchy.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 98C82AE12B8CA0F000AED485 /* bensound-jazzyfrenchy.m4a */; };
B5220836256051830086FB3A /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5220835256051830086FB3A /* AudioPlayerService.swift */; };
B5220948256074910086FB3A /* MulticastDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5220947256074910086FB3A /* MulticastDelegate.swift */; };
B52209502561883E0086FB3A /* EqualizerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B522094F2561883E0086FB3A /* EqualizerViewController.swift */; };
@@ -42,6 +44,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
9848089F28C0F549001160E6 /* hipjazz.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = hipjazz.wav; sourceTree = "<group>"; };
98C82AE12B8CA0F000AED485 /* bensound-jazzyfrenchy.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "bensound-jazzyfrenchy.m4a"; sourceTree = "<group>"; };
B5220835256051830086FB3A /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = "<group>"; };
B5220947256074910086FB3A /* MulticastDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MulticastDelegate.swift; sourceTree = "<group>"; };
B522094F2561883E0086FB3A /* EqualizerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerViewController.swift; sourceTree = "<group>"; };
@@ -78,6 +82,8 @@
B524D59D2560177C00F5A88F /* Resources */ = {
isa = PBXGroup;
children = (
98C82AE12B8CA0F000AED485 /* bensound-jazzyfrenchy.m4a */,
9848089F28C0F549001160E6 /* hipjazz.wav */,
B524D59B2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 */,
B5AEDBDD2475274D007D8101 /* Assets.xcassets */,
B5AEDBDF2475274D007D8101 /* LaunchScreen.storyboard */,
@@ -211,6 +217,8 @@
B524D59C2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 in Resources */,
B5AEDBE12475274D007D8101 /* LaunchScreen.storyboard in Resources */,
B5AEDBDE2475274D007D8101 /* Assets.xcassets in Resources */,
98C82AE22B8CA16A00AED485 /* bensound-jazzyfrenchy.m4a in Resources */,
984808A028C0F549001160E6 /* hipjazz.wav in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -371,8 +379,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 5Y92JCRVR7;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = AudioExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -381,6 +389,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -390,8 +399,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 5Y92JCRVR7;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = AudioExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -400,6 +409,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -85,18 +85,6 @@
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
<AdditionalOption
key = "MallocStackLogging"
value = ""
isEnabled = "YES">
</AdditionalOption>
<AdditionalOption
key = "PrefersMallocStackLoggingLite"
value = ""
isEnabled = "YES">
</AdditionalOption>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -6,6 +6,7 @@
// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
//
import AVFoundation
import UIKit
final class AppCoordinator {
@@ -1,5 +1,5 @@
//
// EqualzerViewModel.swift
// EqualizerViewModel.swift
// AudioExample
//
// Created by Dimitrios Chatzieleftheriou on 15/11/2020.
@@ -121,7 +121,8 @@ extension PlayerViewController: UITableViewDataSource {
return cell
}
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = item.queues ? "Queue item" : nil
let queuedItem = item.queues ? "Queue item" : nil
cell.detailTextLabel?.text = queuedItem ?? item.subtitle
update(status: item.status, of: cell)
return cell
}
@@ -138,7 +139,11 @@ extension PlayerViewController: UITableViewDataSource {
cell.accessoryView = UIImageView(image: UIImage(systemName: "pause.fill"))
case .stopped:
cell.accessoryView = nil
case .error:
cell.accessoryView = UIImageView(image: UIImage(systemName: "exclamationmark.octagon"))
cell.accessoryView?.tintColor = .red
}
guard status != .error else { return }
cell.accessoryView?.tintColor = .systemTeal
}
}
@@ -48,7 +48,7 @@ final class PlayerViewModel {
print("malformed url error")
return
}
playlistItemsService.add(item: PlaylistItem(url: url, name: urlString, status: .stopped, queues: false))
playlistItemsService.add(item: PlaylistItem(url: url, name: urlString, subtitle: nil, status: .stopped, queues: false))
reloadContent?(.all)
}
@@ -92,8 +92,12 @@ extension PlayerViewModel: AudioPlayerServiceDelegate {
case .stopped:
playlistItemsService.setStatus(for: item, status: .stopped)
reloadContent?(.item(IndexPath(item: item, section: 0)))
case .error:
playlistItemsService.setStatus(for: item, status: .error)
reloadContent?(.item(IndexPath(item: item, section: 0)))
default:
break
playlistItemsService.setStatus(for: item, status: .stopped)
reloadContent?(.all)
}
}
Binary file not shown.
Binary file not shown.
@@ -16,8 +16,11 @@ enum AudioContent: Int, CaseIterable {
case radiox
case khruangbin
case piano
case optimized
case nonOptimized
case remoteWave
case local
case podcast
case localWave
var title: String {
switch self {
@@ -35,10 +38,45 @@ enum AudioContent: Int, CaseIterable {
return "Khruangbin (mp3 preview)"
case .piano:
return "Piano (mp3)"
case .remoteWave:
return "Sample remote (wave)"
case .local:
return "Local file (mp3)"
case .podcast:
return "Swift by Sundell. Ep. 50 (mp3)"
return "Jazzy Frenchy (local mp3)"
case .localWave:
return "Local file (local wave)"
case .optimized:
return "Jazze French (m4a - optimized)"
case .nonOptimized:
return "Jazze French (m4a - non-optimized)"
}
}
var subtitle: String? {
switch self {
case .offradio:
return nil
case .enlefko:
return nil
case .pepper966:
return nil
case .kosmos:
return nil
case .radiox:
return nil
case .khruangbin:
return nil
case .piano:
return nil
case .remoteWave:
return nil
case .local:
return "Music by: bensound.com"
case .localWave:
return "Music by: bensound.com"
case .optimized:
return "Music by: bensound.com"
case .nonOptimized:
return "Music by: bensound.com"
}
}
@@ -49,7 +87,7 @@ enum AudioContent: Int, CaseIterable {
case .offradio:
return URL(string: "https://s3.yesstreaming.net:17062/stream")!
case .pepper966:
return URL(string: "https://ample-09.radiojar.com/pepper.m4a?1593699983=&rj-tok=AAABcw_1KyMAIViq2XpI098ZSQ&rj-ttl=5")!
return URL(string: "https://n04.radiojar.com/pepper.m4a?1662039818=&rj-tok=AAABgvlUaioALhdOXDt0mgajoA&rj-ttl=5")!
case .kosmos:
return URL(string: "https://radiostreaming.ert.gr/ert-kosmos")!
case .radiox:
@@ -58,11 +96,18 @@ enum AudioContent: Int, CaseIterable {
return URL(string: "https://p.scdn.co/mp3-preview/cab4b09c23ffc11774d879977131df9d150fcef4?cid=d8a5ed958d274c2e8ee717e6a4b0971d")!
case .piano:
return URL(string: "https://www.kozco.com/tech/piano2-CoolEdit.mp3")!
case .optimized:
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/bensound-jazzyfrenchy-optimized.m4a")!
case .nonOptimized:
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/bensound-jazzyfrenchy.m4a")!
case .local:
let path = Bundle.main.path(forResource: "bensound-jazzyfrenchy", ofType: "mp3")!
let path = Bundle.main.path(forResource: "bensound-jazzyfrenchy", ofType: "m4a")!
return URL(fileURLWithPath: path)
case .podcast:
return URL(string: "https://hwcdn.libsyn.com/p/f/6/e/f6e7cb785cf0f71f/SwiftBySundell50.mp3?c_id=45232967&cs_id=45232967&expiration=1605613140&hwt=f9ff0b2f758c3286cd75322e14ef7a23")!
case .localWave:
let path = Bundle.main.path(forResource: "hipjazz", ofType: "wav")!
return URL(fileURLWithPath: path)
case .remoteWave:
return URL(string: "https://file-examples.com/wp-content/storage/2017/11/file_example_WAV_5MG.wav")!
}
}
}
@@ -111,7 +111,8 @@ final class AudioPlayerService {
// Note that a real app might need to observer other AVAudioSession notifications as well
audioSystemResetObserver = NotificationCenter.default.addObserver(forName: AVAudioSession.mediaServicesWereResetNotification,
object: nil,
queue: nil) { [unowned self] _ in
queue: nil)
{ [unowned self] _ in
self.configureAudioSession()
self.recreatePlayer()
}
@@ -148,22 +149,25 @@ final class AudioPlayerService {
}
extension AudioPlayerService: AudioPlayerDelegate {
func audioPlayerDidStartPlaying(player _: AudioPlayer, with _: AudioEntryId) {
func audioPlayerDidStartPlaying(player _: AudioPlayer, with id: AudioEntryId) {
print("audioPlayerDidStartPlaying entryId: \(id)")
delegate.invoke(invocation: { $0.didStartPlaying() })
}
func audioPlayerDidFinishBuffering(player _: AudioPlayer, with _: AudioEntryId) {}
func audioPlayerStateChanged(player _: AudioPlayer, with newState: AudioPlayerState, previous _: AudioPlayerState) {
print("audioPlayerDidStartPlaying newState: \(newState)")
delegate.invoke(invocation: { $0.statusChanged(status: newState) })
}
func audioPlayerDidFinishPlaying(player _: AudioPlayer,
entryId _: AudioEntryId,
stopReason _: AudioPlayerStopReason,
entryId id: AudioEntryId,
stopReason reason: AudioPlayerStopReason,
progress _: Double,
duration _: Double)
{
print("audioPlayerDidFinishPlaying entryId: \(id), reason: \(reason)")
delegate.invoke(invocation: { $0.didStopPlaying() })
}
@@ -14,23 +14,27 @@ struct PlaylistItem: Equatable {
case paused
case buffering
case stopped
case error
}
let url: URL
let name: String
let subtitle: String?
let status: Status
let queues: Bool
init(content: AudioContent, queues: Bool) {
name = content.title
subtitle = content.subtitle
url = content.streamUrl
status = .stopped
self.queues = queues
}
init(url: URL, name: String, status: Status, queues: Bool) {
init(url: URL, name: String, subtitle: String?, status: Status, queues: Bool) {
self.url = url
self.name = name
self.subtitle = subtitle
self.status = status
self.queues = queues
}
@@ -73,7 +77,13 @@ final class PlaylistItemsService {
guard let item = item(at: index) else {
return
}
items[index] = PlaylistItem(url: item.url, name: item.name, status: status, queues: item.queues)
items[index] = PlaylistItem(
url: item.url,
name: item.name,
subtitle: item.subtitle,
status: status,
queues: item.queues
)
}
}
+2 -2
View File
@@ -1,13 +1,13 @@
Pod::Spec.new do |s|
s.name = 'AudioStreaming'
s.version = '0.9.0'
s.version = '1.2.1'
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 = '12.0'
s.ios.deployment_target = '13.0'
s.swift_versions = ['5.1', '5.2', '5.3']
+44 -22
View File
@@ -7,6 +7,11 @@
objects = {
/* Begin PBXBuildFile section */
98ABF69E2BAB07A20059C441 /* Mp4Restructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */; };
98C82AE62B8CA8BC00AED485 /* RemoteMp4Restructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */; };
98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98CC396D28BD651E006C9FF9 /* Atomic.swift */; };
98DC00CC2B961F5E0068900A /* ByteBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC00CB2B961F5E0068900A /* ByteBuffer.swift */; };
98DC00CE2B9726380068900A /* ByteBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC00CD2B9726380068900A /* ByteBufferTests.swift */; };
B500732024D00BAC00BB4475 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500731F24D00BAC00BB4475 /* Logger.swift */; };
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514657E248E3884005C03F7 /* DispatchTimerSource.swift */; };
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */; };
@@ -33,7 +38,6 @@
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */; };
B5667A922499063D00D93F85 /* AudioPlayerContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667A912499063D00D93F85 /* AudioPlayerContext.swift */; };
B5667B3E249BC43100D93F85 /* AudioPlayerRenderProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */; };
B5737340254DE43E003DFBEC /* measure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B573733F254DE43E003DFBEC /* measure.swift */; };
B57829CF2548B32B00C78D36 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57829CE2548B32B00C78D36 /* Lock.swift */; };
B58386382544A2C10087A712 /* EntryFrames.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58386372544A2C10087A712 /* EntryFrames.swift */; };
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B583863F254584A50087A712 /* ProcessedPackets.swift */; };
@@ -64,8 +68,7 @@
B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */; };
B5EF955B247EBCB3003E8FF8 /* AudioFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */; };
B5EF955D247ECBB1003E8FF8 /* RemoteAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */; };
B5F883B62476DADB00D277C1 /* Protected.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B52476DADB00D277C1 /* Protected.swift */; };
B5F883BA2477CEFC00D277C1 /* ProtectedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B82477CBF600D277C1 /* ProtectedTests.swift */; };
B5F883BA2477CEFC00D277C1 /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B82477CBF600D277C1 /* AtomicTests.swift */; };
B5F883C32477DC4400D277C1 /* NetworkDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */; };
B5FB6C0525516507002C0A37 /* AudioConverter+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */; };
/* End PBXBuildFile section */
@@ -94,6 +97,11 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mp4Restructure.swift; sourceTree = "<group>"; };
98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMp4Restructure.swift; sourceTree = "<group>"; };
98CC396D28BD651E006C9FF9 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
98DC00CB2B961F5E0068900A /* ByteBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteBuffer.swift; sourceTree = "<group>"; };
98DC00CD2B9726380068900A /* ByteBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteBufferTests.swift; sourceTree = "<group>"; };
B500731F24D00BAC00BB4475 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSource.swift; sourceTree = "<group>"; };
B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioFormat+Convenience.swift"; sourceTree = "<group>"; };
@@ -121,7 +129,6 @@
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileStreamProcessor.swift; sourceTree = "<group>"; };
B5667A912499063D00D93F85 /* AudioPlayerContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerContext.swift; sourceTree = "<group>"; };
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerRenderProcessor.swift; sourceTree = "<group>"; };
B573733F254DE43E003DFBEC /* measure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = measure.swift; sourceTree = "<group>"; };
B57829CE2548B32B00C78D36 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = "<group>"; };
B580CB1D25628CF4006D7DD8 /* AudioStreaming.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioStreaming.podspec; sourceTree = "<group>"; };
B580CB1E25628CF4006D7DD8 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
@@ -158,8 +165,7 @@
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamSource.swift; sourceTree = "<group>"; };
B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileType.swift; sourceTree = "<group>"; };
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAudioSource.swift; sourceTree = "<group>"; };
B5F883B52476DADB00D277C1 /* Protected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protected.swift; sourceTree = "<group>"; };
B5F883B82477CBF600D277C1 /* ProtectedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectedTests.swift; sourceTree = "<group>"; };
B5F883B82477CBF600D277C1 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = "<group>"; };
B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDataStream.swift; sourceTree = "<group>"; };
B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioConverter+Helpers.swift"; sourceTree = "<group>"; };
B5FFF5FD2549FA02006BBB7C /* AudioExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioExample.xctestplan; sourceTree = "<group>"; };
@@ -184,6 +190,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
98C82AE42B8CA8AA00AED485 /* Mp4 */ = {
isa = PBXGroup;
children = (
98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */,
98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */,
);
path = Mp4;
sourceTree = "<group>";
};
B5276B70247D4D3D00D2F56A /* Network */ = {
isa = PBXGroup;
children = (
@@ -302,6 +317,7 @@
B58BD7FC255DB653005B756D /* Audio Source */ = {
isa = PBXGroup;
children = (
98C82AE42B8CA8AA00AED485 /* Mp4 */,
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */,
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */,
B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */,
@@ -321,11 +337,11 @@
B592E13025460883008866FB /* Helpers */ = {
isa = PBXGroup;
children = (
B573733F254DE43E003DFBEC /* measure.swift */,
98DC00CB2B961F5E0068900A /* ByteBuffer.swift */,
98CC396D28BD651E006C9FF9 /* Atomic.swift */,
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */,
B57829CE2548B32B00C78D36 /* Lock.swift */,
B500731F24D00BAC00BB4475 /* Logger.swift */,
B5F883B52476DADB00D277C1 /* Protected.swift */,
B54C3E55255F286D00B356F2 /* Retrier.swift */,
);
path = Helpers;
@@ -431,8 +447,8 @@
B5F883B42476DABE00D277C1 /* Core */ = {
isa = PBXGroup;
children = (
B592E11E2545FF33008866FB /* Structures */,
B55CE97624813BA10001C498 /* Extensions */,
B592E11E2545FF33008866FB /* Structures */,
B5276B70247D4D3D00D2F56A /* Network */,
B592E13025460883008866FB /* Helpers */,
);
@@ -443,10 +459,11 @@
isa = PBXGroup;
children = (
B5EF954A247DA450003E8FF8 /* Network */,
B5F883B82477CBF600D277C1 /* ProtectedTests.swift */,
B5F883B82477CBF600D277C1 /* AtomicTests.swift */,
B51FE0C12488F96A00F2A4D2 /* QueueTests.swift */,
B592E12825460146008866FB /* BiMapTests.swift */,
B592E133254608B4008866FB /* DispatchTimerSourceTests.swift */,
98DC00CD2B9726380068900A /* ByteBufferTests.swift */,
);
path = Core;
sourceTree = "<group>";
@@ -584,7 +601,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -599,6 +616,7 @@
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */,
B54C3E56255F286D00B356F2 /* Retrier.swift in Sources */,
B59DF10424916FD50043C498 /* DispatchQueue+Helpers.swift in Sources */,
98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */,
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */,
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */,
B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */,
@@ -607,7 +625,6 @@
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */,
B54D876D2490E4A000C361A0 /* UnitDescriptions.swift in Sources */,
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */,
B5737340254DE43E003DFBEC /* measure.swift in Sources */,
B55CEABC24853CD20001C498 /* AudioPlayer.swift in Sources */,
B5667B3E249BC43100D93F85 /* AudioPlayerRenderProcessor.swift in Sources */,
B5276B6F247D21A000D2F56A /* NetworkingClient.swift in Sources */,
@@ -617,6 +634,7 @@
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */,
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */,
B59D0B6F255C904900D6CCE5 /* FileAudioSource.swift in Sources */,
98DC00CC2B961F5E0068900A /* ByteBuffer.swift in Sources */,
B5EF9555247E9393003E8FF8 /* AudioEntry.swift in Sources */,
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */,
B51FE0C02488F67C00F2A4D2 /* Queue.swift in Sources */,
@@ -633,12 +651,13 @@
B55CE96E248058B60001C498 /* MetadataParser.swift in Sources */,
B5838644254584BE0087A712 /* AudioStreamState.swift in Sources */,
B500732024D00BAC00BB4475 /* Logger.swift in Sources */,
98C82AE62B8CA8BC00AED485 /* RemoteMp4Restructure.swift in Sources */,
B5276B74247D4D9F00D2F56A /* NetworkSessionDelegate.swift in Sources */,
B55F77D624DACE140057F431 /* BufferContext.swift in Sources */,
B5838648254584D90087A712 /* SeekRequest.swift in Sources */,
B5D82E65255DD562009EDAA4 /* NetStatusService.swift in Sources */,
B55CE97824813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift in Sources */,
B5F883B62476DADB00D277C1 /* Protected.swift in Sources */,
98ABF69E2BAB07A20059C441 /* Mp4Restructure.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -651,10 +670,11 @@
B51FE0C824892D1600F2A4D2 /* PlayerQueueEntriesTest.swift in Sources */,
B55CEABA248530C00001C498 /* MetadataParser.swift in Sources */,
B51FE0C22488F96A00F2A4D2 /* QueueTests.swift in Sources */,
B5F883BA2477CEFC00D277C1 /* ProtectedTests.swift in Sources */,
B5F883BA2477CEFC00D277C1 /* AtomicTests.swift in Sources */,
B592E134254608B4008866FB /* DispatchTimerSourceTests.swift in Sources */,
B55CEAB82485172D0001C498 /* HTTPHeaderParserTests.swift in Sources */,
B592E12925460146008866FB /* BiMapTests.swift in Sources */,
98DC00CE2B9726380068900A /* ByteBufferTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -721,8 +741,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 0.1.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -780,8 +800,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 0.1.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -805,13 +825,13 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AudioStreaming/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.9.0;
MARKETING_VERSION = 1.2.1;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -836,13 +856,13 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AudioStreaming/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.9.0;
MARKETING_VERSION = 1.2.1;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -861,6 +881,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -881,6 +902,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -8,11 +8,13 @@ import AVFoundation
@discardableResult
func fileStreamGetProperty<Value>(value: inout Value, fileStream streamId: AudioFileStreamID, propertyId: AudioFileStreamPropertyID) -> OSStatus {
var (size, _) = fileStreamGetPropertyInfo(fileStream: streamId, propertyId: propertyId)
let status = AudioFileStreamGetProperty(streamId, propertyId, &size, &value)
guard status == noErr else {
return withUnsafeMutablePointer(to: &value) { pointer in
let status = AudioFileStreamGetProperty(streamId, propertyId, &size, pointer)
guard status == noErr else {
return status
}
return status
}
return status
}
func fileStreamGetPropertyInfo(fileStream streamId: AudioFileStreamID, propertyId: AudioFileStreamPropertyID) -> (size: UInt32, status: OSStatus) {
@@ -112,3 +114,50 @@ public enum AudioFileStreamError: CustomDebugStringConvertible {
}
}
}
public extension AudioFileStreamPropertyID {
var description: String {
switch self {
case kAudioFileStreamProperty_ReadyToProducePackets:
return "Ready to produce packets"
case kAudioFileStreamProperty_FileFormat:
return "File format"
case kAudioFileStreamProperty_DataFormat:
return "Data format"
case kAudioFileStreamProperty_AudioDataByteCount:
return "Byte count"
case kAudioFileStreamProperty_AudioDataPacketCount:
return "Packet count"
case kAudioFileStreamProperty_DataOffset:
return "Data offset"
case kAudioFileStreamProperty_BitRate:
return "Bit rate"
case kAudioFileStreamProperty_FormatList:
return "Format list"
case kAudioFileStreamProperty_MagicCookieData:
return "Magic cookie"
case kAudioFileStreamProperty_MaximumPacketSize:
return "Max packet size"
case kAudioFileStreamProperty_ChannelLayout:
return "Channel layout"
case kAudioFileStreamProperty_PacketToFrame:
return "Packet to frame"
case kAudioFileStreamProperty_FrameToPacket:
return "Frame to packet"
case kAudioFileStreamProperty_PacketToByte:
return "Packet to byte"
case kAudioFileStreamProperty_ByteToPacket:
return "Byte to packet"
case kAudioFileStreamProperty_PacketTableInfo:
return "Packet table"
case kAudioFileStreamProperty_PacketSizeUpperBound:
return "Packet size upper bound"
case kAudioFileStreamProperty_AverageBytesPerPacket:
return "Average bytes per packet"
case kAudioFileStreamProperty_InfoDictionary:
return "Info dictionary"
default:
return "Unknown"
}
}
}
@@ -13,10 +13,10 @@ final class Atomic<Value> {
_value = value
}
var value: Value { lock.around { _value } }
var value: Value { lock.withLock { _value } }
func write(_ transform: (inout Value) -> Void) {
lock.around { transform(&self._value) }
lock.withLock { transform(&self._value) }
}
}
@@ -0,0 +1,187 @@
//
// Created by Dimitrios Chatzieleftheriou on 4/03/2024.
// Copyright © 2024 Decimal. All rights reserved.
//
import Foundation
// Struct representing a buffer for handling binary data
struct ByteBuffer {
// Custom errors for ByteBuffer
enum Error: Swift.Error {
case eof // End of file
case parse // Parsing error
}
// Data storage for the buffer
private(set) var storage = Data()
// Current offset in the buffer
var offset: Int = 0
// Calculated property for the number of bytes available for reading
var bytesAvailable: Int {
storage.count - offset
}
// Calculated property for the length of the buffer
var length: Int {
get {
storage.count
}
set {
// Adjusting the length of the buffer
switch true {
case storage.count < newValue:
storage.append(Data(count: newValue - storage.count))
case newValue < storage.count:
storage = storage.subdata(in: 0 ..< newValue)
default:
break
}
}
}
// Subscript for accessing individual bytes in the buffer
subscript(i: Int) -> UInt8 {
get { storage[i] }
set { storage[i] = newValue }
}
// Initialize the buffer with given data
init(data: Data) {
storage = data
offset = 0
}
// Initialize the buffer with a specified size, filling it with zeros
init(size: Int) {
storage = Data(repeating: 0x00, count: size)
offset = 0
}
// Clear the buffer (reset offset to zero)
@discardableResult
mutating func clear() -> Self {
offset = 0
return self
}
// Rewind the buffer (reset offset to zero)
mutating func rewind() {
offset = 0
}
// Read a specified number of bytes from the buffer
mutating func readBytes(_ length: Int) throws -> Data {
guard length <= bytesAvailable else {
throw ByteBuffer.Error.eof
}
offset += length
return storage.subdata(in: offset - length ..< offset)
}
// Write data into the buffer
@discardableResult
mutating func writeBytes(_ value: Data) -> Self {
// If the offset is at the end, append the value to the data
if offset == storage.count {
storage.append(value)
offset = storage.count
return self
}
// Otherwise, write the value into the buffer at the current offset
let length: Int = min(storage.count, value.count)
storage[offset ..< offset + length] = value[0 ..< length]
// If the value is longer than the remaining space, append the rest to the data
if length == storage.count {
storage.append(value[length ..< value.count])
}
offset += value.count
return self
}
// Write integer value into the buffer
@discardableResult
mutating func put<T: FixedWidthInteger>(_ value: T) -> ByteBuffer {
writeBytes(value.data)
}
// Write float value into the buffer
@discardableResult
mutating func put(_ value: Float) -> ByteBuffer {
writeBytes(Data(value.data.reversed()))
}
// Write double value into the buffer
@discardableResult
mutating func put(_ value: Double) -> ByteBuffer {
writeBytes(Data(value.data.reversed()))
}
// Read an integer value from the buffer
mutating func getInteger<T: FixedWidthInteger>() throws -> T {
let sizeOfInteger = MemoryLayout<T>.size
guard sizeOfInteger <= bytesAvailable else {
throw ByteBuffer.Error.eof
}
offset += sizeOfInteger
return T(data: storage[offset - sizeOfInteger ..< offset]).bigEndian
}
// Read an integer value from a specific index in the buffer
func getInteger<T: FixedWidthInteger>(_ index: Int) throws -> T {
let sizeOfInteger = MemoryLayout<T>.size
guard sizeOfInteger + index <= length else {
throw ByteBuffer.Error.eof
}
return T(data: storage[index ..< index + sizeOfInteger]).bigEndian
}
// Read a float value from the buffer
mutating func getFloat() throws -> Float {
let sizeOfFloat = MemoryLayout<UInt32>.size
guard sizeOfFloat <= bytesAvailable else {
throw ByteBuffer.Error.eof
}
offset += sizeOfFloat
return Float(data: Data(storage.subdata(in: offset - sizeOfFloat ..< offset).reversed()))
}
// Read a double value from the buffer
mutating func getDouble() throws -> Double {
let sizeOfDouble = MemoryLayout<UInt64>.size
guard sizeOfDouble <= bytesAvailable else {
throw ByteBuffer.Error.eof
}
offset += sizeOfDouble
return Double(data: Data(storage.subdata(in: offset - sizeOfDouble ..< offset).reversed()))
}
}
// Extension to provide conformance to ExpressibleByIntegerLiteral for easy conversion between integers and Data
extension ExpressibleByIntegerLiteral {
// Convert integer to Data
var data: Data {
return withUnsafePointer(to: self) { pointer in
Data(bytes: pointer, count: MemoryLayout<Self>.size)
}
}
// Initialize from Data
init(data: Data) {
let diff: Int = MemoryLayout<Self>.size - data.count
if diff > 0 {
var buffer = Data(repeating: 0, count: diff)
buffer.append(data)
self = buffer.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: Self.self).pointee }
return
}
self = data.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: Self.self).pointee }
}
// Initialize from Data slice
init(data: Slice<Data>) {
self.init(data: Data(data))
}
}
@@ -11,10 +11,10 @@ import Foundation
final class DispatchTimerSource {
private var handler: (() -> Void)?
private let timer: DispatchSourceTimer
internal var state: SourceState = .suspended
var state: SourceState = .suspended
/// The state of the timer
internal enum SourceState {
enum SourceState {
case activated
case suspended
}
+24 -17
View File
@@ -8,46 +8,53 @@ import Foundation
protocol Lock {
func lock()
func unlock()
}
extension Lock {
// Execute a closure while acquiring a lock and returns the closure value
@inline(__always)
func around<Value>(_ closure: () -> Value) -> Value {
lock(); defer { unlock() }
return closure()
}
func withLock<Result>(body: () throws -> Result) rethrows -> Result
// Execute a closure while acquiring a lock
@inline(__always)
func around(_ closure: () -> Void) {
lock(); defer { unlock() }
closure()
}
func withLock(body: () -> Void)
}
/// A wrapper for `os_unfair_lock`
/// - Tag: UnfairLock
final class UnfairLock: Lock {
private let unfairLock: os_unfair_lock_t
@usableFromInline let unfairLock: UnsafeMutablePointer<os_unfair_lock>
internal init() {
init() {
unfairLock = .allocate(capacity: 1)
unfairLock.initialize(to: os_unfair_lock())
}
deinit {
unfairLock.deinitialize(count: 1)
unfairLock.deallocate()
}
@inlinable
@inline(__always)
internal func lock() {
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
os_unfair_lock_lock(unfairLock)
defer { os_unfair_lock_unlock(unfairLock) }
return try body()
}
@inlinable
@inline(__always)
func withLock(body: () -> Void) {
os_unfair_lock_lock(unfairLock)
defer { os_unfair_lock_unlock(unfairLock) }
body()
}
@inlinable
@inline(__always)
func lock() {
os_unfair_lock_lock(unfairLock)
}
@inlinable
@inline(__always)
internal func unlock() {
func unlock() {
os_unfair_lock_unlock(unfairLock)
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import os
private let loggingSubsystem = "audio.streaming.log"
internal enum Logger {
enum Logger {
private static let audioRendering = OSLog(subsystem: loggingSubsystem, category: "audio.rendering")
private static let networking = OSLog(subsystem: loggingSubsystem, category: "audio.networking")
private static let generic = OSLog(subsystem: loggingSubsystem, category: "audio.streaming.generic")
@@ -1,24 +0,0 @@
//
// Created by Dimitrios Chatzieleftheriou on 21/05/2020.
// Copyright © 2020 Decimal. All rights reserved.
//
internal final class Protected<Value> {
var value: Value { lock.around { _value } }
private let lock = UnfairLock()
private var _value: Value
init(_ value: Value) {
_value = value
}
func read<Element>(_ closure: (Value) -> Element) -> Element {
lock.around { closure(self._value) }
}
@discardableResult
func write<Element>(_ closure: (inout Value) -> Element) -> Element {
lock.around { closure(&self._value) }
}
}
-15
View File
@@ -1,15 +0,0 @@
//
// measure.swift
// AudioStreaming
//
// Created by Dimitrios Chatzieleftheriou on 31/10/2020.
// Copyright © 2020 Decimal. All rights reserved.
//
import Foundation
func measure(name: String = "", block: () -> Void) {
let started = ProcessInfo.processInfo.systemUptime
block()
print("diff for \(name): \(String(format: "%.6f", ProcessInfo.processInfo.systemUptime - started))")
}
@@ -5,7 +5,7 @@
import Foundation
internal final class NetworkDataStream {
final class NetworkDataStream {
typealias StreamResult = Result<Response, Error>
typealias StreamCompletion = (_ event: NetworkDataStream.ResponseEvent) -> Void
@@ -52,7 +52,7 @@ internal final class NetworkDataStream {
task?.response as? HTTPURLResponse
}
internal init(id: UUID, underlyingQueue: DispatchQueue) {
init(id: UUID, underlyingQueue: DispatchQueue) {
self.id = id
self.underlyingQueue = underlyingQueue
state = .initialised
@@ -94,7 +94,7 @@ internal final class NetworkDataStream {
// MARK: Internal
internal func didReceive(response: HTTPURLResponse?) {
func didReceive(response: HTTPURLResponse?) {
underlyingQueue.async { [weak self] in
guard let self = self else { return }
guard let streamCallback = self.streamCallback else { return }
@@ -102,7 +102,7 @@ internal final class NetworkDataStream {
}
}
internal func didReceive(data: Data, response: HTTPURLResponse?) {
func didReceive(data: Data, response: HTTPURLResponse?) {
underlyingQueue.async { [weak self] in
guard let self = self else { return }
guard let streamCallback = self.streamCallback else { return }
@@ -111,7 +111,7 @@ internal final class NetworkDataStream {
}
}
internal func didComplete(with error: Error?, response: HTTPURLResponse?) {
func didComplete(with error: Error?, response: HTTPURLResponse?) {
underlyingQueue.async { [weak self] in
guard let self = self else { return }
guard let stream = self.streamCallback else { return }
@@ -5,10 +5,10 @@
import Foundation
internal final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
weak var taskProvider: StreamTaskProvider?
internal func stream(for task: URLSessionTask) -> NetworkDataStream? {
func stream(for task: URLSessionTask) -> NetworkDataStream? {
guard let taskProvider = taskProvider else {
assertionFailure("couldn't found taskProvider")
return nil
@@ -16,22 +16,22 @@ internal final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
return taskProvider.dataStream(for: task)
}
internal func urlSession(_: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data)
func urlSession(_: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data)
{
guard let stream = self.stream(for: dataTask) else {
guard let stream = stream(for: dataTask) else {
return
}
stream.didReceive(data: data,
response: dataTask.response as? HTTPURLResponse)
}
internal func urlSession(_: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?)
func urlSession(_: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?)
{
guard let stream = self.stream(for: task) else {
guard let stream = stream(for: task) else {
return
}
stream.didComplete(with: error, response: task.response as? HTTPURLResponse)
@@ -42,7 +42,7 @@ internal final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
{
guard let stream = self.stream(for: dataTask) else {
guard let stream = stream(for: dataTask) else {
return
}
stream.didReceive(response: response as? HTTPURLResponse)
@@ -44,7 +44,7 @@ extension URLSessionConfiguration {
}
}
internal final class NetworkingClient {
final class NetworkingClient {
let session: URLSession
weak var delegate: NetworkSessionDelegate?
let networkQueue: DispatchQueue
@@ -52,9 +52,9 @@ internal final class NetworkingClient {
var tasksLock = UnfairLock()
var tasks = BiMap<URLSessionTask, NetworkDataStream>()
internal init(configuration: URLSessionConfiguration = .networkingConfiguration,
delegate: NetworkSessionDelegate = NetworkSessionDelegate(),
networkQueue: DispatchQueue = DispatchQueue(label: "audio.streaming.session.network.queue"))
init(configuration: URLSessionConfiguration = .networkingConfiguration,
delegate: NetworkSessionDelegate = NetworkSessionDelegate(),
networkQueue: DispatchQueue = DispatchQueue(label: "audio.streaming.session.network.queue"))
{
let delegateQueue = operationQueue(underlyingQueue: networkQueue)
let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue)
@@ -70,19 +70,35 @@ internal final class NetworkingClient {
/// Creates a data stream for the given `URLRequest`
/// - parameter request: A `URLRequest` to be used for the data stream
internal func stream(request: URLRequest) -> NetworkDataStream {
func stream(request: URLRequest) -> NetworkDataStream {
let stream = NetworkDataStream(id: UUID(), underlyingQueue: networkQueue)
setupRequest(stream, request: request)
return stream
}
internal func remove(task: NetworkDataStream) {
tasksLock.lock(); defer { tasksLock.unlock() }
if !tasks.isEmpty {
tasks[task] = nil
func remove(task: NetworkDataStream) {
tasksLock.withLock {
if !tasks.isEmpty {
tasks[task] = nil
}
}
}
@discardableResult
func task(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) -> URLSessionDataTask {
let task = session.dataTask(with: request) { data, _, error in
if let error {
completion(Result<Data, Error>.failure(error))
return
}
if let data {
completion(Result<Data, Error>.success(data))
}
}
task.resume()
return task
}
// MARK: Private
/// Schedules the given `NetworkDataStream` to be performed immediately
@@ -99,14 +115,16 @@ internal final class NetworkingClient {
// MARK: StreamTaskProvider conformance
extension NetworkingClient: StreamTaskProvider {
internal func dataStream(for request: URLSessionTask) -> NetworkDataStream? {
tasksLock.lock(); defer { tasksLock.unlock() }
return tasks[request] ?? nil
func dataStream(for request: URLSessionTask) -> NetworkDataStream? {
tasksLock.withLock {
tasks[request] ?? nil
}
}
internal func sessionTask(for stream: NetworkDataStream) -> URLSessionTask? {
tasksLock.lock(); defer { tasksLock.unlock() }
return tasks[stream] ?? nil
func sessionTask(for stream: NetworkDataStream) -> URLSessionTask? {
tasksLock.withLock {
tasks[stream] ?? nil
}
}
}
@@ -7,11 +7,11 @@ import AudioToolbox
import AVFoundation
public struct AudioEntryId: Equatable {
internal var unique = UUID()
var unique = UUID()
public var id: String
}
internal class AudioEntry {
class AudioEntry {
private let estimationMinPackets = 2
private let estimationMinPacketsPreferred = 64
@@ -22,9 +22,7 @@ internal class AudioEntry {
let id: AudioEntryId
/// The sample rate from the `audioStreamFormat`
var sampleRate: Float {
Float(audioStreamFormat.mSampleRate)
}
var sampleRate: Float
var audioFileHint: AudioFileTypeID {
source.audioFileHint
@@ -49,9 +47,7 @@ internal class AudioEntry {
private(set) var framesState: EntryFramesState
private(set) var processedPacketsState: ProcessedPacketsState
var packetDuration: Double {
return Double(audioStreamFormat.mFramesPerPacket) / Double(sampleRate)
}
var packetDuration: Double
private var averagePacketByteSize: Double {
let packets = processedPacketsState
@@ -72,6 +68,8 @@ internal class AudioEntry {
processedPacketsState = ProcessedPacketsState()
framesState = EntryFramesState()
audioStreamState = AudioStreamState()
sampleRate = 0
packetDuration = 0
}
func close() {
@@ -94,7 +92,9 @@ internal class AudioEntry {
func reset() {
lock.lock(); defer { lock.unlock() }
framesState = EntryFramesState()
framesState.played = 0
framesState.queued = 0
framesState.lastFrameQueued = -1
}
func has(same source: CoreAudioStreamSource) -> Bool {
@@ -8,6 +8,6 @@ import Foundation
final class SeekRequest {
let lock = UnfairLock()
var requested: Bool = false
var version = Protected<Int>(0)
var version = Atomic<Int>(0)
var time: Double = 0
}
@@ -3,6 +3,7 @@
// Copyright © 2020 Decimal. All rights reserved.
//
import Foundation
import AVFoundation
final class FileAudioSource: NSObject, CoreAudioStreamSource {
@@ -17,6 +18,12 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
audioFileType(fileExtension: url.pathExtension)
}
private var isMp4: Bool {
audioFileHint == kAudioFileM4AType || audioFileHint == kAudioFileMPEG4Type
}
private var mp4IsAlreadyOptimized: Bool = false
private var seekOffset: Int
private let url: URL
@@ -26,6 +33,8 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
private var buffer: UnsafeMutablePointer<UInt8>
private var inputStream: InputStream?
private var mp4Restructure: Mp4Restructure
init(url: URL,
fileManager: FileManager = .default,
underlyingQueue: DispatchQueue,
@@ -35,6 +44,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
self.underlyingQueue = underlyingQueue
self.fileManager = fileManager
self.readSize = readSize
self.mp4Restructure = Mp4Restructure()
buffer = UnsafeMutablePointer.uint8pointer(of: readSize)
seekOffset = 0
position = 0
@@ -43,6 +53,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
deinit {
buffer.deallocate()
mp4Restructure.clear()
}
func close() {
@@ -54,12 +65,8 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
inputStream.delegate = nil
}
func suspend() {
guard let inputStream = inputStream else {
return
}
CFReadStreamSetDispatchQueue(inputStream, nil)
}
// no-op
func suspend() {}
func resume() {
guard let inputStream = inputStream else {
@@ -69,8 +76,6 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
func seek(at offset: Int) {
close()
do {
try performOpen(seek: offset)
} catch {
@@ -79,30 +84,30 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
private func performOpen(seek seekOffset: Int) throws {
guard let inputStream = InputStream(url: url) else {
throw AudioSystemError.playerStartError
}
self.inputStream = inputStream
var reopened = false
let streamStatus = inputStream.streamStatus
if streamStatus == .notOpen || streamStatus == .error {
let status = inputStream?.streamStatus ?? .closed
if status == .atEnd || status == .closed || status == .error {
reopened = true
close()
open(inputStream: inputStream)
try open()
}
let attributes = try fileManager.attributesOfItem(atPath: url.path)
length = (attributes[.size] as? Int) ?? 0
var offset = seekOffset
if isMp4, mp4Restructure.dataOptimized {
offset = mp4Restructure.seekAdjusted(offset: seekOffset)
}
if inputStream.setProperty(seekOffset, forKey: .fileCurrentOffsetKey) {
position = seekOffset
if inputStream?.setProperty(offset, forKey: .fileCurrentOffsetKey) == true {
position = offset
} else {
position = 0
}
if !reopened {
if inputStream.hasBytesAvailable {
dataAvailable()
underlyingQueue.async { [weak self] in
if self?.inputStream?.hasBytesAvailable == true {
self?.dataAvailable()
}
}
}
}
@@ -112,17 +117,62 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
let read = inputStream.read(buffer, maxLength: readSize)
if read > 0 {
let data = Data(bytes: buffer, count: read)
delegate?.dataAvailable(source: self, data: data)
if isMp4, !mp4IsAlreadyOptimized {
if !mp4Restructure.dataOptimized {
do {
if let mp4OptimizeInfo = try mp4Restructure.checkIsOptimized(data: data) {
try performMp4Restructure(inputStream: inputStream, mp4OptimizeInfo: mp4OptimizeInfo)
} else {
mp4IsAlreadyOptimized = true
delegate?.dataAvailable(source: self, data: data)
}
} catch {
delegate?.errorOccurred(source: self, error: error)
}
} else {
delegate?.dataAvailable(source: self, data: data)
}
} else {
delegate?.dataAvailable(source: self, data: data)
}
position += read
} else {
position += getCurrentOffsetFromStream()
}
}
private func open(inputStream: InputStream) {
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)
}
} else {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
}
} else {
delegate?.errorOccurred(source: self, error: inputStream.streamError ?? AudioSystemError.playerStartError)
}
}
private func open() throws {
guard let inputStream = InputStream(url: url) else {
throw AudioSystemError.playerStartError
}
self.inputStream = inputStream
CFReadStreamSetDispatchQueue(inputStream, underlyingQueue)
inputStream.delegate = self
inputStream.open()
let attributes = try fileManager.attributesOfItem(atPath: url.path)
length = (attributes[.size] as? Int) ?? 0
}
private func getCurrentOffsetFromStream() -> Int {
@@ -142,8 +192,6 @@ extension FileAudioSource: StreamDelegate {
delegate?.endOfFileOccurred(source: self)
case .errorOccurred:
delegate?.errorOccurred(source: self, error: AudioPlayerError.codecError)
case .endEncountered:
delegate?.endOfFileOccurred(source: self)
default:
break
}
@@ -0,0 +1,280 @@
//
// Created by Dimitrios Chatzieleftheriou on 20/03/2024.
// Copyright © 2020 Decimal. All rights reserved.
//
import Foundation
struct MP4Atom: Equatable, CustomDebugStringConvertible {
let type: Int
let size: Int
let offset: Int
var data: Data?
var isFreeSpaceAtom: Bool {
type == Atoms.free || type == Atoms.skip || type == Atoms.wide
}
var debugDescription: String {
"[Atom][size: \(size))][type: \(Atoms.integerToFourCC(type) ?? "")][offset: \(offset)]"
}
}
struct Mp4OptimizeInfo: Equatable {
let moovOffset: Int
let moovSize: Int
}
/// These are some atoms, helpful for audio mp4
enum Atoms {
static var ftyp: Int { fourCcToInt("ftyp") }
static var moov: Int { fourCcToInt("moov") }
static var mdat: Int { fourCcToInt("mdat") }
static var free: Int { fourCcToInt("free") }
static var skip: Int { fourCcToInt("skip") }
static var wide: Int { fourCcToInt("wide") }
static var cmov: Int { fourCcToInt("cmov") }
static var stco: Int { fourCcToInt("stco") }
static var co64: Int { fourCcToInt("c064") }
static var atomPreampleSize: Int = 8
static func fourCcToInt(_ fourCc: String) -> Int {
let data = fourCc.data(using: .ascii)!
return Int(bigEndian: Int(data: data))
}
static func integerToFourCC(_ value: Int) -> String? {
guard value >= 0, value <= 0xFFFF_FFFF else {
return nil // Integer value out of range
}
var bytes: [UInt8] = []
bytes.append(UInt8((value >> 24) & 0xFF))
bytes.append(UInt8((value >> 16) & 0xFF))
bytes.append(UInt8((value >> 8) & 0xFF))
bytes.append(UInt8(value & 0xFF))
let data = Data(bytes)
return String(data: data, encoding: .ascii)
}
}
enum Mp4RestructureError: Error {
case unableToRestructureData
case missingMoovData
case invalidMoovAtom
case invalidAtomSize
case invalidAtomType
case invalidOffset
case missingMdatAtom
case missingMoovAtom
case compressedAtomNotSupported
case nonOptimizedMp4AndServerCannotSeek
case networkError(Error)
}
final class Mp4Restructure {
private var atomOffset: Int = 0
private var atoms: [MP4Atom] = []
private var ftyp: MP4Atom?
private var foundMoov = false
private var foundMdat = false
private(set) var dataOptimized: Bool = false
private var moovAtomSize: Int = 0
func clear() {
atomOffset = 0
atoms = []
ftyp = nil
foundMdat = false
foundMoov = false
}
/// Adjust the seekOffset of subtracting the moovAtomSize
/// - Parameter offset: A byte offset
/// - Returns: An adjusted byte offset
func seekAdjusted(offset: Int) -> Int {
offset - moovAtomSize
}
func restructureMoov(data: Data) throws -> (initialData: Data, mdatOffset: Int) {
let (atomData, moovSize) = try doRestructureMoov(data: data)
moovAtomSize = moovSize
guard let mdatIndex = atoms.firstIndex(where: { $0.type == Atoms.mdat }) else {
throw Mp4RestructureError.missingMdatAtom
}
let mdatAtom = atoms[mdatIndex]
let atoms = Array(atoms[..<mdatIndex])
let dataOfAtomsBefore = atoms.filter { $0.data != nil }.compactMap(\.data)
let accumulatedInitialData = dataOfAtomsBefore.reduce(into: Data()) { partialResult, data in
partialResult.append(data)
}
let initialData = accumulatedInitialData + atomData
let mdatOffset: Int
if let ftyp = ftyp {
mdatOffset = ftyp.offset + ftyp.size
} else {
let freeSpaceAtoms = atoms.filter(\.isFreeSpaceAtom)
let freeSpaceSize = freeSpaceAtoms.reduce(into: 0) { partialResult, atom in
partialResult += atom.size
}
mdatOffset = mdatAtom.offset - freeSpaceSize
}
dataOptimized = true
return (initialData, mdatOffset)
}
/// 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 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
case Atoms.moov:
let moov = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
atoms.append(moov)
foundMoov = true
default:
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 nil
} else if !foundMoov && foundMdat {
Logger.debug("🕵️ detected an non-optimized mp4", category: .generic)
let possibleMoovOffset = Int(atomOffset) + atomSize
return Mp4OptimizeInfo(moovOffset: possibleMoovOffset, moovSize: atomSize)
}
}
atomOffset += atomSize
}
return nil
}
/// logic taken from qt-faststart.c over at ffmpeg
/// https://github.com/FFmpeg/FFmpeg/blob/b47b2c5b912558b639c8542993e1256f9c69e675/tools/qt-faststart.c
private func doRestructureMoov(data: Data) throws -> (Data, Int) {
var moovAtomSize: Int = 0
var moovAtomType: Int = 0
var originalData = ByteBuffer(data: data)
var offset: Int = 0
// do search for moov within the new data
while offset < originalData.length {
moovAtomSize = Int(try originalData.getInteger() as UInt32)
moovAtomType = Int(try originalData.getInteger() as UInt32)
if moovAtomType == Atoms.moov {
break
}
offset += moovAtomSize
}
// error if we couldn't find an moov type
guard moovAtomType == Atoms.moov else {
throw Mp4RestructureError.missingMoovAtom
}
originalData.offset = offset
var moovAtom = ByteBuffer(size: moovAtomSize)
let slicedData: Data = try originalData.readBytes(moovAtom.length)
moovAtom.writeBytes(slicedData)
moovAtom.rewind()
if try Int(moovAtom.getInteger(12) as UInt32) == Atoms.cmov {
Logger.debug("Compressed moov atom not supported", category: .generic)
throw Mp4RestructureError.compressedAtomNotSupported
}
var atomType: Int
var atomSize: Int
// crawl through the atom and restructure offsets
while moovAtom.bytesAvailable >= 8 {
let atomHead = moovAtom.offset
atomType = try Int(moovAtom.getInteger(atomHead + 4) as UInt32)
if !(atomType == Atoms.stco || atomType == Atoms.co64) {
moovAtom.offset += 1
continue
}
atomSize = try Int(moovAtom.getInteger(atomHead) as UInt32)
if atomSize > moovAtom.bytesAvailable {
Logger.debug("aborting due to a bad size on an atom", category: .generic)
throw Mp4RestructureError.unableToRestructureData
}
// we need to skip the offset by `12` which come from the bytes of [size/4][type/4][version/1][flags/3]
// more info https://developer.apple.com/documentation/quicktime-file-format/chunk_offset_atom
moovAtom.offset = atomHead + 12
if moovAtom.bytesAvailable < 4 {
Logger.debug("aborting due to a malformed atom", category: .generic)
throw Mp4RestructureError.unableToRestructureData
}
// 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)
if atomType == Atoms.stco {
Logger.debug("🏗️ patching stco atom...", category: .generic)
if moovAtom.bytesAvailable < numberOfOffsetEntries * 4 {
Logger.debug("aborting due to bad atom..", category: .generic)
throw Mp4RestructureError.unableToRestructureData
}
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 + moovAtomSize
if currentOffset < 0, adjustOffset >= 0 {
throw Mp4RestructureError.unableToRestructureData
}
moovAtom.put(UInt32(adjustOffset).bigEndian)
}
} else if atomType == Atoms.co64 {
Logger.debug("🏗️ patching co64 atom...", category: .generic)
if moovAtom.bytesAvailable < numberOfOffsetEntries * 8 {
Logger.debug("aborting due to bad atom..", category: .generic)
throw Mp4RestructureError.unableToRestructureData
}
for _ in 0 ..< numberOfOffsetEntries {
let currentOffset: Int = try moovAtom.getInteger(moovAtom.offset)
// adjust the offset by adding the size of moov atom
moovAtom.put(currentOffset + moovAtomSize)
}
}
}
return (moovAtom.storage, moovAtomSize)
}
func getInteger<T: FixedWidthInteger>(data: Data, offset: Int) throws -> T {
let sizeOfInteger = MemoryLayout<T>.size
guard sizeOfInteger <= data.count else {
throw ByteBuffer.Error.eof
}
let _offset = offset + sizeOfInteger
return T(data: data[_offset - sizeOfInteger ..< _offset]).bigEndian
}
}
@@ -0,0 +1,146 @@
//
// Created by Dimitrios Chatzieleftheriou on 10/03/2024.
// Copyright © 2020 Decimal. All rights reserved.
//
import Foundation
final class RemoteMp4Restructure {
struct RestructuredData {
var initialData: Data
var mdatOffset: Int
}
private var audioData: Data
private var atomOffset: Int = 0
private var atoms: [MP4Atom] = []
private var ftyp: MP4Atom?
private var foundMoov = false
private var foundMdat = false
private var task: NetworkDataStream?
private(set) var dataOptimized: Bool = false
private var moovAtomSize: Int = 0
private let url: URL
private let networking: NetworkingClient
private let mp4Restructure: Mp4Restructure
init(url: URL, networking: NetworkingClient, restructure: Mp4Restructure = Mp4Restructure()) {
self.url = url
self.networking = networking
self.audioData = Data()
self.mp4Restructure = restructure
}
func clear() {
mp4Restructure.clear()
audioData = Data()
task?.cancel()
task = nil
}
/// Adjust the seekOffset of subtracting the moovAtomSize
/// - Parameter offset: A byte offset
/// - Returns: An adjusted byte offset
func seekAdjusted(offset: Int) -> Int {
mp4Restructure.seekAdjusted(offset: offset)
}
///
/// Gather audio and parse along the way, if moov atom is found, continue as usual
/// if mdat is found before moov:
/// - Get mdat size and make a byte request Range: bytes=mdatAtomSize- for possible moov atom
/// - once the request is complete search for an moov atom and restructure it
/// - finally, make a byte request Range: bytes=mdatOffset- to get the mdat
/// Atoms needs to be as following for the AudioFileStreamParse to work
/// [ftyp][moov][mdat]
///
func optimizeIfNeeded(completion: @escaping (Result<RestructuredData?, Error>) -> Void) {
task = networking.stream(request: urlForPartialContent(with: url, offset: 0))
.responseStream { [weak self] event in
guard let self else { return }
switch event {
case .response:
break
case let .stream(.success(response)):
guard let data = response.data else {
self.audioData = Data()
completion(.failure(Mp4RestructureError.unableToRestructureData))
return
}
self.audioData.append(data)
do {
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))
return
}
// stop request, fetch moov and restructure
self.audioData = Data()
self.task?.cancel()
self.task = nil
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: 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))
}
case let .stream(.failure(error)):
completion(.failure(Mp4RestructureError.networkError(error)))
case .complete:
break
}
}
task?.resume()
}
func fetchAndRestructureMoovAtom(offset: Int, completion: @escaping (Result<(data: Data, offset: Int), Error>) -> Void) {
networking.task(request: urlForPartialContent(with: url, offset: offset)) { [weak self] result in
guard let self else { return }
switch result {
case let .success(data):
do {
let (initialData, mdatOffset) = try self.mp4Restructure.restructureMoov(data: data)
completion(.success((initialData, mdatOffset)))
} catch {
completion(.failure(error))
}
case let .failure(failure):
completion(.failure(Mp4RestructureError.networkError(failure)))
}
}
}
private func urlForPartialContent(with url: URL, offset: Int) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.networkServiceType = .avStreaming
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
urlRequest.timeoutInterval = 60
urlRequest.addValue("*/*", forHTTPHeaderField: "Accept")
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
urlRequest.addValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
return urlRequest
}
}
@@ -8,6 +8,10 @@ import AVFoundation
import Foundation
import Network
enum RemoteAudioSourceError: Error {
case mp4NotSeekable
}
public class RemoteAudioSource: AudioStreamSource {
weak var delegate: AudioStreamSourceDelegate?
@@ -31,23 +35,25 @@ public class RemoteAudioSource: AudioStreamSource {
private var seekOffset: Int
private var supportsSeek: Bool
internal var metadataStreamProcessor: MetadataStreamSource
var metadataStreamProcessor: MetadataStreamSource
private var shouldTryParsingIcycastHeaders: Bool = false
private let icycastHeadersProcessor: IcycastHeadersProcessor
internal var audioFileHint: AudioFileTypeID {
var audioFileHint: AudioFileTypeID {
guard let output = parsedHeaderOutput, output.typeId != 0 else {
return audioFileType(fileExtension: url.pathExtension)
}
return output.typeId
}
internal let underlyingQueue: DispatchQueue
internal let streamOperationQueue: OperationQueue
internal let netStatusService: NetStatusProvider
internal var waitingForNetwork = false
internal let retrierTimeout: Retrier
private let mp4Restructure: RemoteMp4Restructure
let underlyingQueue: DispatchQueue
let streamOperationQueue: OperationQueue
let netStatusService: NetStatusProvider
var waitingForNetwork = false
let retrierTimeout: Retrier
init(networking: NetworkingClient,
metadataStreamSource: MetadataStreamSource,
@@ -74,6 +80,7 @@ public class RemoteAudioSource: AudioStreamSource {
streamOperationQueue.isSuspended = true
streamOperationQueue.name = "remote.audio.source.data.stream.queue"
retrierTimeout = retrier
mp4Restructure = RemoteMp4Restructure(url: url, networking: networkingClient)
startNetworkService()
}
@@ -128,6 +135,7 @@ public class RemoteAudioSource: AudioStreamSource {
return
}
mp4Restructure.clear()
retrierTimeout.cancel()
metadataStreamProcessor.reset()
icycastHeadersProcessor.reset()
@@ -158,8 +166,27 @@ public class RemoteAudioSource: AudioStreamSource {
}
private func performOpen(seek seekOffset: Int) {
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
if seekOffset == 0 {
initialRequest { [weak self] in
guard let self else { return }
if self.parsedHeaderOutput?.isMp4 == true {
self.handleMp4Files()
} else {
self.doPerfomOpen(seek: 0)
}
}
} else {
if mp4Restructure.dataOptimized {
let adjustedOffset = mp4Restructure.seekAdjusted(offset: seekOffset)
doPerfomOpen(seek: adjustedOffset)
} else {
doPerfomOpen(seek: seekOffset)
}
}
}
private func doPerfomOpen(seek seekOffset: Int) {
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
streamRequest = networkingClient.stream(request: urlRequest)
.responseStream { [weak self] event in
guard let self = self else { return }
@@ -170,6 +197,41 @@ public class RemoteAudioSource: AudioStreamSource {
metadataStreamProcessor.delegate = self
}
private func initialRequest(completion: @escaping () -> Void) {
let urlRequest = fetchUrlForPartialContent(with: url)
let task: NetworkDataStream = networkingClient.stream(request: urlRequest)
task.responseStream { [weak self] event in
switch event {
case let .response(urlResponse):
self?.parseResponseHeader(response: urlResponse)
task.cancel()
completion()
default:
break
}
}.resume()
}
private func handleMp4Files() {
mp4Restructure.optimizeIfNeeded { [weak self] result in
guard let self else { return }
switch result {
case let .success(value):
if let value {
self.addStreamOperation {
let audioCount = self.processAudio(data: value.initialData)
self.relativePosition += audioCount
}
self.doPerfomOpen(seek: value.mdatOffset)
} else {
self.doPerfomOpen(seek: 0)
}
case let .failure(failure):
self.delegate?.errorOccurred(source: self, error: failure)
}
}
}
// MARK: - Network Handle Methods
private func handleResponse(event: NetworkDataStream.ResponseEvent) {
@@ -195,7 +257,7 @@ public class RemoteAudioSource: AudioStreamSource {
private func handleSuccessfulStreamEvent(response: NetworkDataStream.Response) {
guard let audioData = response.data else {
self.delegate?.errorOccurred(source: self, error: NetworkError.missingData)
delegate?.errorOccurred(source: self, error: NetworkError.missingData)
return
}
addStreamOperation { [weak self] in
@@ -219,7 +281,7 @@ public class RemoteAudioSource: AudioStreamSource {
}
}
private func handleFailedStreamEvent(error: Error) {
private func handleFailedStreamEvent(error _: Error) {
if !netStatusService.isConnected {
waitingForNetwork = true
return
@@ -254,7 +316,9 @@ public class RemoteAudioSource: AudioStreamSource {
return
}
if let acceptRanges = parser.value(forHTTPHeaderField: HeaderField.acceptRanges, in: response) {
if httpStatusCode == 206 {
supportsSeek = true
} else if let acceptRanges = parser.value(forHTTPHeaderField: HeaderField.acceptRanges, in: response) {
supportsSeek = acceptRanges != "none"
}
@@ -297,6 +361,22 @@ public class RemoteAudioSource: AudioStreamSource {
return urlRequest
}
private func fetchUrlForPartialContent(with url: URL) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.networkServiceType = .avStreaming
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
urlRequest.timeoutInterval = 60
for header in additionalRequestHeaders {
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
}
urlRequest.addValue("*/*", forHTTPHeaderField: "Accept")
urlRequest.addValue("1", forHTTPHeaderField: "Icy-MetaData")
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
urlRequest.addValue("bytes=0-1", forHTTPHeaderField: "Range")
return urlRequest
}
private func retryOnError() {
retrierTimeout.retry { [weak self] in
guard let self = self else { return }
@@ -311,6 +391,7 @@ public class RemoteAudioSource: AudioStreamSource {
/// - Parameter block: A closure to be executed
private func addStreamOperation(_ block: @escaping () -> Void) {
let operation = BlockOperation(block: block)
operation.qualityOfService = .userInitiated
streamOperationQueue.addOperation(operation)
}
@@ -100,15 +100,13 @@ open class AudioPlayer {
}
/// An `AVAudioFormat` object for the canonical audio stream
private var outputAudioFormat: AVAudioFormat = {
AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100.0, channels: 2, interleaved: true)!
}()
private var outputAudioFormat: AVAudioFormat = .init(commonFormat: .pcmFormatFloat32, sampleRate: 44100.0, channels: 2, interleaved: true)!
/// Keeps track of the player's state before being paused.
private var stateBeforePaused: InternalState = .initial
/// The underlying `AVAudioEngine` object
private let audioEngine = AVAudioEngine()
private let audioEngine: AVAudioEngine
/// An `AVAudioUnit` object that represents the audio player
private(set) var player = AVAudioUnit()
/// An `AVAudioUnitTimePitch` that controls the playback rate of the audio engine
@@ -134,28 +132,38 @@ open class AudioPlayer {
public init(configuration: AudioPlayerConfiguration = .default) {
self.configuration = configuration.normalizeValues()
let engine = AVAudioEngine()
audioEngine = engine
rendererContext = AudioRendererContext(configuration: configuration, outputAudioFormat: outputAudioFormat)
playerContext = AudioPlayerContext()
entriesQueue = PlayerQueueEntries()
serializationQueue = DispatchQueue(label: "streaming.core.queue", qos: .userInitiated)
sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated)
sourceQueue = DispatchQueue(label: "source.queue", qos: .default)
entryProvider = AudioEntryProvider(networkingClient: NetworkingClient(),
underlyingQueue: sourceQueue,
outputAudioFormat: outputAudioFormat)
entryProvider = AudioEntryProvider(
networkingClient: NetworkingClient(),
underlyingQueue: sourceQueue,
outputAudioFormat: outputAudioFormat
)
fileStreamProcessor = AudioFileStreamProcessor(playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
fileStreamProcessor = AudioFileStreamProcessor(
playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription
)
frameFilterProcessor = FrameFilterProcessor(mixerNode: audioEngine.mainMixerNode)
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
playerRenderProcessor = AudioPlayerRenderProcessor(
playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription
)
frameFilterProcessor = FrameFilterProcessor(
mixerNodeProvider: {
engine.mainMixerNode
}
)
configPlayerContext()
configPlayerNode()
setupEngine()
@@ -346,7 +354,7 @@ open class AudioPlayer {
/// - Note: The nodes will be added after the default rate node
/// - Parameter node: An array of `AVAudioNode` instances
public func attach(nodes: [AVAudioNode]) {
nodes.forEach { node in
for node in nodes {
customAttachedNodes.append(node)
}
nodes.forEach(audioEngine.attach)
@@ -368,7 +376,7 @@ open class AudioPlayer {
/// Detaches the given `AVAudioNode`s from the engine
/// - Parameter node: An array of `AVAudioNode` instances
public func detachCustomAttachedNodes() {
customAttachedNodes.forEach { node in
for node in customAttachedNodes {
audioEngine.detach(node)
}
attachAndConnectDefaultNodes()
@@ -506,6 +514,7 @@ open class AudioPlayer {
/// Pauses the audio engine and stops the player's hardware
private func pauseEngine() {
guard isEngineRunning else { return }
audioEngine.reset()
audioEngine.pause()
player.auAudioUnit.stopHardware()
Logger.debug("engine paused ⏸", category: .generic)
@@ -552,8 +561,8 @@ open class AudioPlayer {
setCurrentReading(entry: entry, startPlaying: true, shouldClearQueue: true)
rendererContext.resetBuffers()
} else if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested,
playingEntry != playerContext.audioReadingEntry
playingEntry.seekRequest.requested,
playingEntry != playerContext.audioReadingEntry
{
playingEntry.audioStreamState.processedDataFormat = false
playingEntry.reset()
@@ -645,44 +654,41 @@ open class AudioPlayer {
}
private func processFinishPlaying(entry: AudioEntry?, with nextEntry: AudioEntry?) {
let playingEntry = playerContext.entriesLock.around { playerContext.audioPlayingEntry }
let playingEntry = playerContext.entriesLock.withLock { playerContext.audioPlayingEntry }
guard entry == playingEntry else { return }
let isPlayingSameItemProbablySeek = playerContext.audioPlayingEntry === nextEntry
let notifyDelegateEntryFinishedPlaying: (AudioEntry?, Bool) -> Void = { [weak self] entry, _ in
guard let self = self else { return }
if let entry = entry, !isPlayingSameItemProbablySeek {
let entryId = entry.id
let progressInFrames = entry.progressInFrames()
let progress = Double(progressInFrames) / self.outputAudioFormat.basicStreamDescription.mSampleRate
let duration = entry.duration()
asyncOnMain {
self.delegate?.audioPlayerDidFinishPlaying(player: self,
entryId: entryId,
stopReason: self.stopReason,
progress: progress,
duration: duration)
}
}
}
if let nextEntry = nextEntry {
if !isPlayingSameItemProbablySeek {
nextEntry.lock.around {
nextEntry.lock.withLock {
nextEntry.seekTime = 0
}
nextEntry.seekRequest.lock.around {
nextEntry.seekRequest.lock.withLock {
nextEntry.seekRequest.requested = false
}
}
playerContext.entriesLock.lock()
playerContext.audioPlayingEntry = nextEntry
let playingQueueEntryId = playerContext.audioPlayingEntry?.id ?? AudioEntryId(id: "")
playerContext.entriesLock.unlock()
let playingQueueEntryId = playingEntry?.id ?? AudioEntryId(id: "")
notifyDelegateEntryFinishedPlaying(entry, isPlayingSameItemProbablySeek)
if let entry = entry, !isPlayingSameItemProbablySeek {
let entryId = entry.id
let progressInFrames = entry.progressInFrames()
let progress = Double(progressInFrames) / outputAudioFormat.basicStreamDescription.mSampleRate
let duration = entry.duration()
asyncOnMain { [weak self] in
guard let self else { return }
self.delegate?.audioPlayerDidFinishPlaying(
player: self,
entryId: entryId,
stopReason: self.stopReason,
progress: progress,
duration: duration
)
}
}
if !isPlayingSameItemProbablySeek {
playerContext.setInternalState(to: .waitingForData)
@@ -692,10 +698,29 @@ open class AudioPlayer {
}
}
} else {
notifyDelegateEntryFinishedPlaying(entry, isPlayingSameItemProbablySeek)
playerContext.entriesLock.lock()
playerContext.audioPlayingEntry = nil
playerContext.entriesLock.unlock()
if let entry = entry, !isPlayingSameItemProbablySeek {
let entryId = entry.id
let progressInFrames = entry.progressInFrames()
let progress = Double(progressInFrames) / outputAudioFormat.basicStreamDescription.mSampleRate
let duration = entry.duration()
sourceQueue.async { [weak self] in
guard let self else { return }
self.processSource()
asyncOnMain {
self.delegate?.audioPlayerDidFinishPlaying(
player: self,
entryId: entryId,
stopReason: self.stopReason,
progress: progress,
duration: duration
)
}
}
}
}
sourceQueue.async { [weak self] in
self?.processSource()
@@ -724,7 +749,6 @@ open class AudioPlayer {
private func raiseUnexpected(error: AudioPlayerError) {
playerContext.setInternalState(to: .error)
// todo raise on main thread from playback thread
asyncOnMain { [weak self] in
guard let self = self else { return }
self.delegate?.audioPlayerUnexpectedError(player: self, error: error)
@@ -5,13 +5,13 @@
import Foundation
internal final class AudioPlayerContext {
var stopReason: Protected<AudioPlayerStopReason>
final class AudioPlayerContext {
var stopReason: Atomic<AudioPlayerStopReason>
var state: Protected<AudioPlayerState>
var state: Atomic<AudioPlayerState>
var stateChanged: ((_ oldState: AudioPlayerState, _ newState: AudioPlayerState) -> Void)?
var muted: Protected<Bool>
var muted: Atomic<Bool>
var internalState: AudioPlayer.InternalState {
playerInternalState.value
@@ -24,12 +24,12 @@ internal final class AudioPlayerContext {
/// This is the player's internal state to use
/// - NOTE: Do not use directly instead use the `internalState` to set and get the property
/// or the `setInternalState(to:when:)`method
private var playerInternalState = Protected<AudioPlayer.InternalState>(.initial)
private var playerInternalState = Atomic<AudioPlayer.InternalState>(.initial)
init() {
stopReason = Protected<AudioPlayerStopReason>(.none)
state = Protected<AudioPlayerState>(.ready)
muted = Protected<Bool>(false)
stopReason = Atomic<AudioPlayerStopReason>(.none)
state = Atomic<AudioPlayerState>(.ready)
muted = Atomic<Bool>(false)
entriesLock = UnfairLock()
}
@@ -38,11 +38,13 @@ internal final class AudioPlayerContext {
/// - parameter state: The new `PlayerInternalState`
/// - parameter inState: If the `inState` expression is not nil, the internalState will be set if the evaluated expression is `true`
/// - NOTE: This sets the underlying `__playerInternalState` variable
internal func setInternalState(to state: AudioPlayer.InternalState,
when inState: ((AudioPlayer.InternalState) -> Bool)? = nil)
func setInternalState(to state: AudioPlayer.InternalState,
when inState: ((AudioPlayer.InternalState) -> Bool)? = nil)
{
let newValues = playerStateAndStopReason(for: state)
stopReason.write { $0 = newValues.stopReason }
if let stopReason = newValues.stopReason {
self.stopReason.write { $0 = stopReason }
}
guard state != internalState else { return }
if let inState = inState, !inState(internalState) {
return
@@ -30,26 +30,26 @@ extension AudioPlayer {
/// Helper method that returns `AudioPlayerState` and `StopReason` based on the given `InternalState`
/// - Parameter internalState: A value of `InternalState`
/// - Returns: A tuple of `(AudioPlayerState, AudioPlayerStopReason)`
func playerStateAndStopReason(for internalState: AudioPlayer.InternalState) -> (state: AudioPlayerState,
stopReason: AudioPlayerStopReason)
{
func playerStateAndStopReason(
for internalState: AudioPlayer.InternalState
) -> (state: AudioPlayerState, stopReason: AudioPlayerStopReason?) {
switch internalState {
case .initial:
return (.ready, .none)
return (.ready, AudioPlayerStopReason.none)
case .running, .playing, .waitingForDataAfterSeek:
return (.playing, .none)
return (.playing, AudioPlayerStopReason.none)
case .pendingNext, .rebuffering, .waitingForData:
return (.bufferring, .none)
return (.bufferring, AudioPlayerStopReason.none)
case .stopped:
return (.stopped, .userAction)
return (.stopped, nil)
case .paused:
return (.paused, .none)
return (.paused, AudioPlayerStopReason.none)
case .disposed:
return (.disposed, .userAction)
case .error:
return (.error, .error)
return (.error, AudioPlayerStopReason.error)
default:
return (.ready, .none)
return (.ready, AudioPlayerStopReason.none)
}
}
@@ -6,10 +6,10 @@
import AVFoundation
import CoreAudio
internal var maxFramesPerSlice: AVAudioFrameCount = 8192
var maxFramesPerSlice: AVAudioFrameCount = 8192
final class AudioRendererContext {
var waiting = Protected<Bool>(false)
var waiting = Atomic<Bool>(false)
let lock = UnfairLock()
@@ -24,7 +24,7 @@ final class AudioRendererContext {
let framesRequiredAfterRebuffering: UInt32
let framesRequiredForDataAfterSeekPlaying: UInt32
var waitingForDataAfterSeekFrameCount = Protected<Int32>(0)
var waitingForDataAfterSeekFrameCount = Atomic<Int32>(0)
private let configuration: AudioPlayerConfiguration
@@ -34,13 +34,13 @@ final class AudioFileStreamProcessor {
private let rendererContext: AudioRendererContext
private let outputAudioFormat: AudioStreamBasicDescription
internal var audioFileStream: AudioFileStreamID?
internal var audioConverter: AudioConverterRef?
internal var discontinuous: Bool = false
internal var inputFormat = AudioStreamBasicDescription()
var audioFileStream: AudioFileStreamID?
var audioConverter: AudioConverterRef?
var discontinuous: Bool = false
var inputFormat = AudioStreamBasicDescription()
internal var currentFileFormat: String = ""
internal let fileFormatsForDelayedConverterCreation: Set = ["fa4m", "f4pm"]
var currentFileFormat: String = ""
let fileFormatsForDelayedConverterCreation: Set = ["fa4m", "f4pm"]
var isFileStreamOpen: Bool {
audioFileStream != nil
@@ -116,26 +116,19 @@ final class AudioFileStreamProcessor {
readingEntry.lock.unlock()
let bitrate = readingEntry.calculatedBitrate()
if readingEntry.processedPacketsState.count > 0, bitrate > 0 {
if readingEntry.packetDuration > 0, bitrate > 0 {
var ioFlags = AudioFileStreamSeekFlags(rawValue: 0)
var packetsAlignedByteOffset: Int64 = 0
let seekPacket = Int64(floor(readingEntry.seekRequest.time / readingEntry.packetDuration))
let seekStatus = AudioFileStreamSeek(stream, seekPacket, &packetsAlignedByteOffset, &ioFlags)
guard seekStatus == noErr else {
let streamError = AudioFileStreamError(status: seekStatus)
Logger.error("seek failed %@", category: .generic, args: streamError.debugDescription)
return
}
let dataOffset = Int64(readingEntry.audioStreamState.dataOffset)
if !ioFlags.contains(.offsetIsEstimated) {
seekByteOffset = packetsAlignedByteOffset + dataOffset
let delta = Double((seekByteOffset - dataOffset) - packetsAlignedByteOffset) / bitrate * 8
if seekStatus == noErr, !ioFlags.contains(.offsetIsEstimated) {
let delta = Double((seekByteOffset - dataOffset) - packetsAlignedByteOffset) / (bitrate * 8)
readingEntry.lock.lock()
readingEntry.seekTime -= delta
readingEntry.lock.unlock()
seekByteOffset = packetsAlignedByteOffset + dataOffset
}
}
@@ -235,9 +228,11 @@ final class AudioFileStreamProcessor {
case kAudioFileStreamProperty_ReadyToProducePackets:
// check converter for discontinuous stream
processReadyToProducePackets(fileStream: fileStream)
processPacketUpperBoundAndMaxPacketSize(fileStream: fileStream)
case kAudioFileStreamProperty_FormatList:
processFormatList(fileStream: fileStream)
default: break
default:
break
}
}
@@ -279,6 +274,9 @@ final class AudioFileStreamProcessor {
entry.audioStreamFormat = audioStreamFormat
}
entry.sampleRate = Float(audioStreamFormat.mSampleRate)
entry.packetDuration = Double(audioStreamFormat.mFramesPerPacket) / Double(entry.sampleRate)
var packetBufferSize: UInt32 = 0
var status = fileStreamGetProperty(value: &packetBufferSize,
fileStream: fileStream,
@@ -291,7 +289,7 @@ final class AudioFileStreamProcessor {
packetBufferSize = 2048 // default value
}
}
entry.lock.around {
entry.lock.withLock {
entry.processedPacketsState.bufferSize = packetBufferSize
}
@@ -301,6 +299,25 @@ final class AudioFileStreamProcessor {
}
}
private func processPacketUpperBoundAndMaxPacketSize(fileStream: AudioFileStreamID) {
guard let entry = playerContext.audioReadingEntry else { return }
var packetBufferSize: UInt32 = 0
var status = fileStreamGetProperty(value: &packetBufferSize,
fileStream: fileStream,
propertyId: kAudioFileStreamProperty_PacketSizeUpperBound)
if status != 0 || packetBufferSize == 0 {
status = fileStreamGetProperty(value: &packetBufferSize,
fileStream: fileStream,
propertyId: kAudioFileStreamProperty_MaximumPacketSize)
if status != 0 || packetBufferSize == 0 {
packetBufferSize = 2048 // default value
}
}
entry.lock.withLock {
entry.processedPacketsState.bufferSize = packetBufferSize
}
}
private func processDataByteCount(fileStream: AudioFileStreamID) {
guard let entry = playerContext.audioReadingEntry else { return }
var audioDataByteCount: UInt64 = 0
@@ -90,7 +90,7 @@ final class AudioPlayerRenderProcessor: NSObject {
waitForBuffer = true
}
} else if state == .waitingForDataAfterSeek {
var requiredFramesToStart: Int = 1024
var requiredFramesToStart = 1024
if framesState.lastFrameQueued >= 0 {
requiredFramesToStart = min(requiredFramesToStart, framesState.lastFrameQueued - framesState.queued)
}
@@ -179,9 +179,9 @@ final class AudioPlayerRenderProcessor: NSObject {
if totalFramesCopied < inNumberFrames {
let delta = inNumberFrames - totalFramesCopied
writeSilence(outputBuffer: &bufferList.mBuffers,
outputBufferSize: Int(delta * frameSizeInBytes),
offset: Int(totalFramesCopied * frameSizeInBytes))
if let mData = bufferList.mBuffers.mData {
memset(mData + Int(totalFramesCopied * frameSizeInBytes), 0, Int(delta * frameSizeInBytes))
}
if playingEntry != nil || AudioPlayer.InternalState.waiting.contains(state) {
if playerContext.internalState != .rebuffering {
@@ -198,7 +198,6 @@ final class AudioPlayerRenderProcessor: NSObject {
state.contains(.running) && state != .playing
}
}
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 }
}
} else {
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 }
@@ -211,7 +210,7 @@ final class AudioPlayerRenderProcessor: NSObject {
}
currentPlayingEntry.lock.lock()
var extraFramesPlayedNotAssigned: Int = 0
var extraFramesPlayedNotAssigned = 0
var framesPlayedForCurrent = Int(totalFramesCopied)
if currentPlayingEntry.framesState.lastFrameQueued >= 0 {
@@ -328,7 +327,5 @@ final class AudioPlayerRenderProcessor: NSObject {
{
guard let mData = outputBuffer.mData else { return }
memset(mData + offset, 0, outputBufferSize)
outputBuffer.mDataByteSize = UInt32(outputBufferSize)
outputBuffer.mNumberChannels = outputAudioFormat.mChannelsPerFrame
}
}
@@ -78,14 +78,15 @@ final class FrameFilterProcessor: NSObject, FrameFiltering {
}
private let lock = UnfairLock()
private let mixerNode: AVAudioMixerNode
private let mixerNodeProvider: () -> AVAudioMixerNode
private lazy var mixerNode: AVAudioMixerNode = mixerNodeProvider()
private(set) var entries: [FilterEntry] = []
private var hasInstalledTap: Bool = false
init(mixerNode: AVAudioMixerNode) {
self.mixerNode = mixerNode
init(mixerNodeProvider: @escaping (() -> AVAudioMixerNode)) {
self.mixerNodeProvider = mixerNodeProvider
}
public func add(entry: FilterEntry) {
@@ -7,7 +7,7 @@ import AudioToolbox
import Foundation
/// mapping from mime types to `AudioFileTypeID`
internal let fileTypesFromMimeType: [String: AudioFileTypeID] =
let fileTypesFromMimeType: [String: AudioFileTypeID] =
[
"audio/mp3": kAudioFileMP3Type,
"audio/mpg": kAudioFileMP3Type,
@@ -44,7 +44,7 @@ func audioFileType(mimeType: String) -> AudioFileTypeID {
}
/// mapping from file extension to `AudioFileTypeID`
internal let fileTypesFromFileExtension: [String: AudioFileTypeID] =
let fileTypesFromFileExtension: [String: AudioFileTypeID] =
[
"mp3": kAudioFileMP3Type,
"wav": kAudioFileWAVEType,
@@ -17,14 +17,14 @@ final class PlayerQueueEntries {
/// Returns `true` when both underlying entries are empty
var isEmpty: Bool {
lock.around {
lock.withLock {
bufferring.isEmpty && upcoming.isEmpty
}
}
/// Returns the count of both underlying entries
var count: Int {
lock.around {
lock.withLock {
bufferring.count + upcoming.count
}
}
@@ -6,7 +6,7 @@
import AudioToolbox.AudioFile
import Foundation
struct HeaderField {
enum HeaderField {
public static let acceptRanges = "Accept-Ranges"
public static let contentLength = "Content-Length"
public static let contentType = "Content-Type"
@@ -22,6 +22,11 @@ struct HTTPHeaderParserOutput {
let typeId: AudioFileTypeID
// Metadata Support
let metadataStep: Int
let seekable: Bool
var isMp4: Bool {
(typeId == kAudioFileMPEG4Type || typeId == kAudioFileM4AType)
}
}
protocol HTTPHeaderParsing: Parser {
@@ -46,7 +51,7 @@ struct HTTPHeaderParser: HTTPHeaderParsing {
typeId = audioFileType(mimeType: contentType)
}
var fileLength: Int = 0
var fileLength = 0
if input.statusCode == 200 {
let contentLength = value(forHTTPHeaderField: HeaderField.contentLength, in: input)
if let contentLength = contentLength, let length = Int(contentLength) {
@@ -70,9 +75,12 @@ struct HTTPHeaderParser: HTTPHeaderParsing {
metadataStep = intValue
}
return HTTPHeaderParserOutput(fileLength: fileLength,
typeId: typeId,
metadataStep: metadataStep)
return HTTPHeaderParserOutput(
fileLength: fileLength,
typeId: typeId,
metadataStep: metadataStep,
seekable: input.statusCode == 206
)
}
}
@@ -91,10 +99,8 @@ extension Parser where Self: HTTPHeaderParsing {
private func valueForCaseInsensitiveKey(_ key: String, fields: [String: String]) -> String? {
let keyToBeFound = key.lowercased()
for (key, value) in fields {
if key.lowercased() == keyToBeFound {
return value
}
for (key, value) in fields where key.lowercased() == keyToBeFound {
return value
}
return nil
}
@@ -25,8 +25,11 @@ struct IcycastHeaderParser: Parser {
let contentType = result[HeaderField.contentType.lowercased()] ?? "audio/mpeg"
let typeId = audioFileType(mimeType: contentType)
return HTTPHeaderParserOutput(fileLength: 0,
typeId: typeId,
metadataStep: metadataStep)
return HTTPHeaderParserOutput(
fileLength: 0,
typeId: typeId,
metadataStep: metadataStep,
seekable: false
)
}
}
@@ -7,27 +7,27 @@ import XCTest
@testable import AudioStreaming
class ProtectedTests: XCTestCase {
class AtomicTests: XCTestCase {
func testProtectedValuesAreAccessedSafely() {
measure {
let protected = Protected<Int>(0)
let atomic = Atomic<Int>(0)
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
_ = protected.value
protected.write { $0 += 1 }
DispatchQueue.concurrentPerform(iterations: 100_000) { _ in
_ = atomic.value
atomic.write { $0 += 1 }
}
XCTAssertEqual(protected.value, 1_000_000)
XCTAssertEqual(atomic.value, 100_000)
}
}
func testThatProtectedReadAndWriteAreSafe() {
measure {
let initialValue = "aValue"
let protected = Protected<String>(initialValue)
let protected = Atomic<String>(initialValue)
DispatchQueue.concurrentPerform(iterations: 1000) { i in
_ = protected.read { $0 }
_ = protected.value
protected.write { $0 = "\(i)" }
}
+1 -1
View File
@@ -1,5 +1,5 @@
//
// BiMap.swift
// BiMapTests.swift
// AudioStreamingTests
//
// Created by Dimitrios Chatzieleftheriou on 26/05/2020.
@@ -0,0 +1,75 @@
//
// Copyright © Blockchain Luxembourg S.A. All rights reserved.
import XCTest
@testable import AudioStreaming
final class ByteBufferTests: XCTestCase {
func testWriteAndReadBytes() {
var buffer = ByteBuffer(size: 10)
// Write bytes to the buffer
let testData = Data([0x01, 0x02, 0x03, 0x04])
buffer.writeBytes(testData)
buffer.rewind()
// Read the written bytes
do {
let readData = try buffer.readBytes(4)
XCTAssertEqual(readData, testData)
} catch {
XCTFail("Error reading bytes: \(error)")
}
}
func testWriteAndReadInteger() {
var buffer = ByteBuffer(size: 8)
// Write integer to the buffer
let testInteger: UInt32 = 123_456_789
buffer.put(testInteger)
buffer.rewind()
// Read the written integer
do {
let readInteger: UInt32 = try buffer.getInteger()
XCTAssertEqual(readInteger, testInteger.bigEndian)
} catch {
XCTFail("Error reading integer: \(error)")
}
}
func testWriteAndReadFloat() {
var buffer = ByteBuffer(size: 8)
// Write float to the buffer
let testFloat: Float = 123.456
buffer.put(testFloat)
buffer.rewind()
// Read the written float
do {
let readFloat: Float = try buffer.getFloat()
XCTAssertEqual(readFloat, testFloat, accuracy: 0.001)
} catch {
XCTFail("Error reading float: \(error)")
}
}
func testWriteAndReadDouble() {
var buffer = ByteBuffer(size: 8)
// Write double to the buffer
let testDouble = 123.456
buffer.put(testDouble)
buffer.rewind()
// Read the written double
do {
let readDouble: Double = try buffer.getDouble()
XCTAssertEqual(readDouble, testDouble, accuracy: 0.001)
} catch {
XCTFail("Error reading double: \(error)")
}
}
}
@@ -1,5 +1,5 @@
//
// DispatchReadSourceTests.swift
// DispatchTimerSourceTests.swift
// AudioStreamingTests
//
// Created by Dimitrios Chatzieleftheriou on 25/10/2020.
@@ -4,7 +4,6 @@
//
import XCTest
@testable import AudioStreaming
class NetworkingClientTests: XCTestCase {
+4 -2
View File
@@ -10,12 +10,14 @@ Under the hood `AudioStreaming` uses `AVAudioEngine` and `CoreAudio` for playbac
- AIFF, AIFC, WAVE, CAF, NeXT, ADTS, MPEG Audio Layer 3, AAC audio formats
- M4A (_Optimized files only_)
As of 1.2.0 version, there's support for non-optimized remote M4A, please report any issues
Known limitations:
- As described above non-optimised M4A files are not supported this is a limitation of [AudioFileStream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services?language=swift)
~~- As described above non-optimised M4A files are not supported this is a limitation of [AudioFileStream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services?language=swift)~~
# Requirements
- iOS 12.0+
- iOS 13.0+
- Swift 5.x
# Using AudioStreaming