Compare commits

..

12 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
45 changed files with 1173 additions and 201 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.
@@ -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;
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)
}
}
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,14 +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 .localWave:
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
+2 -2
View File
@@ -1,13 +1,13 @@
Pod::Spec.new do |s|
s.name = 'AudioStreaming'
s.version = '1.1.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']
+34 -12
View File
@@ -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,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 = 1.1.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 = 1.1.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"
}
}
}
@@ -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
}
+3 -3
View File
@@ -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)
}
}
+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")
-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,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
@@ -92,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 {
@@ -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() {
@@ -55,7 +66,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
// no-op
func suspend() { }
func suspend() {}
func resume() {
guard let inputStream = inputStream else {
@@ -73,27 +84,30 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
private func performOpen(seek seekOffset: Int) throws {
var reopened = false
let streamStatus = inputStream?.streamStatus ?? .closed
if streamStatus == .notOpen || streamStatus == .closed || streamStatus == .error || streamStatus == .atEnd {
let status = inputStream?.streamStatus ?? .closed
if status == .atEnd || status == .closed || status == .error {
reopened = true
close()
try open()
}
guard let inputStream = inputStream else {
return
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()
}
}
}
}
@@ -103,13 +117,51 @@ 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()
}
}
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
@@ -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,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,7 +133,7 @@ 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()
@@ -143,22 +141,29 @@ open class AudioPlayer {
serializationQueue = DispatchQueue(label: "streaming.core.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
)
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
playerRenderProcessor = AudioPlayerRenderProcessor(
playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription
)
frameFilterProcessor = FrameFilterProcessor(mixerNodeProvider: {
engine.mainMixerNode
})
frameFilterProcessor = FrameFilterProcessor(
mixerNodeProvider: {
engine.mainMixerNode
}
)
configPlayerContext()
configPlayerNode()
setupEngine()
@@ -349,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)
@@ -371,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()
@@ -509,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)
@@ -555,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()
@@ -653,24 +659,6 @@ open class AudioPlayer {
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.withLock {
@@ -685,7 +673,22 @@ open class AudioPlayer {
let playingQueueEntryId = playerContext.audioPlayingEntry?.id ?? AudioEntryId(id: "")
playerContext.entriesLock.unlock()
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)
@@ -695,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()
@@ -727,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,7 +5,7 @@
import Foundation
internal final class AudioPlayerContext {
final class AudioPlayerContext {
var stopReason: Atomic<AudioPlayerStopReason>
var state: Atomic<AudioPlayerState>
@@ -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,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
@@ -122,20 +122,13 @@ final class AudioFileStreamProcessor {
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
}
}
@@ -304,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 {
@@ -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 {
@@ -327,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,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
)
}
}
+2 -2
View File
@@ -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 -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