From e06155bcad239f4bb8b25f075e3c54d4a168b942 Mon Sep 17 00:00:00 2001 From: Dennis Bartels Date: Thu, 29 Oct 2026 17:33:14 +0100 Subject: [PATCH] fix(Mp4): locate moov from the mdat header instead of buffering the whole mdat A non-optimized MP4 was only recognized once the complete mdat atom had been downloaded, so a 128 MB hi-res ALAC file buffered for many seconds before playback could start. The mdat header already holds its size, which is enough to request the trailing moov atom. --- .../Audio Source/Mp4/Mp4Restructure.swift | 8 ++- .../Streaming/Mp4RestructureTests.swift | 62 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 AudioStreamingTests/Streaming/Mp4RestructureTests.swift diff --git a/AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift b/AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift index 0b7df80..eaab9f2 100644 --- a/AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift +++ b/AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift @@ -211,10 +211,16 @@ final class Mp4Restructure { foundMoov = true case Atoms.mdat: foundMdat = true + // The mdat header carries its size, so the moov offset is known without buffering the whole mdat. + if ftyp != nil, !foundMoov { + atoms.append(MP4Atom(type: atomType, size: atomSize, offset: atomOffset)) + Logger.debug("🕵️ detected a non-optimized mp4 (header-only observation)", category: .generic) + return .needsRestructure(moovOffset: atomOffset + atomSize) + } default: break } - + if ftyp != nil { if foundMoov && !foundMdat { Logger.debug("🕵️ detected an optimized mp4 (header-only observation)", category: .generic) diff --git a/AudioStreamingTests/Streaming/Mp4RestructureTests.swift b/AudioStreamingTests/Streaming/Mp4RestructureTests.swift new file mode 100644 index 0000000..d3a5e16 --- /dev/null +++ b/AudioStreamingTests/Streaming/Mp4RestructureTests.swift @@ -0,0 +1,62 @@ +import XCTest +@testable import AudioStreaming + +final class Mp4RestructureTests: XCTestCase { + private func atom(_ type: String, payload: Data) -> Data { + var data = Data() + data.append(contentsOf: withUnsafeBytes(of: UInt32(8 + payload.count).bigEndian, Array.init)) + data.append(type.data(using: .ascii)!) + data.append(payload) + return data + } + + private func uint32(_ value: UInt32) -> Data { + Data(withUnsafeBytes(of: value.bigEndian, Array.init)) + } + + private var ftyp: Data { atom("ftyp", payload: Data("M4A \0\0\u{2}\0M4A isom".utf8)) } + + private func moov(chunkOffsets: [UInt32]) -> Data { + var stco = Data([0, 0, 0, 0]) + uint32(UInt32(chunkOffsets.count)) + chunkOffsets.forEach { stco.append(uint32($0)) } + return atom("moov", payload: atom("trak", payload: atom("stco", payload: stco))) + } + + func testNonOptimizedIsDetectedFromMdatHeaderAlone() throws { + let mdatSize = 128_486_896 + var head = ftyp + atom("free", payload: Data()) + let mdatOffset = head.count + head.append(uint32(UInt32(mdatSize))) + head.append("mdat".data(using: .ascii)!) + head.append(Data(repeating: 0, count: 4096)) + + let result = try Mp4Restructure().checkIsOptimized(data: head) + + XCTAssertEqual(result, .needsRestructure(moovOffset: mdatOffset + mdatSize)) + } + + func testOptimizedIsDetectedFromMoovHeaderAlone() throws { + var head = ftyp + head.append(uint32(900_000)) + head.append("moov".data(using: .ascii)!) + + XCTAssertEqual(try Mp4Restructure().checkIsOptimized(data: head), .optimized) + } + + func testRestructureAfterHeaderOnlyDetectionMovesMoovBeforeMdat() throws { + let ftyp = self.ftyp + let mdat = atom("mdat", payload: Data(repeating: 0xAB, count: 64)) + let chunkOffset = UInt32(ftyp.count + 8) + let moov = moov(chunkOffsets: [chunkOffset]) + let restructure = Mp4Restructure() + + let check = try restructure.checkIsOptimized(data: ftyp + mdat.prefix(16)) + XCTAssertEqual(check, .needsRestructure(moovOffset: ftyp.count + mdat.count)) + + let (initialData, mdatOffset) = try restructure.restructureMoov(data: moov) + + XCTAssertEqual(mdatOffset, ftyp.count) + XCTAssertEqual(initialData, ftyp + self.moov(chunkOffsets: [chunkOffset + UInt32(moov.count)])) + XCTAssertEqual(restructure.seekAdjusted(offset: 1000), 1000 - moov.count) + } +}