From 8a5934717bb5b512b476036235f0645344590a98 Mon Sep 17 00:00:00 2001 From: Truls Aagedal <5827165+aagedal@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:56:26 +0100 Subject: [PATCH 1/2] Recognise MXF AVC Long GOP and MPEG-2 HL picture coding labels codecNameForUL only mapped the AVC Intra family (01.32.xx) under the 0x02 registry kind, so Sony XAVC Long and bmx/FFmpeg-written H.264 MXF (01.31.xx, RP 2008) returned no codec. Map them to avc1 and derive the ffprobe profile name from byte 14. Also map MPEG-2 MP@HL / 422P@HL (01.03.xx / 01.04.xx, XDCAM HD/HD422), which had the same gap. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 11 ++++ .../SwiftMediaMetadata/Video/MXFReader.swift | 22 +++++++- .../Video/MXFReaderTests.swift | 55 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f9751..1432c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ 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.2] — 2026-11-01 + +### Fixed + +- MXF picture descriptors carrying SMPTE RP 2008 AVC Long GOP labels + (`04.01.02.02.01.31.xx.xx`, e.g. Sony XAVC Long and bmx/FFmpeg-written + H.264 MXF) now report codec `avc1`, and the profile from the label's + profile byte (e.g. `High 4:2:2`), instead of no codec. +- MXF MPEG-2 MP@HL and 422P@HL labels (`04.01.02.02.01.03.xx` / + `01.04.xx`, e.g. XDCAM HD and HD422) now report codec `mpeg2video`. + ## [3.1.1] — 2026-10-31 ### Fixed diff --git a/Sources/SwiftMediaMetadata/Video/MXFReader.swift b/Sources/SwiftMediaMetadata/Video/MXFReader.swift index acadf0b..5295f9e 100644 --- a/Sources/SwiftMediaMetadata/Video/MXFReader.swift +++ b/Sources/SwiftMediaMetadata/Video/MXFReader.swift @@ -883,6 +883,10 @@ public struct MXFReader: Sendable { // 0x2? — High 10 Intra (class 50/100/200, 4:2:0 10-bit) // 0x3? — High 4:2:2 Intra (class 50/100/200, 10-bit) // We fold all subclasses into the top-level ffprobe name. + // Long GOP AVC labels (variant 0x31) carry the profile in + // byte 14 per SMPTE RP 2008. 0x11 is left unmapped: bmx + // registers it as Constrained Baseline but FFmpeg writes it + // for any Baseline stream. if value.count >= 16, stream.profile == nil { let kind = value[value.startIndex + 11] let variant = value[value.startIndex + 13] @@ -893,6 +897,17 @@ public struct MXFReader: Sendable { case 0x30: stream.profile = "High 4:2:2 Intra" default: break } + } else if kind == 0x02, variant == 0x31 { + switch byte14 { + case 0x10: stream.profile = "Baseline" + case 0x20: stream.profile = "Main" + case 0x30: stream.profile = "Extended" + case 0x40: stream.profile = "High" + case 0x50: stream.profile = "High 10" + case 0x60: stream.profile = "High 4:2:2" + case 0x70: stream.profile = "High 4:4:4 Predictive" + default: break + } } } case 0x3301: // ComponentDepth (UInt32) @@ -1161,8 +1176,13 @@ public struct MXFReader: Sendable { if kind == 0x02 { let byte14 = ul[s + 14] switch variant { - case 0x32: // AVC in MXF (SMPTE ST 381-3 / RP 2008) + case 0x31: // AVC Long GOP profiles (SMPTE RP 2008), e.g. Sony XAVC Long return ("avc1", "H.264 / AVC") + case 0x32: // AVC Intra profiles (SMPTE ST 381-3 / RP 2008) + return ("avc1", "H.264 / AVC") + case 0x03 where ul[s + 12] == 0x01, // MPEG-2 MP@HL (e.g. XDCAM HD Long GOP) + 0x04 where ul[s + 12] == 0x01: // MPEG-2 422P@HL (e.g. XDCAM HD422) + return ("mpeg2video", "MPEG-2 Video") case 0x33: // HEVC in MXF (SMPTE ST 2067-51) return ("hvc1", "H.265 / HEVC") case 0x0A: // JPEG 2000 in MXF (SMPTE ST 422) diff --git a/Tests/SwiftMediaMetadataTests/Video/MXFReaderTests.swift b/Tests/SwiftMediaMetadataTests/Video/MXFReaderTests.swift index 4fe8d1f..e1f3f92 100644 --- a/Tests/SwiftMediaMetadataTests/Video/MXFReaderTests.swift +++ b/Tests/SwiftMediaMetadataTests/Video/MXFReaderTests.swift @@ -159,6 +159,61 @@ final class MXFReaderTests: XCTestCase { XCTAssertEqual(stream.profile, "High 4:2:2 Intra") } + func testParsePictureDescriptorDetectsXAVCLongGOPHigh422() { + // Sony XAVC Long 4:2:2 10-bit (camera/Resolve-written) — RP 2008 + // AVC Long GOP label urn:smpte:ul:060e2b34.0401010d.04010202.01316001. + let ul = Data([ + 0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0D, + 0x04, 0x01, 0x02, 0x02, 0x01, 0x31, 0x60, 0x01, + ]) + let body = localTagLV(0x3201, value: ul) + var stream = VideoStream(index: 0) + var duration: TimeInterval? = nil + MXFReader.parsePictureDescriptor(body, into: &stream, duration: &duration) + XCTAssertEqual(stream.codec, "avc1") + XCTAssertEqual(stream.codecName, "H.264 / AVC") + XCTAssertEqual(stream.profile, "High 4:2:2") + } + + func testParsePictureDescriptorDetectsBMXAVCLongGOPHigh() { + // bmx/FFmpeg-written registry version 0x0A, High profile. + let ul = Data([ + 0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x0A, + 0x04, 0x01, 0x02, 0x02, 0x01, 0x31, 0x40, 0x01, + ]) + let body = localTagLV(0x3201, value: ul) + var stream = VideoStream(index: 0) + var duration: TimeInterval? = nil + MXFReader.parsePictureDescriptor(body, into: &stream, duration: &duration) + XCTAssertEqual(stream.codec, "avc1") + XCTAssertEqual(stream.profile, "High") + } + + func testParsePictureDescriptorLeavesAmbiguousAVCBaselineProfileUnset() { + let ul = pictureCodingUL(kind: 0x02, variant: 0x31, byte14: 0x11) + let body = localTagLV(0x3201, value: ul) + var stream = VideoStream(index: 0) + var duration: TimeInterval? = nil + MXFReader.parsePictureDescriptor(body, into: &stream, duration: &duration) + XCTAssertEqual(stream.codec, "avc1") + XCTAssertNil(stream.profile) + } + + func testParsePictureDescriptorMapsMPEG2HighLevelLongGOPULs() { + // RP 224 MPEG-2 MP@HL / 422P@HL Long GOP (XDCAM HD / HD422). + for variant: UInt8 in [0x03, 0x04] { + let ul = Data([ + 0x06, 0x0E, 0x2B, 0x34, 0x04, 0x01, 0x01, 0x03, + 0x04, 0x01, 0x02, 0x02, 0x01, variant, 0x03, 0x00, + ]) + let body = localTagLV(0x3201, value: ul) + var stream = VideoStream(index: 0) + var duration: TimeInterval? = nil + MXFReader.parsePictureDescriptor(body, into: &stream, duration: &duration) + XCTAssertEqual(stream.codec, "mpeg2video", "variant \(variant)") + } + } + // MARK: - C. Picture descriptor — dimensions, frame rate, duration func testParsePictureDescriptorReadsStoredAndDisplayDimensions() { From 543322436fae357ab7c6bffeadd8b31e1b81852a Mon Sep 17 00:00:00 2001 From: Truls Aagedal <5827165+aagedal@users.noreply.github.com> Date: Sun, 1 Nov 2026 18:05:51 +0100 Subject: [PATCH 2/2] Prepare SwiftMediaMetadata 3.1.2 release Co-Authored-By: Claude Opus 5.5 --- README.md | 8 ++++---- RELEASE_CHECKLIST.md | 13 +++++++------ Sources/CLI/SwiftMediaMetadata.swift | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9d52c2c..f79a3bf 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.1 release](https://github.com/aagedal/SwiftMediaMetadata/releases/tag/3.1.1): +[3.1.2 release](https://github.com/aagedal/SwiftMediaMetadata/releases/tag/3.1.2): ```sh -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 +curl -LO https://github.com/aagedal/SwiftMediaMetadata/releases/download/3.1.2/swift-exif-macos-arm64.tar.gz +curl -LO https://github.com/aagedal/SwiftMediaMetadata/releases/download/3.1.2/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.1"), + .package(url: "https://github.com/aagedal/SwiftMediaMetadata.git", from: "3.1.2"), ] ``` diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index c80a575..3eaa90f 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -1,10 +1,11 @@ -# SwiftMediaMetadata 3.1.1 release checklist +# SwiftMediaMetadata 3.1.2 release checklist -- [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] MXF AVC Long GOP picture coding labels (`04.01.02.02.01.31.xx.xx`, + SMPTE RP 2008) map to `avc1` with the profile from byte 14; MPEG-2 MP@HL / + 422P@HL labels (`01.03.xx` / `01.04.xx`) map to `mpeg2video`. +- [x] Verified against Sony/Resolve XAVC Long 4:2:2 10-bit MXF (25p, 50i, + 59.94p) and XDCAM HD422 MXF: codec and profile match ffprobe. +- [x] Set CLI version, README and changelog to 3.1.2. - [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 3c7d2fc..5fa1d52 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.1", + version: "3.1.2", subcommands: [ ReadCommand.self, WriteCommand.self,