diff --git a/AudioStreaming/Streaming/Audio Source/FileAudioSource.swift b/AudioStreaming/Streaming/Audio Source/FileAudioSource.swift index d15551c..4a356b3 100644 --- a/AudioStreaming/Streaming/Audio Source/FileAudioSource.swift +++ b/AudioStreaming/Streaming/Audio Source/FileAudioSource.swift @@ -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, @@ -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 { diff --git a/AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift b/AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift index c41395c..0b7df80 100644 --- a/AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift +++ b/AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift @@ -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: diff --git a/AudioStreaming/Streaming/Audio Source/Mp4/RemoteMp4Restructure.swift b/AudioStreaming/Streaming/Audio Source/Mp4/RemoteMp4Restructure.swift index 25f1de8..e830c36 100644 --- a/AudioStreaming/Streaming/Audio Source/Mp4/RemoteMp4Restructure.swift +++ b/AudioStreaming/Streaming/Audio Source/Mp4/RemoteMp4Restructure.swift @@ -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 @@ -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)) @@ -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 @@ -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