Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion AudioStreaming/Streaming/Audio Source/FileAudioSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
private var inputStream: InputStream?

private var mp4Restructure: Mp4Restructure
private var mp4ProbeBuffer: Data = Data()

init(url: URL,
fileManager: FileManager = .default,
Expand Down Expand Up @@ -120,14 +121,17 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
if isMp4, !mp4IsAlreadyOptimized {
if !mp4Restructure.dataOptimized {
do {
switch try mp4Restructure.checkIsOptimized(data: data) {
mp4ProbeBuffer.append(data)
switch try mp4Restructure.checkIsOptimized(data: mp4ProbeBuffer) {
case .undetermined:
// Not enough bytes yet; wait for more data before deciding
break
case .optimized:
mp4IsAlreadyOptimized = true
mp4ProbeBuffer = Data()
delegate?.dataAvailable(source: self, data: data)
case let .needsRestructure(moovOffset):
mp4ProbeBuffer = Data()
try performMp4Restructure(inputStream: inputStream, moovOffset: moovOffset)
}
} catch {
Expand Down
73 changes: 70 additions & 3 deletions AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,84 @@ final class Mp4Restructure {

// Handle extended size (64-bit)
if atomSize == 1 {
if atomOffset + 16 > data.count { break }
if atomOffset + 16 > data.count {
// Mark presence from header only to allow early decisions
switch atomType {
case Atoms.ftyp:
if ftyp == nil {
ftyp = MP4Atom(type: atomType, size: Int.max, offset: atomOffset, data: nil)
}
case Atoms.mdat:
foundMdat = true
case Atoms.moov:
foundMoov = true
default:
break
}
if ftyp != nil, foundMoov, !foundMdat {
Logger.debug("🕵️ detected an optimized mp4", category: .generic)
return .optimized
}
// For non-optimized case we need a reliable moov offset; wait for more data
break
}
let ext: UInt64 = try getInteger(data: data, offset: atomOffset + 8)
atomSize = Int(ext)
headerSize = 16
} else if atomSize == 0 {
// Size extends to EOF; with partial data we can't determine full box
// Size extends to EOF; still record what we saw and decide if possible
switch atomType {
case Atoms.ftyp:
if ftyp == nil {
// We only have header; store minimal info
let start = atomOffset
let end = min(data.count, atomOffset + headerSize)
let ftypData = data[start ..< end]
let ftyp = MP4Atom(type: atomType, size: 0, offset: atomOffset, data: ftypData)
self.ftyp = ftyp
}
case Atoms.mdat:
foundMdat = true
case Atoms.moov:
foundMoov = true
default:
break
}
if ftyp != nil, foundMoov, !foundMdat {
Logger.debug("🕵️ detected an optimized mp4", category: .generic)
return .optimized
}
// Otherwise we can't reliably compute moov offset yet
break
}

// Bounds and sanity checks
if atomSize < headerSize || atomOffset + atomSize > data.count { break }
if atomSize < headerSize {
break
}
if atomOffset + atomSize > data.count {
// We have the header but not the full atom yet. Record presence for decision.
switch atomType {
case Atoms.ftyp:
if ftyp == nil {
ftyp = MP4Atom(type: atomType, size: atomSize, offset: atomOffset, data: nil)
}
case Atoms.moov:
foundMoov = true
case Atoms.mdat:
foundMdat = true
default:
break
}

if ftyp != nil {
if foundMoov && !foundMdat {
Logger.debug("🕵️ detected an optimized mp4 (header-only observation)", category: .generic)
return .optimized
}
}
break
}

switch atomType {
case Atoms.ftyp:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ final class RemoteMp4Restructure {

private let mp4Restructure: Mp4Restructure

private var ignoreFailureDueToCancel: Bool = false

init(url: URL, networking: NetworkingClient, restructure: Mp4Restructure = Mp4Restructure()) {
self.url = url
self.networking = networking
Expand Down Expand Up @@ -81,6 +83,7 @@ final class RemoteMp4Restructure {
break // keep streaming until decision can be made
case .optimized:
self.audioData = Data()
self.ignoreFailureDueToCancel = true
self.task?.cancel()
self.task = nil
completion(.success(nil))
Expand All @@ -92,6 +95,7 @@ final class RemoteMp4Restructure {
}
// stop request, fetch moov and restructure
self.audioData = Data()
self.ignoreFailureDueToCancel = true
self.task?.cancel()
self.task = nil
self.fetchAndRestructureMoovAtom(offset: moovOffset) { result in
Expand All @@ -108,6 +112,15 @@ final class RemoteMp4Restructure {
completion(.failure(Mp4RestructureError.invalidAtomSize))
}
case let .stream(.failure(error)):
// Ignore the error if it was caused by our intentional cancel
if ignoreFailureDueToCancel {
ignoreFailureDueToCancel = false
break
}
let nsError = error as NSError
if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled {
break
}
completion(.failure(Mp4RestructureError.networkError(error)))
case .complete:
break
Expand Down