Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 738397c637 | |||
| 1f70860473 |
@@ -101,7 +101,7 @@ enum AudioContent: Int, CaseIterable {
|
||||
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")!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '1.2.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'
|
||||
|
||||
@@ -831,7 +831,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
MARKETING_VERSION = 1.2.1;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
@@ -862,7 +862,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.0;
|
||||
MARKETING_VERSION = 1.2.1;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
||||
@@ -92,7 +92,9 @@ 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() {
|
||||
@@ -73,18 +84,32 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
||||
}
|
||||
|
||||
private func performOpen(seek seekOffset: Int) throws {
|
||||
close()
|
||||
try open()
|
||||
|
||||
guard let inputStream = inputStream else {
|
||||
return
|
||||
var reopened = false
|
||||
let status = inputStream?.streamStatus ?? .closed
|
||||
if status == .atEnd || status == .closed || status == .error {
|
||||
reopened = true
|
||||
close()
|
||||
try open()
|
||||
}
|
||||
|
||||
if inputStream.setProperty(seekOffset, forKey: .fileCurrentOffsetKey) {
|
||||
position = seekOffset
|
||||
var offset = seekOffset
|
||||
if isMp4, mp4Restructure.dataOptimized {
|
||||
offset = mp4Restructure.seekAdjusted(offset: seekOffset)
|
||||
}
|
||||
|
||||
if inputStream?.setProperty(offset, forKey: .fileCurrentOffsetKey) == true {
|
||||
position = offset
|
||||
} else {
|
||||
position = 0
|
||||
}
|
||||
|
||||
if !reopened {
|
||||
underlyingQueue.async { [weak self] in
|
||||
if self?.inputStream?.hasBytesAvailable == true {
|
||||
self?.dataAvailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dataAvailable() {
|
||||
@@ -92,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
|
||||
|
||||
@@ -11,17 +11,28 @@ struct MP4Atom: Equatable, CustomDebugStringConvertible {
|
||||
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") }
|
||||
@@ -104,11 +115,22 @@ final class Mp4Restructure {
|
||||
partialResult.append(data)
|
||||
}
|
||||
let initialData = accumulatedInitialData + atomData
|
||||
let mdatOffset = mdatAtom.offset - Atoms.atomPreampleSize
|
||||
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)
|
||||
}
|
||||
|
||||
func checkIsOptimized(data: Data) throws -> (optimized: Bool, offset: Int?)? {
|
||||
/// 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)
|
||||
@@ -123,7 +145,7 @@ final class Mp4Restructure {
|
||||
// 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)
|
||||
atomSize = Int(try getInteger(data: data, offset: atomOffset + 8) as UInt64)
|
||||
}
|
||||
let mdat = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
|
||||
atoms.append(mdat)
|
||||
@@ -139,11 +161,11 @@ final class Mp4Restructure {
|
||||
if ftyp != nil {
|
||||
if foundMoov && !foundMdat {
|
||||
Logger.debug("🕵️ detected an optimized mp4", category: .generic)
|
||||
return (true, nil)
|
||||
return nil
|
||||
} else if !foundMoov && foundMdat {
|
||||
Logger.debug("🕵️ detected an non-optimized mp4", category: .generic)
|
||||
let possibleMoovOffset = Int(atomOffset) + atomSize
|
||||
return (false, possibleMoovOffset)
|
||||
return Mp4OptimizeInfo(moovOffset: possibleMoovOffset, moovSize: atomSize)
|
||||
}
|
||||
}
|
||||
atomOffset += atomSize
|
||||
|
||||
@@ -77,33 +77,31 @@ final class RemoteMp4Restructure {
|
||||
do {
|
||||
let value = try 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))
|
||||
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))
|
||||
|
||||
@@ -749,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)
|
||||
|
||||
@@ -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,6 +228,7 @@ final class AudioFileStreamProcessor {
|
||||
case kAudioFileStreamProperty_ReadyToProducePackets:
|
||||
// check converter for discontinuous stream
|
||||
processReadyToProducePackets(fileStream: fileStream)
|
||||
processPacketUpperBoundAndMaxPacketSize(fileStream: fileStream)
|
||||
case kAudioFileStreamProperty_FormatList:
|
||||
processFormatList(fileStream: fileStream)
|
||||
default:
|
||||
@@ -305,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
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user