Skip to content

Commit 633018d

Browse files
authored
Fixes an issue with m4a incorrectly pass as non-optimized (#129)
1 parent eb2df02 commit 633018d

3 files changed

Lines changed: 88 additions & 4 deletions

File tree

AudioStreaming/Streaming/Audio Source/FileAudioSource.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
3434
private var inputStream: InputStream?
3535

3636
private var mp4Restructure: Mp4Restructure
37+
private var mp4ProbeBuffer: Data = Data()
3738

3839
init(url: URL,
3940
fileManager: FileManager = .default,
@@ -120,14 +121,17 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
120121
if isMp4, !mp4IsAlreadyOptimized {
121122
if !mp4Restructure.dataOptimized {
122123
do {
123-
switch try mp4Restructure.checkIsOptimized(data: data) {
124+
mp4ProbeBuffer.append(data)
125+
switch try mp4Restructure.checkIsOptimized(data: mp4ProbeBuffer) {
124126
case .undetermined:
125127
// Not enough bytes yet; wait for more data before deciding
126128
break
127129
case .optimized:
128130
mp4IsAlreadyOptimized = true
131+
mp4ProbeBuffer = Data()
129132
delegate?.dataAvailable(source: self, data: data)
130133
case let .needsRestructure(moovOffset):
134+
mp4ProbeBuffer = Data()
131135
try performMp4Restructure(inputStream: inputStream, moovOffset: moovOffset)
132136
}
133137
} catch {

AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,84 @@ final class Mp4Restructure {
145145

146146
// Handle extended size (64-bit)
147147
if atomSize == 1 {
148-
if atomOffset + 16 > data.count { break }
148+
if atomOffset + 16 > data.count {
149+
// Mark presence from header only to allow early decisions
150+
switch atomType {
151+
case Atoms.ftyp:
152+
if ftyp == nil {
153+
ftyp = MP4Atom(type: atomType, size: Int.max, offset: atomOffset, data: nil)
154+
}
155+
case Atoms.mdat:
156+
foundMdat = true
157+
case Atoms.moov:
158+
foundMoov = true
159+
default:
160+
break
161+
}
162+
if ftyp != nil, foundMoov, !foundMdat {
163+
Logger.debug("🕵️ detected an optimized mp4", category: .generic)
164+
return .optimized
165+
}
166+
// For non-optimized case we need a reliable moov offset; wait for more data
167+
break
168+
}
149169
let ext: UInt64 = try getInteger(data: data, offset: atomOffset + 8)
150170
atomSize = Int(ext)
151171
headerSize = 16
152172
} else if atomSize == 0 {
153-
// Size extends to EOF; with partial data we can't determine full box
173+
// Size extends to EOF; still record what we saw and decide if possible
174+
switch atomType {
175+
case Atoms.ftyp:
176+
if ftyp == nil {
177+
// We only have header; store minimal info
178+
let start = atomOffset
179+
let end = min(data.count, atomOffset + headerSize)
180+
let ftypData = data[start ..< end]
181+
let ftyp = MP4Atom(type: atomType, size: 0, offset: atomOffset, data: ftypData)
182+
self.ftyp = ftyp
183+
}
184+
case Atoms.mdat:
185+
foundMdat = true
186+
case Atoms.moov:
187+
foundMoov = true
188+
default:
189+
break
190+
}
191+
if ftyp != nil, foundMoov, !foundMdat {
192+
Logger.debug("🕵️ detected an optimized mp4", category: .generic)
193+
return .optimized
194+
}
195+
// Otherwise we can't reliably compute moov offset yet
154196
break
155197
}
156198

157199
// Bounds and sanity checks
158-
if atomSize < headerSize || atomOffset + atomSize > data.count { break }
200+
if atomSize < headerSize {
201+
break
202+
}
203+
if atomOffset + atomSize > data.count {
204+
// We have the header but not the full atom yet. Record presence for decision.
205+
switch atomType {
206+
case Atoms.ftyp:
207+
if ftyp == nil {
208+
ftyp = MP4Atom(type: atomType, size: atomSize, offset: atomOffset, data: nil)
209+
}
210+
case Atoms.moov:
211+
foundMoov = true
212+
case Atoms.mdat:
213+
foundMdat = true
214+
default:
215+
break
216+
}
217+
218+
if ftyp != nil {
219+
if foundMoov && !foundMdat {
220+
Logger.debug("🕵️ detected an optimized mp4 (header-only observation)", category: .generic)
221+
return .optimized
222+
}
223+
}
224+
break
225+
}
159226

160227
switch atomType {
161228
case Atoms.ftyp:

AudioStreaming/Streaming/Audio Source/Mp4/RemoteMp4Restructure.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ final class RemoteMp4Restructure {
3131

3232
private let mp4Restructure: Mp4Restructure
3333

34+
private var ignoreFailureDueToCancel: Bool = false
35+
3436
init(url: URL, networking: NetworkingClient, restructure: Mp4Restructure = Mp4Restructure()) {
3537
self.url = url
3638
self.networking = networking
@@ -81,6 +83,7 @@ final class RemoteMp4Restructure {
8183
break // keep streaming until decision can be made
8284
case .optimized:
8385
self.audioData = Data()
86+
self.ignoreFailureDueToCancel = true
8487
self.task?.cancel()
8588
self.task = nil
8689
completion(.success(nil))
@@ -92,6 +95,7 @@ final class RemoteMp4Restructure {
9295
}
9396
// stop request, fetch moov and restructure
9497
self.audioData = Data()
98+
self.ignoreFailureDueToCancel = true
9599
self.task?.cancel()
96100
self.task = nil
97101
self.fetchAndRestructureMoovAtom(offset: moovOffset) { result in
@@ -108,6 +112,15 @@ final class RemoteMp4Restructure {
108112
completion(.failure(Mp4RestructureError.invalidAtomSize))
109113
}
110114
case let .stream(.failure(error)):
115+
// Ignore the error if it was caused by our intentional cancel
116+
if ignoreFailureDueToCancel {
117+
ignoreFailureDueToCancel = false
118+
break
119+
}
120+
let nsError = error as NSError
121+
if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled {
122+
break
123+
}
111124
completion(.failure(Mp4RestructureError.networkError(error)))
112125
case .complete:
113126
break

0 commit comments

Comments
 (0)