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 {