Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eaaac59ef4 | |||
| 413afb2ae1 | |||
| 6434886371 | |||
| 9cbe71d2b4 | |||
| 374da9bc22 | |||
| 2a587c2f8a | |||
| 421a50d591 | |||
| 31ee80fa8b | |||
| 44acae2d17 | |||
| aba2336698 | |||
| 8787f9b7b5 | |||
| 678c0e159a | |||
| 95142492cf | |||
| cf7c66fa9e | |||
| 458a59c497 | |||
| ee42a3bb12 | |||
| e2867ed343 | |||
| 38d0bdb5d9 | |||
| b91ed7cd89 | |||
| 214e58859e |
Vendored
BIN
Binary file not shown.
@@ -8,6 +8,7 @@
|
||||
|
||||
/* 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 */; };
|
||||
@@ -44,6 +45,7 @@
|
||||
|
||||
/* 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>"; };
|
||||
@@ -80,6 +82,7 @@
|
||||
B524D59D2560177C00F5A88F /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
98C82AE12B8CA0F000AED485 /* bensound-jazzyfrenchy.m4a */,
|
||||
9848089F28C0F549001160E6 /* hipjazz.wav */,
|
||||
B524D59B2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 */,
|
||||
B5AEDBDD2475274D007D8101 /* Assets.xcassets */,
|
||||
@@ -214,6 +217,7 @@
|
||||
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;
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -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.
|
||||
|
||||
@@ -139,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -16,6 +16,8 @@ enum AudioContent: Int, CaseIterable {
|
||||
case radiox
|
||||
case khruangbin
|
||||
case piano
|
||||
case optimized
|
||||
case nonOptimized
|
||||
case remoteWave
|
||||
case local
|
||||
case localWave
|
||||
@@ -42,6 +44,10 @@ enum AudioContent: Int, CaseIterable {
|
||||
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)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +73,10 @@ enum AudioContent: Int, CaseIterable {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +96,10 @@ 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")!
|
||||
return URL(fileURLWithPath: path)
|
||||
@@ -93,7 +107,7 @@ enum AudioContent: Int, CaseIterable {
|
||||
let path = Bundle.main.path(forResource: "hipjazz", ofType: "wav")!
|
||||
return URL(fileURLWithPath: path)
|
||||
case .remoteWave:
|
||||
return URL(string: "https://file-examples.com/storage/fe183d9197630fb5c969255/2017/11/file_example_WAV_5MG.wav")!
|
||||
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,6 +14,7 @@ struct PlaylistItem: Equatable {
|
||||
case paused
|
||||
case buffering
|
||||
case stopped
|
||||
case error
|
||||
}
|
||||
|
||||
let url: URL
|
||||
|
||||
@@ -7,7 +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 */; };
|
||||
@@ -34,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 */; };
|
||||
@@ -94,7 +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>"; };
|
||||
@@ -122,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>"; };
|
||||
@@ -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,8 +337,8 @@
|
||||
B592E13025460883008866FB /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
98DC00CB2B961F5E0068900A /* ByteBuffer.swift */,
|
||||
98CC396D28BD651E006C9FF9 /* Atomic.swift */,
|
||||
B573733F254DE43E003DFBEC /* measure.swift */,
|
||||
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */,
|
||||
B57829CE2548B32B00C78D36 /* Lock.swift */,
|
||||
B500731F24D00BAC00BB4475 /* Logger.swift */,
|
||||
@@ -431,8 +447,8 @@
|
||||
B5F883B42476DABE00D277C1 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B592E11E2545FF33008866FB /* Structures */,
|
||||
B55CE97624813BA10001C498 /* Extensions */,
|
||||
B592E11E2545FF33008866FB /* Structures */,
|
||||
B5276B70247D4D3D00D2F56A /* Network */,
|
||||
B592E13025460883008866FB /* Helpers */,
|
||||
);
|
||||
@@ -447,6 +463,7 @@
|
||||
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 */
|
||||
|
||||
@@ -608,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 */,
|
||||
@@ -618,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 */,
|
||||
@@ -634,11 +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 */,
|
||||
98ABF69E2BAB07A20059C441 /* Mp4Restructure.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -655,6 +674,7 @@
|
||||
B592E134254608B4008866FB /* DispatchTimerSourceTests.swift in Sources */,
|
||||
B55CEAB82485172D0001C498 /* HTTPHeaderParserTests.swift in Sources */,
|
||||
B592E12925460146008866FB /* BiMapTests.swift in Sources */,
|
||||
98DC00CE2B9726380068900A /* ByteBufferTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -721,7 +741,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -780,7 +800,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -805,7 +825,7 @@
|
||||
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",
|
||||
@@ -836,7 +856,7 @@
|
||||
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",
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ protocol Lock {
|
||||
final class UnfairLock: Lock {
|
||||
@usableFromInline let unfairLock: UnsafeMutablePointer<os_unfair_lock>
|
||||
|
||||
internal init() {
|
||||
init() {
|
||||
unfairLock = .allocate(capacity: 1)
|
||||
unfairLock.initialize(to: os_unfair_lock())
|
||||
}
|
||||
@@ -48,13 +48,13 @@ final class UnfairLock: Lock {
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
internal func lock() {
|
||||
func lock() {
|
||||
os_unfair_lock_lock(unfairLock)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
internal func unlock() {
|
||||
func unlock() {
|
||||
os_unfair_lock_unlock(unfairLock)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,13 +70,13 @@ 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) {
|
||||
func remove(task: NetworkDataStream) {
|
||||
tasksLock.withLock {
|
||||
if !tasks.isEmpty {
|
||||
tasks[task] = nil
|
||||
@@ -84,6 +84,21 @@ internal final class NetworkingClient {
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -100,13 +115,13 @@ internal final class NetworkingClient {
|
||||
// MARK: StreamTaskProvider conformance
|
||||
|
||||
extension NetworkingClient: StreamTaskProvider {
|
||||
internal func dataStream(for request: URLSessionTask) -> NetworkDataStream? {
|
||||
func dataStream(for request: URLSessionTask) -> NetworkDataStream? {
|
||||
tasksLock.withLock {
|
||||
tasks[request] ?? nil
|
||||
}
|
||||
}
|
||||
|
||||
internal func sessionTask(for stream: NetworkDataStream) -> URLSessionTask? {
|
||||
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
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
||||
}
|
||||
|
||||
// no-op
|
||||
func suspend() { }
|
||||
func suspend() {}
|
||||
|
||||
func resume() {
|
||||
guard let inputStream = inputStream else {
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// 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 debugDescription: String {
|
||||
"[Atom][size: \(size))][type: \(Atoms.integerToFourCC(type) ?? "")][offset: \(offset)]"
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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 = mdatAtom.offset - Atoms.atomPreampleSize
|
||||
return (initialData, mdatOffset)
|
||||
}
|
||||
|
||||
func checkIsOptimized(data: Data) -> (optimized: Bool, offset: Int?)? {
|
||||
while atomOffset < UInt64(data.count) {
|
||||
let atomSize = Int(readUInt32FromData(data: data, offset: atomOffset))
|
||||
let atomType = Int(readUInt32FromData(data: data, offset: atomOffset + 4))
|
||||
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:
|
||||
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 (true, nil)
|
||||
} else if !foundMoov && foundMdat {
|
||||
Logger.debug("🕵️ detected an non-optimized mp4", category: .generic)
|
||||
let possibleMoovOffset = Int(atomOffset) + atomSize
|
||||
return (false, possibleMoovOffset)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
private func readUInt32FromData(data: Data, offset: Int) -> UInt32 {
|
||||
let valueData = data.subdata(in: offset ..< offset + 4)
|
||||
return UInt32(bigEndian: valueData.withUnsafeBytes { $0.load(as: UInt32.self) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// 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)
|
||||
let value = self.mp4Restructure.checkIsOptimized(data: self.audioData)
|
||||
if let value {
|
||||
if let offset = value.offset, !value.optimized {
|
||||
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: offset) { 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))
|
||||
}
|
||||
}
|
||||
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,9 +100,7 @@ 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
|
||||
@@ -135,30 +133,32 @@ open class AudioPlayer {
|
||||
public init(configuration: AudioPlayerConfiguration = .default) {
|
||||
self.configuration = configuration.normalizeValues()
|
||||
let engine = AVAudioEngine()
|
||||
self.audioEngine = engine
|
||||
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: .default)
|
||||
|
||||
|
||||
entryProvider = AudioEntryProvider(
|
||||
networkingClient: NetworkingClient(),
|
||||
underlyingQueue: sourceQueue,
|
||||
outputAudioFormat: outputAudioFormat
|
||||
)
|
||||
|
||||
|
||||
fileStreamProcessor = AudioFileStreamProcessor(
|
||||
playerContext: playerContext,
|
||||
rendererContext: rendererContext,
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
||||
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription
|
||||
)
|
||||
|
||||
playerRenderProcessor = AudioPlayerRenderProcessor(
|
||||
playerContext: playerContext,
|
||||
rendererContext: rendererContext,
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
||||
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription
|
||||
)
|
||||
|
||||
frameFilterProcessor = FrameFilterProcessor(
|
||||
mixerNodeProvider: {
|
||||
engine.mainMixerNode
|
||||
@@ -354,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)
|
||||
@@ -376,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()
|
||||
@@ -514,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)
|
||||
@@ -560,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()
|
||||
@@ -675,7 +676,7 @@ open class AudioPlayer {
|
||||
if let entry = entry, !isPlayingSameItemProbablySeek {
|
||||
let entryId = entry.id
|
||||
let progressInFrames = entry.progressInFrames()
|
||||
let progress = Double(progressInFrames) / self.outputAudioFormat.basicStreamDescription.mSampleRate
|
||||
let progress = Double(progressInFrames) / outputAudioFormat.basicStreamDescription.mSampleRate
|
||||
let duration = entry.duration()
|
||||
asyncOnMain { [weak self] in
|
||||
guard let self else { return }
|
||||
@@ -703,7 +704,7 @@ open class AudioPlayer {
|
||||
if let entry = entry, !isPlayingSameItemProbablySeek {
|
||||
let entryId = entry.id
|
||||
let progressInFrames = entry.progressInFrames()
|
||||
let progress = Double(progressInFrames) / self.outputAudioFormat.basicStreamDescription.mSampleRate
|
||||
let progress = Double(progressInFrames) / outputAudioFormat.basicStreamDescription.mSampleRate
|
||||
let duration = entry.duration()
|
||||
|
||||
sourceQueue.async { [weak self] in
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
internal final class AudioPlayerContext {
|
||||
final class AudioPlayerContext {
|
||||
var stopReason: Atomic<AudioPlayerStopReason>
|
||||
|
||||
var state: Atomic<AudioPlayerState>
|
||||
@@ -38,8 +38,8 @@ 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)
|
||||
if let stopReason = newValues.stopReason {
|
||||
|
||||
@@ -32,8 +32,7 @@ extension AudioPlayer {
|
||||
/// - Returns: A tuple of `(AudioPlayerState, AudioPlayerStopReason)`
|
||||
func playerStateAndStopReason(
|
||||
for internalState: AudioPlayer.InternalState
|
||||
) -> (state: AudioPlayerState, stopReason: AudioPlayerStopReason?)
|
||||
{
|
||||
) -> (state: AudioPlayerState, stopReason: AudioPlayerStopReason?) {
|
||||
switch internalState {
|
||||
case .initial:
|
||||
return (.ready, AudioPlayerStopReason.none)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import AVFoundation
|
||||
import CoreAudio
|
||||
|
||||
internal var maxFramesPerSlice: AVAudioFrameCount = 8192
|
||||
var maxFramesPerSlice: AVAudioFrameCount = 8192
|
||||
|
||||
final class AudioRendererContext {
|
||||
var waiting = Atomic<Bool>(false)
|
||||
|
||||
@@ -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
|
||||
@@ -237,7 +237,8 @@ final class AudioFileStreamProcessor {
|
||||
processReadyToProducePackets(fileStream: fileStream)
|
||||
case kAudioFileStreamProperty_FormatList:
|
||||
processFormatList(fileStream: fileStream)
|
||||
default: break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -210,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 {
|
||||
|
||||
@@ -78,10 +78,8 @@ final class FrameFilterProcessor: NSObject, FrameFiltering {
|
||||
}
|
||||
|
||||
private let lock = UnfairLock()
|
||||
private let mixerNodeProvider: (() -> AVAudioMixerNode)
|
||||
private lazy var mixerNode: AVAudioMixerNode = {
|
||||
return mixerNodeProvider()
|
||||
}()
|
||||
private let mixerNodeProvider: () -> AVAudioMixerNode
|
||||
private lazy var mixerNode: AVAudioMixerNode = mixerNodeProvider()
|
||||
|
||||
private(set) var entries: [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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ class AtomicTests: XCTestCase {
|
||||
measure {
|
||||
let atomic = Atomic<Int>(0)
|
||||
|
||||
DispatchQueue.concurrentPerform(iterations: 100000) { _ in
|
||||
DispatchQueue.concurrentPerform(iterations: 100_000) { _ in
|
||||
_ = atomic.value
|
||||
atomic.write { $0 += 1 }
|
||||
}
|
||||
|
||||
XCTAssertEqual(atomic.value, 100000)
|
||||
XCTAssertEqual(atomic.value, 100_000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user