Skip to content

Releases: aagedal/SwiftMediaMetadata

SwiftMediaMetadata 3.0.3

Choose a tag to compare

@github-actions github-actions released this 22 Oct 14:11
74794cf

Fixed

  • MXF parsing now jumps over essence using the Random Index Pack (or the
    footer's PreviousPartition chain) instead of visiting every frame's KLVs.
    Frame-wrapped XAVC/XDCAM clips previously cost one small read per KLV across
    the whole file — seconds on SSD and far longer on camera cards; a 16 GB
    XAVC clip now parses in ~20 ms. Essence KLVs are also no longer peeked, and
    the NRT/ARRI fallback scan is limited to the header metadata. Files without
    a valid partition index keep the previous full walk.
  • MXF picture dimensions now use the descriptor's display rectangle when it is
    smaller than the stored (coding-block padded) one, matching ffprobe. XAVC
    1080p was reported as 1920×1088 with a 136:135 pixel aspect ratio.

SwiftMediaMetadata 3.0.2

Choose a tag to compare

@github-actions github-actions released this 02 Oct 18:03

Fixed

  • File-based MXF metadata parsing now uses bounded positional reads instead of
    materializing the complete media file. Essence and unneeded KLV payloads are
    skipped while metadata, MCA labels and file-relative offsets are preserved.
  • MXF reads reject invalid bounds, truncated payloads, and file extent changes;
    interrupted reads retry. Same-size concurrent writes are not detected.
  • Added parity, bounded-read, sparse-file, malformed-input, and extent-change
    regression coverage. Caller-owned Data continues to use the existing parser.

SwiftMediaMetadata 3.0.1

Choose a tag to compare

@github-actions github-actions released this 14 Sep 22:16

Fixed

  • Sony RTMD track discovery now skips top-level mdat payloads instead of
    copying the complete audio/video payload into memory. Structural boxes and
    file-absolute RTMD sample offsets retain their existing behavior.
  • The real-world bare-JPEG fixture lookup now supports SwiftPM's current copied
    resource-directory layout, restoring a clean test run on newer toolchains.
  • Release packaging now discovers SwiftPM's active binary output directory
    instead of copying a stale executable from a toolchain-specific build path.

SwiftMediaMetadata 3.0.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 13:19

Added

  • Breaking: XMPValue.languageAlternative and XMPLanguageAlternative
    retain every
    ordered rdf:Alt item, its exact xml:lang tag, duplicate languages, and
    empty values. The reader, writer, exporter, sidecar display, and embedded XMP
    paths round-trip the lossless representation; the existing scalar
    langAlternative case remains the carrier for one x-default value. Clients
    with an exhaustive switch over XMPValue must handle the new case.
  • IPTCXMPSynchronizationOptions and synchronization reports provide explicit
    merge, replace, clear, title, and date-precision policies for standards-aware
    IPTC-IIM ↔ XMP synchronization. The existing legacy synchronization methods
    remain available unchanged.
  • PhotoMetadata provides an XMP-preferred, IIM/Exif-fallback projection that
    retains every carrier candidate and reports disagreements. Canonical edits
    can be applied with PhotoMetadataMutation without discarding the raw XMP
    graph.
  • XMPStructurePatch and structured-array patching let callers update selected
    XMP members while retaining unknown sibling fields. The typed IPTC Extension
    and PLUS setters now use the same lossless merge behavior.
  • ImageMetadata.WriteOptions.FileCreationDatePolicy preserves a destination's
    creation date by default and can explicitly update or assign it independently
    of the modification-date policy.
  • MetadataWriteResult<Output> provides one source-compatible result contract
    for image, video, audio, XMP sidecar, and single-file batch writes. The result
    carries either serialized Data or the committed destination URL together
    with non-fatal warnings.
  • serialized() and writeResult(to:options:) separate in-memory metadata
    serialization from filesystem commits. Existing writeToData() and
    write(to:) entry points remain available as compatibility wrappers.
  • XMPSidecarRevision, XMPSidecarSnapshot, and XMPSidecar.update provide
    content-revisioned sidecar mutations with stale-write detection, bounded
    merge retries, serialized-packet validation, and an installed-state result.
    Transactional and ordinary sidecar writes share a cross-process lock and the
    existing filesystem write-options contract.
  • Per-format metadata capabilities describe read, direct-write, sidecar-only,
    opaque-preservation, stripping, and unsupported behavior independently for
    Exif, IPTC-IIM, XMP, Camera Raw, ICC, and C2PA.
  • semanticMetadata and preservationReport(comparedTo:) compare normalized
    metadata meaning rather than XML prefixes/order, TIFF byte order/offsets, or
    equivalent date and GPS spellings. Reports separate added, removed, changed,
    unrepresentable, and opaque-preserved values.
  • Typed XMP GPS APIs parse decimal, directional decimal, DMS, and
    degrees/decimal-minutes coordinates; retain original lexical values; validate
    ranges and hemisphere conflicts; and emit stable XMP coordinate/rational
    spellings. PhotoMetadata now retains typed XMP and Exif GPS candidates via
    PhotoMetadataValue.number; exhaustive switches over that enum must handle
    the new case.

Changed

  • The image API implementation is split into focused reading, writing, editing,
    extraction, sidecar, location, and format extensions. The public API remains
    source compatible.
  • plus:ImageSupplier structured arrays serialize as the standard-required
    rdf:Seq rather than rdf:Bag.
  • Ambiguous TIFF byte signatures remain TIFF regardless of camera Make. A
    proprietary TIFF-based RAW is classified from a known RAW filename extension
    when reading from a URL, preventing rendered Sony/Olympus/Pentax TIFFs from
    being incorrectly blocked by the RAW write guard.

Fixed

  • Release preflight notes are selected from the requested version's non-empty
    changelog section instead of always reusing the 2.0.0 notes or requiring a
    breaking-change entry in every future release.
  • Atomic image, video, audio, and sidecar writes explicitly restore the
    destination's prior filesystem visibility. This prevents iCloud Drive and
    similar providers from carrying a dot-prefixed staging file's hidden flag
    onto the installed file.
  • Atomic and direct writes restore the selected file creation date after the
    filesystem transaction.

SwiftMediaMetadata 2.0.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 19:45

Added

  • Deterministic parser property tests now exercise TIFF/Exif, ISOBMFF, XMP,
    C2PA/JUMBF, and video bitstreams with truncations, malformed lengths, seeded
    byte soup, and write-read-write stability checks. A reusable script supports
    longer reproducible local runs.
  • Release tooling now provides a single macOS preflight, a tag-driven GitHub
    release workflow, SHA-256 checksums, archive-content validation, and a
    step-by-step 2.0 release/Homebrew checklist.
  • ImageMetadata.WriteOptions.FileModificationDatePolicy controls filesystem
    modification dates independently of timestamps embedded in media metadata.
    Use .preserveExisting for in-place edits or .set(Date) when a new output
    should inherit a source file's date. Image, video, audio, XMP sidecar, and
    batch writes share the policy.
  • Mutating CLI commands accept -P / --preserve-file-modification-date.

Changed

  • Breaking: the Swift package, library product, importable module, source
    target, and test targets are renamed from SwiftExif to
    SwiftMediaMetadata. Clients must use the renamed GitHub repository, depend
    on the SwiftMediaMetadata product, and replace import SwiftExif with
    import SwiftMediaMetadata. The installed swift-exif CLI command is
    unchanged.
  • Image, video, audio, and XMP sidecar writes now use one internal filesystem
    transaction for atomic replacement, backups, temporary-file cleanup, and
    file attributes.
  • The 33,536-city offline geolocation database is now a versioned bundled
    resource instead of 54,000 lines of generated Swift literals. Reverse
    geocoding behavior is unchanged, while a measured clean library build is 79%
    faster and the geolocation debug object shrinks from 12.0 MB to 102 KB.

1.6.0 — Cinema RAW: ARRIRAW, RED, X-OCN, CRM/CRL + per-frame metadata

Choose a tag to compare

@aagedal aagedal released this 02 May 19:12

Added

  • ARRIRAW (MXF) — full slate / lens / camera / medium / frameline / LUT JSON harvest. ARRI cameras (ALEXA 35, ALEXA Mini LF, AMIRA, …) carry production-side metadata as a set of JSON documents wrapped in MXF KLV metadata sets (tag 0x807A MIME, 0x807B JSON payload, 0x807C schema URL). New ARRIJSONParser.swift decodes the six well-known schemas — slate_info, camera_device, lens_device, recording_medium, frameline, custom_lut3d_design — and surfaces them via CameraMetadata.userMeta*. MXFReader routes ALEXA-codec MXF clips through this path; FormatDetector reports arriraw so consumers can distinguish ARRI MXFs from generic broadcast MXF.
  • Sony X-OCN (MXF + NRT AcquisitionRecord) — VENICE / BURANO / F55 / F65 cinema RAW. Format detection routes .mxf clips with the X-OCN essence descriptor through a deep harvest of the embedded NonRealTimeMeta XML (camera attributes, ISO, shutter angle, gamma, gamut, monitoring transform, ASC CDL, roll/tilt sensor metadata). Surfaces alongside ARRIRAW under FileFormat = XOCN. Tested against VENICE 8K samples (8664×5784, ISO 3200, ShutterAngle 173°, full ASC CDL).
  • Canon Cinema RAW Light (.CRM / .CRL) — clip + per-frame CTMD metadata. New CRMReader.swift recognises the Canon-flavored ISOBMFF (brand crx + Canon UUID), exposes FileFormat = CRM, and decodes the CTMD timed-metadata sample groups for ISO, shutter time, focal length, aperture, white balance, and per-frame lens metadata across both the CRAW master format and the proxy CRL.
  • RED RAW (.R3D) — REDCODE clip metadata. New R3DReader.swift parses the RED2 atom and decodes OBKM / RDLG metadata (lens model, slate, sensor, codec). Verified against KOMODO Chroma Key and V-RAPTOR LUT samples.
  • Nikon RAW Video (.R3D, codec NR3D) — N-RAW detection. Nikon Z8 / Z9 N-RAW recordings happen to use the .R3D extension but are an ISOBMFF derivative (ftyp niko, codec NR3D), not REDCODE. Format dispatch sniffs the magic bytes to pick the right reader; misnamed files in "RED RAW" folders no longer get misclassified.
  • Sony RTMD per-frame attributes + IMU export. RTMDReader already surfaced clip-level summaries; the new rtmd-frames CLI subcommand walks every rtmd sample and emits one of three CSV streams — attributes (frame-level ISO, exposure, F-number, focal length, white balance, GPS, date-time, serial), gyroscope (timestamp + pitch/roll/yaw), or accelerometer (timestamp + xyz). Verified against Sony A1 / FX3 / FX30 footage at 2 kHz IMU sampling.

Changed

  • MP4Parser.swift decomposed (no behavior change). The 3,277-line monolith is now split along its existing MARK: boundaries into five files held together by extension MP4Parser:
    • MP4Parser.swift (477 lines) — top-level dispatch, mvhd, format detection
    • MP4UserData.swift (775) — udta/meta/ilst, GPS ©xyz, XMP/NRT uuid, BRAW per-frame bmdf header
    • MP4VisualSampleEntry.swift (557) — stsd visual entry, codec configs (avcC/hvcC/av1C/vvcC/dvcC/dvvC), HDR side-data (mdcv/clli), ProRes/APV FourCC defaults
    • MP4AudioSampleEntry.swift (290) — stsd audio entry, chan, esds
    • MP4TrackParser.swift (881) — trak walking, tkhd/mdhd/edts, timecode-track decoding
    • MP4Chapters.swift (364) — Nero chpl, QuickTime tref→chap, sample-table helpers shared with RTMDReader/BRAWFrameReader
      6.9× reduction in the main file. Verified by diffing the JSON read sweep over 332 mixed video / image files in the local corpus before and after — output is byte-identical.

Fixed

  • Linux cross-compile broken by the ARRIRAW commit. CFNumberIsFloatType is CoreFoundation, Apple-only — swift-corelibs-foundation doesn't ship it, so the static-musl Linux builds failed at the ARRI JSON slate stringifier added in af31434. Replaced with a Swift cast-order check (as? Int64 first; otherwise format as %g), preserving observable output on both platforms.
  • Phase26 --map test asserted lowercase XMP-dc:title / :description while ExifTool-conventional capitalization (XMP-dc:Title / :Description) had already landed in 3374496. Test now asserts the canonical form.

Tests

1478 tests, 0 failures (26 skipped). Verified end-to-end across the local corpus:

  • 47 images (jpg / arw / cr3 / heic / jxl) read sweep — 0 errors
  • 64 videos (8 X-OCN, 12 MXF, 2 ARRIRAW, 2 RED, 1 Nikon RAW, 3 BRAW, 2 CRM, 18 MP4, 11 MOV, …) — 0 errors
  • 3/3 BRAW clips × 821 per-frame rows
  • 7/7 RTMD-bearing clips × ~27k attribute rows + ~1.1M IMU samples each
  • MCA labels match the bmx labels.txt ground truth byte-for-byte
  • macOS arm64 + Linux aarch64 + Linux x86_64 binaries all return identical metadata for image / X-OCN / BRAW spot checks

Binaries

This release ships three CLI binaries:

  • swift-exif-macos-arm64 (5.7 MB, Mach-O arm64, -O+WMO)
  • swift-exif-linux-aarch64 (24 MB, static ELF musl aarch64, -Onone)
  • swift-exif-linux-x86_64 (26 MB, static ELF musl x86_64, -Onone)

Linux binaries tested on alpine:3.20 for both arches.

1.5.0 — BRAW container metadata, ExifTool/ffprobe parity

Choose a tag to compare

@aagedal aagedal released this 01 May 20:45

Added

  • Blackmagic RAW (.braw) container metadata — new format support routed through MP4Parser. BRAW is an ISOBMFF derivative with the legacy QuickTime layout (wide + mdat head, moov tail-placed, no ftyp); the parser tolerates the missing ftyp and the mdta decoder grew handlers for BRAW's typed payloads including the BMD-specific type 71 (float32-BE pair) used for rectangle fields. Surfaces to CameraMetadata: camera make/model + firmware, color science generation, viewing gamma + gamut, compression ratio, shutter type; off-speed captureFps (distinct from the mvhd/stts-derived frameRate); production slate (clip number / scene / take / reel / camera / environment / day-night); sensor area, crop / safe-area rectangles, LUT used, post-3DLUT mode, embedded LUT name, frameguide aspect ratio, gamut compression. Per-frame interpretation (white point, tint, absolute ISO, shutter angle) lives in proprietary bfdn / ctrn boxes inside the codec sample entry and remains unparsed. Verified against Pyxis 12K / 6K and Cinema 6K samples.
  • MPEG-TS bitstream decoding — MPEGReader now decodes inline H.264 / HEVC SPS, AAC ADTS frame headers, and PCR timestamps directly from Transport Stream packets, surfacing pixel format, profile/level, framerate, sample rate, channel layout, and timing on .ts / .m2ts files without ffprobe. New MPEGBitstream.swift houses the bit-readers (Exp-Golomb, NAL emulation-prevention unescaping, ADTS frame parsing).
  • SMPTE ST 377-4 MCA audio labels for MXF — multichannel audio labeling per SMPTE ST 377-4 is now decoded out of MXF audio descriptors. New MCAAudioLabeling, MCALabelsRenderer, MXFMCAReader, surfaced through VideoStream / VideoMetadata and the JSON exporter. New CLI subcommand swift-exif mxf-labels emits a bmx-compatible labels.txt round-trip for production audio workflows.
  • Apple ecosystem support (Phase 18) — AppleMakerNote.swift (iPhone MakerNote: lens model, ContentIdentifier, image stabilization, HDR mode, …), AAESidecar.swift (Apple Photos .aae adjustment XML), HEIFAuxiliaryImages.swift (depth maps, alpha mattes, HDR gain maps via shared ISOBMFF / iloc), and Live Photo ContentIdentifier surfaced from MP4Parser.
  • Pentax, Leica, and Sigma MakerNote parsers — three additional vendor MakerNote implementations with matching writer support and round-trip test coverage.
  • ExifTool / ffprobe parity initiative — PARITY_PLAN.md plus the first phases:
    • HEIC parity (Phase 1) — infe-driven content_type, explicit meta → iprp → ipco walk, top-level colr fallback.
    • JPEG XL (Phase 2) — decode SizeHeader from the JXL codestream so File:ImageWidth / Height are populated.
    • MP4 streams (Phase 3.1 / 3.2 / 3.5) — preserve track declaration order in streams, surface data tracks, hoist chapter tracks out of streams into a sibling chapters array.
    • MP3 / M4A streams (Phase 3.3) — emit a single synthetic audio stream from --streams to match ffprobe.
    • MKV track order (Phase 3.4) — preserve Matroska track declaration order.
    • GPS / subtitle codec cosmetics (Phase 4) — render GPS coordinates with degree signs, alias tx3g to mov_text.

Changed

  • Format-level duration now matches ffprobe. Previously format.duration was mvhd verbatim; mvhd is the longest mdhd of any track, so files like ChapterMarkerDualSubtitle.mp4 reported 98.691 s while audio/video both ended at ~71.16 s. The MP4 reader now caps mvhd at max(audio, video) when subtitle/data tracks inflate it, and keeps the smaller mvhd value untouched when edit-list trimming legitimately makes it shorter than the longest essence stream.
  • Audio file format.bit_rate now matches ffprobe. For MP3 / M4A / FLAC, the container bit_rate is now derived as file_size × 8 / duration (whole-file rate including container overhead and AAC priming/postroll padding) instead of the audio stream's declared rate.
  • MXF and MPEG-TS per-stream Duration. MXF essence descriptors and MPEG-TS streams don't carry per-track durations the way MP4 does. The format-level duration is now propagated to streams that don't already have one (matching ffprobe behaviour); legitimate per-stream durations are never overwritten.
  • GPMF SCAL divisor lookup is now hoisted out of the per-sample GPS5 loop in GPMFReader — pure refactor, behavior preserved.

Fixed

  • GIF parser sub-block handling — regression tests added covering the earlier sub-block-overrun hardening (truncated extension blocks, malformed image descriptors, unterminated sub-block chains).

Tests

1420 tests, 0 failures (26 skipped), covering BRAW slate extraction across Pyxis 12K / 6K / Cinema 6K, the new MakerNote families, MXF MCA round-trip via bmx, MPEG-TS bitstream parity vs ffprobe, and HEIF auxiliary image extraction.

1.4.0 — Recursive XMPValue, more RAW formats, C2PA thumbnails

Choose a tag to compare

@aagedal aagedal released this 29 Apr 20:40

Added

  • NRW, SRW, and generic .raw extension support — RawFormat now lists nrw (Nikon Coolpix), srw (Samsung), and raw alongside the existing TIFF-based RAWs. Read and write parity with NEF / ARW / ORF / PEF via the shared TIFFFileParser.
  • ImageMetadata.extractC2PAThumbnails() — convenience accessor returning every c2pa.thumbnail.claim.* / c2pa.thumbnail.ingredient.* assertion as a C2PAThumbnail (label, raw bytes, format suffix). Mirrors the existing extractThumbnail() pattern for EXIF IFD1 thumbnails.
  • Recursive XMPValue for nested structured schemas — Adobe Camera Raw's MaskGroupBasedCorrections (rdf:Bag of corrections, each holding its own rdf:Bag of mask sub-structs), face regions in mwg-rs, and other recursive XMP shapes now ride the generic API instead of being hand-rolled. Both writer and reader recurse.

Changed

  • Breaking — XMPValue.structure payload is now [String: XMPValue] (was [String: String]), and XMPValue.structuredArray is [[String: XMPValue]] (was [[String: String]]). Direct enum-pattern consumers must wrap field values as .simple(...) when constructing them, and unwrap when reading.

    Two new convenience accessors smooth the migration: flatStructureValue and flatStructuredArrayValue return the legacy [String: String] shape by dropping any non-.simple entries — useful for IPTC / xmpDM timecode / stRef schemas. XMPData.flatten and XMPData.wrapSimple round-trip between the two forms.

  • Build script drops macOS Intel — Scripts/build-release.sh now ships macOS arm64 + Linux x86_64-musl + Linux aarch64-musl only.

Tests

1087 tests, 0 failures (14 new tests covering recursive XMP structures, C2PA thumbnail edge cases, and NRW write round-trip).

1.3.1 — Scan Type resolution + parser hardening

Choose a tag to compare

@aagedal aagedal released this 23 Apr 23:40

Added

  • VideoScanType + scanType / scanOrder helpers on VideoMetadata
    and VideoStream — derive MediaInfo-style "Scan Type" (Progressive /
    Interlaced / Unknown) and "Scan Order" (TFF / BFF) UI columns without
    enumerating every VideoFieldOrder case. fieldOrder still encodes
    both values in one enum and remains the ground truth.
  • MXF FieldDominance parsing (0x3212) — SMPTE 377-1 §G.2.51 tag now
    resolves TFF vs BFF for interlaced essence descriptors that carry it.

Fixed

  • fieldOrder now resolves for every supported container. Previously,
    MP4 / MOV without a fiel atom, Matroska / WebM without FlagInterlaced,
    and MXF with FrameLayout=1 (separated fields) all returned nil or
    .unknown, leaving downstream "Scan Type" UIs blank for the majority
    of real-world files. New behaviour:
    • MP4 / MOV: absence of fiel defaults to .progressive (matches
      ffmpeg's mov demuxer and iPhone / camera convention).
    • Matroska / WebM: absence of FlagInterlaced / FieldOrder
      defaults to .progressive for video essence (cover-art MJPEG tracks
      keep nil). VP8 / VP9 / AV1 have no interlaced coding mode; HEVC /
      H.264 writers only emit these elements for genuinely interlaced
      source.
    • MXF: FrameLayout=1 (separated fields) and FrameLayout=3
      (mixed) now resolve to .topFieldFirst (the broadcast convention and
      what MediaInfo / ffprobe report for untagged interlaced essence).
      FieldDominance=2 overrides to .bottomFieldFirst. Previously these
      returned .unknown.

Security

  • Int-overflow hardening across binary parsers — malformed input can
    no longer trap the process via arithmetic overflow on 32-bit lengths,
    offsets, or sub-block sizes. Affected paths: ICC profile, IPTC IIM,
    MPF, XMP, PNG, PSD, and the shared Data slicing helpers.
  • MP4 chapter allocation cap — chpl (Nero) and udta > chap
    readers refuse chapter counts that would allocate more than a sane
    ceiling, stopping a malformed count field from triggering
    gigabyte-scale Array.reserveCapacity on import.
  • GIF sub-block overrun fix — application-extension sub-block reader
    no longer walks past the declared data length when a truncated stream
    omits its terminator.
  • Video-read memory cap — the MKV / WebM front-end limits aggregate
    bytes buffered for a single import, keeping multi-file scans (batch
    imports, folder-watch) well under 1 GB resident even when fed
    unusually large Matroska clusters.
  • CBOR decoder overflow guard — string and byte-string readers
    reject lengths that would overflow Int, preventing a crafted C2PA
    manifest from crashing the CBOR front-end.
  • zlib deflate-bomb cap — PNG / ICC / XMP inflate paths stop
    producing output once a generous decompressed-size ceiling is
    reached, so a tiny deflate stream can no longer expand to gigabytes
    and exhaust memory.

Validation

Audited against a 22-file video fixture set (H.264, HEVC, VVC, AV1,
VP8/VP9, ProRes, ProRes RAW, APV, AVC-Intra, MXF broadcast):

  • All 20 modern MP4 / MOV / MKV / WebM files now resolve Scan Type to
    Progressive (previously nil for 6 of them).
  • Both broadcast MXF files (AVC-Intra 50) now resolve to Scan Type =
    Interlaced, Scan Order = TFF — matches MediaInfo output.

1.3.0 — Chapter markers + multi-source timecodes

Choose a tag to compare

@aagedal aagedal released this 23 Apr 14:50

Added

  • Chapter markers across three container families:
    • MP4 / MOV / M4V: QuickTime text-track chapters (tref > chap pointing
      at a text/subt track whose stts-timed UTF-8 samples are the titles —
      written by DaVinci Resolve, Apple Compressor, iTunes, ffmpeg
      -map_chapters), with Nero udta > chpl as the fallback (x264, ffmpeg,
      MP4Box).
    • Matroska / WebM: top-level Chapters master element — every
      EditionEntry + ChapterAtom, honouring EditionFlagHidden and
      ChapterFlagHidden; supports both ChapLanguage and the newer
      ChapLanguageBCP47.
    • New VideoChapter struct (index, optional id, startTime,
      endTime, title, language, computed duration). Exposed via
      VideoMetadata.chapters.
  • Provenance-tagged timecodes: [Timecode] on VideoMetadata —
    every independent clip-level source (tmcdTrack, quicktimeUdta, xmpDM,
    xmpDMAlt, mxfMaterialPackage, mxfFilePackage, sonyNRT) recorded
    separately with optional frame-rate companion. The scalar timecode stays
    in sync with the first recorded value for backward compatibility.
    recordTimecode(value, source:) helper dedupes entries and appends a
    timecode mismatch: … warning when two sources disagree.
  • XMP timecode parsing — XMPData.startTimecode / .altTimecode
    decode xmpDM:startTimeCode / xmpDM:altTimecode, mapping timeFormat
    (24Timecode, 29.97Timecode, 2997DropTimecode, 50Timecode, …) to a
    numeric fps.
  • MXF TimecodeComponent labelling — the first-encountered component is
    tagged .mxfMaterialPackage, subsequent ones .mxfFilePackage (header
    metadata always puts MaterialPackage before SourcePackage).
  • Sony NRT LtcChangeTable — LtcChange@frameCount="0" now feeds a
    .sonyNRT entry in timecodes, accepting both the hex-encoded SMPTE 12M
    LTC word (XDCAM professional bodies) and the already-formatted
    HH:MM:SS:FF string (Alpha consumer bodies).
  • CLI --streams --format json now emits:
    • format.Timecodes — array of { value, source, frameRate } entries.
    • format.ChapterCount / ChapterStartTimes / ChapterEndTimes /
      ChapterDurations / ChapterTitles / ChapterLanguages.
    • Per-chapter rows in the streams array with StreamType="chapter",
      carrying Index, ChapterUID, StartTime, EndTime, Duration,
      Title, Language — mirrors ffprobe's -show_chapters layout.

Changed

  • QuickTime text chapter tracks are no longer reported as subtitles.
    Any track referenced via tref > chap (on any parent track — DaVinci
    writes it on every trak, ffmpeg's mov muxer writes it only on non-video
    traks) is now excluded from VideoMetadata.subtitleStreams, matching
    ffprobe's -select_streams s output exactly. Real user-facing
    subtitles (tx3g, stpp, wvtt, …) are unaffected.
  • Per-stream timecode is only set when the video track explicitly
    cross-references a tmcd track via tref > tmcd — matches ffprobe's
    behaviour of leaving per-stream tags empty for tmcd tracks that are
    only advertised at the clip level (e.g. Atomos Ninja ProRes RAW).

Fixed

  • DaVinci Resolve and ffmpeg-muxed MP4/MOV files no longer inflate the
    subtitle count by including the hidden chapter-text track.

Validation

Verified end-to-end against:

  • DaVinci Resolve MP4 + MOV exports (with 0, 1, and 2 real subtitle
    tracks alongside the chapter text track).
  • ffmpeg-remuxed MP4 with a second SRT-sourced subtitle track and
    chapters preserved via -map_chapters.
  • 90 GB Harry Potter Matroska with 37 chapters (English).