From 5c5b5da9b4a5d42bbd5062cdcc49a6a745e73191 Mon Sep 17 00:00:00 2001 From: Truls Aagedal <5827165+aagedal@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:55:43 +0100 Subject: [PATCH] Detect ProRes 4444 alpha from sample-entry depth (3.1.1) ap4h/ap4x streams were always reported as yuva444p12le, but alpha is optional in ProRes 4444 and most masters are opaque. Use the VisualSampleEntry depth (32 = alpha, 24 = opaque) and, when that is inconclusive, the first frame header's alpha_channel_type, matching ffprobe. Also set hasAlphaChannel, keep the depth out of bitDepth for ProRes, and apply FourCC defaults to sample entries with no children. Prepare the 3.1.1 patch release (version, README, changelog, checklist). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 17 +++ README.md | 11 +- RELEASE_CHECKLIST.md | 12 +- Sources/CLI/SwiftMediaMetadata.swift | 2 +- .../SwiftMediaMetadata/API/VideoStream.swift | 6 +- .../SwiftMediaMetadata/Video/MP4Parser.swift | 1 + .../Video/MP4TrackParser.swift | 6 + .../Video/MP4VisualSampleEntry.swift | 111 +++++++++++--- .../Video/MP4ParserTests.swift | 141 ++++++++++++++++++ 9 files changed, 274 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d039ec..d2f9751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ Version numbers follow [Semantic Versioning](https://semver.org/) and track the CLI; the library target follows the same numbering. +## [3.1.1] — 2026-10-31 + +### Fixed + +- ProRes 4444 / 4444 XQ (`ap4h` / `ap4x`) in MOV/MP4 no longer always report + `pixelFormat = "yuva444p12le"`. Alpha is optional in 4444, and most camera + and grading masters are opaque. The VisualSampleEntry depth now decides it: + 32 means alpha (`yuva444p12le`), 24 means opaque (`yuv444p12le`). For any + other depth the first frame header's `alpha_channel_type` decides, as in + ffprobe; this is a 26-byte read from the mapped file. Without either signal + the stream is reported opaque. `hasAlphaChannel` is now set for these + streams as well. +- The sample-entry depth is no longer read as `bitDepth` for ProRes, so + 4444 stays at 12 bits. +- FourCC-derived profile, chroma and bit-depth defaults (ProRes, ProRes RAW, + APV) now apply to sample entries that have no child boxes. + ## [3.1.0] — 2026-10-30 ### Added diff --git a/README.md b/README.md index 2da6df5..9d52c2c 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,11 @@ swift-exif --version ``` Or download the archive and checksum directly from the -[3.1.0 release](https://github.com/aagedal/SwiftMediaMetadata/releases/tag/3.1.0): +[3.1.1 release](https://github.com/aagedal/SwiftMediaMetadata/releases/tag/3.1.1): ```sh -curl -LO https://github.com/aagedal/SwiftMediaMetadata/releases/download/3.1.0/swift-exif-macos-arm64.tar.gz -curl -LO https://github.com/aagedal/SwiftMediaMetadata/releases/download/3.1.0/swift-exif-macos-arm64.tar.gz.sha256 +curl -LO https://github.com/aagedal/SwiftMediaMetadata/releases/download/3.1.1/swift-exif-macos-arm64.tar.gz +curl -LO https://github.com/aagedal/SwiftMediaMetadata/releases/download/3.1.1/swift-exif-macos-arm64.tar.gz.sha256 shasum -a 256 -c swift-exif-macos-arm64.tar.gz.sha256 tar -xzf swift-exif-macos-arm64.tar.gz ./swift-exif-macos-arm64/swift-exif --version @@ -83,7 +83,7 @@ Add the package to your `Package.swift` with: ```swift dependencies: [ - .package(url: "https://github.com/aagedal/SwiftMediaMetadata.git", from: "3.1.0"), + .package(url: "https://github.com/aagedal/SwiftMediaMetadata.git", from: "3.1.1"), ] ``` @@ -475,7 +475,8 @@ plus convenience accessors that mirror the first video stream at the top level | `bitDepth` | 8 / 10 / 12 from `hvcC` / `av1C` / CDCI / Matroska `BitsPerChannel` or `CodecPrivate` | | `chromaSubsampling` | `"4:2:0"`, `"4:2:2"`, `"4:4:4"`, `"4:0:0"`, `"4:1:1"` | | `chromaLocation` | `"left"`, `"center"`, `"topleft"`, `"top"`, `"bottomleft"`, `"bottom"` — matches ffprobe `chroma_location` | -| `pixelFormat` | ffprobe-style `pix_fmt` string — `"yuv420p"`, `"yuv420p10le"`, `"yuvj420p"`, `"yuv444p12le"`, `"gray10le"`, derived from codec + chroma + depth + range | +| `pixelFormat` | ffprobe-style `pix_fmt` string — `"yuv420p"`, `"yuv420p10le"`, `"yuvj420p"`, `"yuv444p12le"`, `"gray10le"`, derived from codec + chroma + depth + range. ProRes 4444 / 4444 XQ report `"yuva444p12le"` only when the stream carries alpha (sample-entry depth 32, or a non-zero frame-header `alpha_channel_type`), otherwise `"yuv444p12le"` | +| `hasAlphaChannel` | `true` when alpha is signalled (HEVC `alpha_channel_info` SEI, ProRes 4444 with alpha); `false` for ProRes 4444 known to be opaque; `nil` when nothing signals it | | `frameRate` | fps — from `stsz`/`stts` for ISOBMFF, Matroska `DefaultDuration`, MXF `SampleRate`, AVI `dwRate`/`dwScale`, MPEG-2 sequence header | | `avgFrameRate` / `rFrameRate` | ffprobe-compatible pair — `avg_frame_rate` (average) and `r_frame_rate` (raw cadence) | | `duration` | Per-track duration in seconds | diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index 46504ac..c80a575 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -1,10 +1,10 @@ -# SwiftMediaMetadata 3.1.0 release checklist +# SwiftMediaMetadata 3.1.1 release checklist -- [x] Matroska `Attachments` exposed as `VideoMetadata.attachments`, skipping - incomplete entries so indices match ffmpeg's `t:` attachment streams. -- [x] Verified against an ffmpeg-muxed MKV with three font attachments (names, - MIME types and payload sizes match ffmpeg). -- [x] Set CLI version, README and changelog to 3.1.0. +- [x] ProRes 4444 / 4444 XQ alpha follows the sample-entry depth (32 alpha, + 24 opaque), falling back to the first frame header's alpha_channel_type. +- [x] Verified against ffmpeg-encoded `ap4h` / `ap4x` MOVs with and without + alpha, including copies with the depth zeroed: `pixelFormat` matches ffprobe. +- [x] Set CLI version, README and changelog to 3.1.1. - [x] Run complete release preflight on the final release tree. - [ ] Merge the reviewed release changes into `main` before tagging. The release workflow rejects tags whose commits are not in `main`'s history. diff --git a/Sources/CLI/SwiftMediaMetadata.swift b/Sources/CLI/SwiftMediaMetadata.swift index d273bdc..3c7d2fc 100644 --- a/Sources/CLI/SwiftMediaMetadata.swift +++ b/Sources/CLI/SwiftMediaMetadata.swift @@ -6,7 +6,7 @@ struct SwiftMediaMetadataCLI: ParsableCommand { static let configuration = CommandConfiguration( commandName: "swift-exif", abstract: "Read, write, and manipulate media metadata.", - version: "3.1.0", + version: "3.1.1", subcommands: [ ReadCommand.self, WriteCommand.self, diff --git a/Sources/SwiftMediaMetadata/API/VideoStream.swift b/Sources/SwiftMediaMetadata/API/VideoStream.swift index 5a23205..5c91885 100644 --- a/Sources/SwiftMediaMetadata/API/VideoStream.swift +++ b/Sources/SwiftMediaMetadata/API/VideoStream.swift @@ -180,8 +180,10 @@ public struct VideoStream: Sendable, Equatable { /// captions (detected from H.264/HEVC SEI A/53 user-data wrappers, or /// from MP4 `\u{a9}cca` / Matroska CodecPrivate hints). public var hasClosedCaptions: Bool? - /// True when the stream signals an alpha (transparency) channel via the - /// HEVC `alpha_channel_info` SEI message. + /// True when the stream signals an alpha (transparency) channel: the + /// HEVC `alpha_channel_info` SEI message, or ProRes 4444 / 4444 XQ with a + /// 32-bit sample-entry depth or a non-zero frame-header alpha_channel_type. + /// False for ProRes 4444 known to be opaque; nil when nothing signals it. public var hasAlphaChannel: Bool? public init(index: Int) { diff --git a/Sources/SwiftMediaMetadata/Video/MP4Parser.swift b/Sources/SwiftMediaMetadata/Video/MP4Parser.swift index 61d6d91..f359683 100644 --- a/Sources/SwiftMediaMetadata/Video/MP4Parser.swift +++ b/Sources/SwiftMediaMetadata/Video/MP4Parser.swift @@ -68,6 +68,7 @@ public struct MP4Parser: Sendable { trak.data, movieTimescale: movieTimescale, chapterTrackIDs: chapterReferencedTrackIDs, + fullData: data, into: &metadata ) } diff --git a/Sources/SwiftMediaMetadata/Video/MP4TrackParser.swift b/Sources/SwiftMediaMetadata/Video/MP4TrackParser.swift index ed4e06a..861a548 100644 --- a/Sources/SwiftMediaMetadata/Video/MP4TrackParser.swift +++ b/Sources/SwiftMediaMetadata/Video/MP4TrackParser.swift @@ -17,6 +17,7 @@ extension MP4Parser { _ data: Data, movieTimescale: UInt32, chapterTrackIDs: Set = [], + fullData: Data? = nil, into metadata: inout VideoMetadata ) { guard let children = try? ISOBMFFBoxReader.parseBoxes(from: data) else { return } @@ -174,6 +175,11 @@ extension MP4Parser { stream.isDefault = tkhdIsDefault stream.rotation = tkhdRotation parseVisualSampleEntry(stsdBox.data, into: &stream) + if let fullData { + applyProRes4444FrameHeaderAlpha( + stblChildren: stblChildren, fullData: fullData, into: &stream + ) + } // Blackmagic RAW: pull the three uint32 codec-config atoms // (bfdn / ctrn / bver) out of the BRAW sample entry's child // boxes and surface them next to the moov.meta clip slate. diff --git a/Sources/SwiftMediaMetadata/Video/MP4VisualSampleEntry.swift b/Sources/SwiftMediaMetadata/Video/MP4VisualSampleEntry.swift index eb3ea25..08e130e 100644 --- a/Sources/SwiftMediaMetadata/Video/MP4VisualSampleEntry.swift +++ b/Sources/SwiftMediaMetadata/Video/MP4VisualSampleEntry.swift @@ -59,10 +59,15 @@ extension MP4Parser { // Most codecs advertise 0x18 (24 = packed RGB 8-bit) here; higher-bit-depth // values are rare and unreliable — the codec-specific config box (hvcC, // av1C, prores atom …) is the real source of truth, parsed below. + // ProRes uses the field as a pixel depth (24 = opaque, 32 = alpha), not + // bits per component, so it never feeds bitDepth there. + var sampleEntryDepth: Int? if stsdData.count >= payloadBase + 76 { if (try? reader.seek(to: payloadBase + 74)) != nil, let depth = try? reader.readUInt16BigEndian() { - if depth > 0, depth != 0x18, depth % 3 == 0, depth <= 96 { + sampleEntryDepth = Int(depth) + if !isProResFourCC(codec), + depth > 0, depth != 0x18, depth % 3 == 0, depth <= 96 { stream.bitDepth = Int(depth / 3) } } @@ -72,21 +77,40 @@ extension MP4Parser { // start 8 bytes after the fixed VisualSampleEntry header at payloadBase + 78. let entryEnd = entryStart + Int(entrySize32) let childrenStart = payloadBase + 78 - guard childrenStart < entryEnd, entryEnd <= stsdData.count else { return } - - let childrenData = Data( - stsdData[(stsdData.startIndex + childrenStart) ..< (stsdData.startIndex + entryEnd)] - ) - if let kids = try? ISOBMFFBoxReader.parseBoxes(from: childrenData) { - for kid in kids { - applyVisualChildBox(kid, into: &stream) + if childrenStart < entryEnd, entryEnd <= stsdData.count { + let childrenData = Data( + stsdData[(stsdData.startIndex + childrenStart) ..< (stsdData.startIndex + entryEnd)] + ) + if let kids = try? ISOBMFFBoxReader.parseBoxes(from: childrenData) { + for kid in kids { + applyVisualChildBox(kid, into: &stream) + } } } // Sample entries whose FourCC uniquely identifies the codec variant // (ProRes, APV, ProRes RAW) can skip frame-header parsing entirely — // fill in profile/chroma/bit_depth from the FourCC. - applyFourCCDefaults(codec, into: &stream) + applyFourCCDefaults(codec, sampleEntryDepth: sampleEntryDepth, into: &stream) + } + + static func isProResFourCC(_ codec: String) -> Bool { + switch codec { + case "apco", "apcs", "apcn", "apch", "ap4h", "ap4x": return true + default: return false + } + } + + /// ProRes 4444 / 4444 XQ: alpha is optional. QuickTime writers signal it + /// through the VisualSampleEntry depth (32 = alpha, 24 = opaque). Any + /// other value is inconclusive — `MP4Parser.applyProRes4444FrameHeaderAlpha` + /// then checks the first frame header, the field ffprobe decides from. + static func proRes4444HasAlpha(sampleEntryDepth depth: Int?) -> Bool? { + switch depth { + case 32: return true + case 24: return false + default: return nil + } } static func applyVisualChildBox(_ box: ISOBMFFBox, into stream: inout VideoStream) { @@ -200,12 +224,60 @@ extension MP4Parser { } } + /// Fallback for ProRes 4444 / 4444 XQ tracks whose sample-entry depth + /// didn't settle alpha: read the first frame header (26 bytes at the first + /// chunk offset — the file is memory-mapped, so this touches one page). + /// Frame layout (SMPTE RDD 36): frame_size(4) + 'icpf'(4), then the frame + /// header — header_size(2) … alpha_channel_type in the low nibble of + /// header byte 17. Non-zero means alpha, as in ffmpeg's decoder. + static func applyProRes4444FrameHeaderAlpha( + stblChildren: [ISOBMFFBox], + fullData: Data, + into stream: inout VideoStream + ) { + guard stream.codec == "ap4h" || stream.codec == "ap4x", + stream.hasAlphaChannel == nil else { return } + let chunkOffset: UInt64? = { + if let co64 = stblChildren.first(where: { $0.type == "co64" }) { + return parseCO64First(co64.data) + } + if let stco = stblChildren.first(where: { $0.type == "stco" }) { + return parseSTCOFirst(stco.data).map(UInt64.init) + } + return nil + }() + guard let offset = chunkOffset, + let hasAlpha = proResFrameHeaderHasAlpha(at: offset, in: fullData) else { return } + stream.hasAlphaChannel = hasAlpha + // Only replace the opaque default applyFourCCDefaults chose. + if hasAlpha, stream.pixelFormat == "yuv444p12le" { + stream.pixelFormat = "yuva444p12le" + } + } + + static func proResFrameHeaderHasAlpha(at offset: UInt64, in fullData: Data) -> Bool? { + // Bound in UInt64 space first: a crafted co64 value past Int.max + // would trap in `Int(offset)`. + guard offset <= UInt64(Int.max) else { return nil } + let s = Int(offset) + guard s <= fullData.count - 26 else { return nil } + let b = fullData.startIndex + s + guard fullData[(b + 4) ..< (b + 8)].elementsEqual("icpf".utf8) else { return nil } + let headerSize = (Int(fullData[b + 8]) << 8) | Int(fullData[b + 9]) + guard headerSize >= 18 else { return nil } + return fullData[b + 8 + 17] & 0x0F != 0 + } + /// Deterministic pix_fmt / profile / bit_depth for codecs whose sample /// entry FourCC uniquely identifies the subcodec — notably ProRes, APV /// and ProRes RAW. ffprobe reads the same fields from the frame header, /// but every FourCC here maps 1:1 so surfacing them from the container /// side matches ffprobe's output without any bitstream parsing. - static func applyFourCCDefaults(_ codec: String, into stream: inout VideoStream) { + static func applyFourCCDefaults( + _ codec: String, + sampleEntryDepth: Int? = nil, + into stream: inout VideoStream + ) { switch codec { // Apple ProRes (SMPTE RP 2019) case "apco": // 422 Proxy @@ -224,16 +296,17 @@ extension MP4Parser { stream.profile = stream.profile ?? "HQ" stream.chromaSubsampling = stream.chromaSubsampling ?? "4:2:2" stream.bitDepth = stream.bitDepth ?? 10 - case "ap4h": // 4444 — always carries alpha, 12-bit YUV - stream.profile = stream.profile ?? "4444" - stream.chromaSubsampling = stream.chromaSubsampling ?? "4:4:4" - stream.bitDepth = stream.bitDepth ?? 12 - stream.pixelFormat = stream.pixelFormat ?? "yuva444p12le" - case "ap4x": // 4444 XQ - stream.profile = stream.profile ?? "4444XQ" + case "ap4h", "ap4x": // 4444 / 4444 XQ — 12-bit YUV, alpha optional + stream.profile = stream.profile ?? (codec == "ap4h" ? "4444" : "4444XQ") stream.chromaSubsampling = stream.chromaSubsampling ?? "4:4:4" stream.bitDepth = stream.bitDepth ?? 12 - stream.pixelFormat = stream.pixelFormat ?? "yuva444p12le" + // Most camera and grading masters are opaque, so an inconclusive + // depth falls back to the opaque format (as ffprobe does when the + // frame header's alpha_channel_type is 0). + let hasAlpha = proRes4444HasAlpha(sampleEntryDepth: sampleEntryDepth) + stream.hasAlphaChannel = stream.hasAlphaChannel ?? hasAlpha + stream.pixelFormat = stream.pixelFormat + ?? (hasAlpha == true ? "yuva444p12le" : "yuv444p12le") // Apple ProRes RAW case "aprn": stream.profile = stream.profile ?? "RAW" diff --git a/Tests/SwiftMediaMetadataTests/Video/MP4ParserTests.swift b/Tests/SwiftMediaMetadataTests/Video/MP4ParserTests.swift index 0e0bd70..a52cf7f 100644 --- a/Tests/SwiftMediaMetadataTests/Video/MP4ParserTests.swift +++ b/Tests/SwiftMediaMetadataTests/Video/MP4ParserTests.swift @@ -744,6 +744,147 @@ final class MP4ParserTests: XCTestCase { return writer.data } + // MARK: - ProRes 4444 alpha + + /// Opaque 4444 masters carry depth 24 and must not be reported with an + /// alpha plane (ffprobe: yuv444p12le). + func testProRes4444Depth24IsOpaque() throws { + let metadata = try VideoMetadata.read(from: buildProResMOV(codec: "ap4h", depth: 24)) + let stream = try XCTUnwrap(metadata.videoStreams.first) + XCTAssertEqual(stream.profile, "4444") + XCTAssertEqual(stream.pixelFormat, "yuv444p12le") + XCTAssertEqual(stream.hasAlphaChannel, false) + XCTAssertEqual(stream.bitDepth, 12) + XCTAssertEqual(stream.chromaSubsampling, "4:4:4") + } + + /// Depth 32 signals alpha; it must not leak into bitDepth either. + func testProRes4444Depth32HasAlpha() throws { + let metadata = try VideoMetadata.read(from: buildProResMOV(codec: "ap4h", depth: 32)) + let stream = try XCTUnwrap(metadata.videoStreams.first) + XCTAssertEqual(stream.pixelFormat, "yuva444p12le") + XCTAssertEqual(stream.hasAlphaChannel, true) + XCTAssertEqual(stream.bitDepth, 12) + } + + func testProRes4444XQDepth24And32() throws { + let opaque = try XCTUnwrap( + VideoMetadata.read(from: buildProResMOV(codec: "ap4x", depth: 24)).videoStreams.first + ) + XCTAssertEqual(opaque.profile, "4444XQ") + XCTAssertEqual(opaque.pixelFormat, "yuv444p12le") + XCTAssertEqual(opaque.bitDepth, 12) + let alpha = try XCTUnwrap( + VideoMetadata.read(from: buildProResMOV(codec: "ap4x", depth: 32)).videoStreams.first + ) + XCTAssertEqual(alpha.pixelFormat, "yuva444p12le") + XCTAssertEqual(alpha.bitDepth, 12) + } + + /// Depth values divisible by 3 must not be read as per-component bit + /// depth for ProRes (48 / 3 would otherwise claim 16-bit). + func testProRes4444UnusualDepthDoesNotOverrideBitDepth() throws { + let stream = try XCTUnwrap( + VideoMetadata.read(from: buildProResMOV(codec: "ap4h", depth: 48)).videoStreams.first + ) + XCTAssertEqual(stream.bitDepth, 12) + XCTAssertEqual(stream.pixelFormat, "yuv444p12le") + XCTAssertNil(stream.hasAlphaChannel) + } + + /// Inconclusive depth: the first frame header's alpha_channel_type decides. + func testProRes4444MissingDepthUsesFrameHeaderAlpha() throws { + let alpha = try XCTUnwrap(VideoMetadata.read( + from: buildProResMOV(codec: "ap4h", depth: 0, frameAlphaType: 1) + ).videoStreams.first) + XCTAssertEqual(alpha.pixelFormat, "yuva444p12le") + XCTAssertEqual(alpha.hasAlphaChannel, true) + + let opaque = try XCTUnwrap(VideoMetadata.read( + from: buildProResMOV(codec: "ap4h", depth: 0, frameAlphaType: 0) + ).videoStreams.first) + XCTAssertEqual(opaque.pixelFormat, "yuv444p12le") + XCTAssertEqual(opaque.hasAlphaChannel, false) + } + + /// Conclusive depth wins over the frame header (no extra read needed). + func testProRes4444DepthTakesPrecedenceOverFrameHeader() throws { + let stream = try XCTUnwrap(VideoMetadata.read( + from: buildProResMOV(codec: "ap4h", depth: 24, frameAlphaType: 1) + ).videoStreams.first) + XCTAssertEqual(stream.pixelFormat, "yuv444p12le") + } + + func testProRes4444MissingDepthAndFrameDefaultsToOpaque() throws { + let stream = try XCTUnwrap( + VideoMetadata.read(from: buildProResMOV(codec: "ap4h", depth: 0)).videoStreams.first + ) + XCTAssertEqual(stream.pixelFormat, "yuv444p12le") + XCTAssertNil(stream.hasAlphaChannel) + } + + /// Build a MOV with one ProRes video track. The VisualSampleEntry depth is + /// written at payload offset 74. When `frameAlphaType` is set, mdat holds + /// a ProRes frame (size + 'icpf' + 20-byte header) referenced by stco. + private func buildProResMOV(codec: String, depth: UInt16, frameAlphaType: UInt8? = nil) -> Data { + var visual = Data(repeating: 0, count: 78) + visual[24] = 0x07; visual[25] = 0x80 // width 1920 + visual[26] = 0x04; visual[27] = 0x38 // height 1080 + visual[74] = UInt8(depth >> 8); visual[75] = UInt8(depth & 0xFF) + let entry = buildBox(codec, data: visual) + var stsdWriter = BinaryWriter(capacity: 16 + entry.count) + stsdWriter.writeBytes([0x00, 0x00, 0x00, 0x00]) + stsdWriter.writeUInt32BigEndian(1) + stsdWriter.writeBytes(entry) + let stsdBox = buildBox("stsd", data: stsdWriter.data) + + // ftyp (16) + mdat header (8) → frame starts at 24. + var mdatBox = Data() + var stcoBox = Data() + if let frameAlphaType { + var header = Data(repeating: 0, count: 20) + header[1] = 20 // frame_header_size + header[17] = frameAlphaType & 0x0F // alpha_channel_type + var frame = BinaryWriter(capacity: 28) + frame.writeUInt32BigEndian(UInt32(8 + header.count)) + frame.writeString("icpf", encoding: .ascii) + frame.writeBytes(header) + mdatBox = buildBox("mdat", data: frame.data) + + var stco = BinaryWriter(capacity: 12) + stco.writeBytes([0x00, 0x00, 0x00, 0x00]) + stco.writeUInt32BigEndian(1) + stco.writeUInt32BigEndian(24) + stcoBox = buildBox("stco", data: stco.data) + } + + var hdlrWriter = BinaryWriter(capacity: 32) + hdlrWriter.writeBytes([0x00, 0x00, 0x00, 0x00]) + hdlrWriter.writeBytes(Data(repeating: 0, count: 4)) + hdlrWriter.writeString("vide", encoding: .ascii) + hdlrWriter.writeBytes(Data(repeating: 0, count: 12)) + let hdlrBox = buildBox("hdlr", data: hdlrWriter.data) + + let stblBox = buildBox("stbl", data: stsdBox + stcoBox) + let minfBox = buildBox("minf", data: stblBox) + let mdiaBox = buildBox("mdia", data: hdlrBox + minfBox) + let trakBox = buildBox("trak", data: mdiaBox) + + var mvhd = BinaryWriter(capacity: 100) + mvhd.writeBytes([0x00, 0x00, 0x00, 0x00]) + mvhd.writeBytes(Data(repeating: 0, count: 96)) + let moovBox = buildBox("moov", data: buildBox("mvhd", data: mvhd.data) + trakBox) + + var file = BinaryWriter(capacity: 256) + file.writeUInt32BigEndian(16) + file.writeString("ftyp", encoding: .ascii) + file.writeString("qt ", encoding: .ascii) + file.writeBytes([0x00, 0x00, 0x00, 0x00]) + file.writeBytes(mdatBox) + file.writeBytes(moovBox) + return file.data + } + // MARK: - tkhd Display-Matrix Rotation func testIdentityMatrixYieldsNoRotation() throws {