Skip to content

Latest commit

 

History

History
695 lines (615 loc) · 35.3 KB

File metadata and controls

695 lines (615 loc) · 35.3 KB

Changelog

All notable changes to this crate are documented here. Format follows Keep a Changelog; the project adheres to Semantic Versioning.

[0.2.0]

Breaking, on two independent counts. frame::Rational widens to i64/NonZeroI64 and its constructor becomes checked (see Changed below). And two public dependencies cross a major: mediatime 0.1 → 0.2 (mediatime::Timestamp appears in frame::TimestampedFrame's public signatures, so a caller holding a mediatime 0.1 value no longer type-checks) and buffa 0.8 → 0.9 (Message is implemented for public types, so a downstream on 0.8 no longer sees those impls). No wire byte changes — every entry below carries its own proof.

Changed

  • frame::Rational is now i64 / NonZeroI64 (was u32 / NonZeroU32), and [Rational::new] is checked rather than total: it panics on num < 0 or den < 0, with a new Rational::try_new -> Option<Self> as the fallible form. SampleAspectRatio (a newtype over Rational) and FrameRate (which composes it) follow automatically and carry no width of their own; SampleAspectRatio::new panics the same way, and its fallible route is the existing Rational::try_new(..).map(SampleAspectRatio::from).

    Why i64, and why mediatime::Timebase stays i32. mediaframe is a pure receiver — nothing here is handed back to a decoder SDK — so "must round-trip into an AVRational", the reason Timebase went to i32, does not apply. What does apply is storage (sqlx has no Type<Postgres> for u32, so a u32 widens to i64 to be stored regardless) and ingest (R3D metadata returns unsigned int, ISO BMFF pasp is unsigned int(32) — values i32 would have to reject). Timebase is additionally an arithmetic operand whose rescale overflow proofs need num < 2^32; Rational never multiplies against a PTS and carries no such proof. The two types differ deliberately — this is not an inconsistency to reconcile.

    The four setters (with_num/with_den/set_num/set_den) now route through new, so the sign invariants have exactly one enforcement site rather than a mutator hole. Deserialize was the other unguarded construction path — the derive assigns fields directly, and the field types no longer carry the invariant — so each field gained a deserialize_with guard; {"num": -5} is now a deserialization error instead of a value the constructor would refuse. The constructor deliberately does not reduce to lowest terms: a stream declaring 2/4 reads back as 2/4.

    The wire format does not change. SampleAspectRatio and Rational move from uint32 num/den to int64 num/den, which is the same plain non-ZigZag varint over every value the old representation could hold. Proven, not inferred: 680 payloads across Rational, SampleAspectRatio and FrameRate — spanning every varint continuation boundary and u32::MAX — encode to identical bytes under both representations, and the i64 build cross-decodes all 680 uint32-era payloads back to the same values and the same bytes. (sint32/sint64 would have been the silent break, since ZigZag re-encodes every value; this crate uses neither.) Decode stays total in the newly reachable directions: a negative numerator clamps to 0 and a zero-or-negative denominator to 1, matching mediatime::Timebase's decode policy.

  • xtask: syn 2 → 3, prettyplease 0.2 → 0.3 — a coupled bump (prettyplease 0.3 requires syn ^3, so neither moves alone). Dev-only: xtask is publish = false, so nothing here reaches the published mediaframe artifact. syn 3's breaking change is Signature::unsafety: Option<Token![unsafe]> → the tri-state Signature::safety: Safety (Rust 2024 unsafe extern); xtask names only syn::Ident and syn::parse2::<syn::File> and never inspects a signature, so it compiles unchanged. prettyplease 0.3 emits byte-identical output to 0.2 for the generated mediaframe/src/codec.rs (89,303 bytes pre-rustfmt), so cargo xtask check's byte-for-byte freshness diff stays green and the committed file needs no regeneration.

  • quickcheck-richderive 0.3 → 0.4 (quickcheck feature) — upstream is a dependency-only release (its own syn 2 → 3 migration); the derive, the accepted attribute keys, and the emitted impls are unchanged. Re-verified against this crate rather than inherited: -Zunpretty=expanded over --features quickcheck,frame,buffa,serde,arbitrary is byte-identical across the bump (263,301 lines). All 40 derive sites keep their #[quickcheck(arbitrary = "…")] attributes as-is — that key names a function, and every value here points at a pub(crate) fn(&mut Gen) -> T in quickcheck_helpers, so none of them is the sibling with = "…" key (which names a module supplying both arbitrary and shrink). No consumer-visible change.

  • buffa 0.8 → 0.9 (buffa feature) — Message::write_to now takes &mut impl EncodeSink in place of &mut impl BufMut, so all 26 write_to signatures in src/buffa.rs move (the trait method's parameter type is what changed, so keeping BufMut is an E0276 "impl has stricter requirements"). Nothing else in the module changes: no body touches a BufMut method directly — every byte goes through buffa's encode_* helpers, whose bodies are unchanged — and buffa carries a blanket impl<T: BufMut + ?Sized> EncodeSink for T, so every existing caller still passes a Vec<u8> / BytesMut. The wire format does not change. Established on this crate's own types rather than inherited: all 37 Message impls were driven over 400 deterministic arbitrary values each (14,800 encodings) under 0.8.1 and 0.9.1, and the encoded bytes are identical in every case — so bytes written by a 0.8-linked peer still decode here. The 112 non-identity round-trips are audio::SampleFormat only, are present identically in both runs, and are the documented Other(SmolStr)Unknown(u32::MAX) collapse, not a regression. EncodeSink's segmented Rope sink is not adopted here.

  • mediatime 0.1 → 0.2mediatime::Timebase's num/den became i32/NonZeroI32 (matching ffmpeg's AVRational, which is {int num; int den;}), Timebase::new now panics on a negative numerator or denominator with try_new returning Option, and its Deserialize gained a range guard. The surface this crate touches is small: mediatime::Timestamp — not Timebase — is what frame::TimestampedFrame carries, and Timestamp::new(i64, Timebase) is unchanged, so the single site that moves is one test's NonZeroU32NonZeroI32 denominator literal. Every other Timebase mention in this crate is prose, and each statement it makes (non-proto-zero 1/1 default; a frame rate is deliberately not a PTS timebase) is still true of 0.2. Also collapses the transient duplicate from the previous commit: mediatime 0.2 requires buffa 0.9, so the graph carries one buffa again.

[0.1.7]

Added

  • Primaries::chromaticities() / Primaries::white_point()const fns exposing the per-standard CIE 1931 xy reference data for each defined Primaries variant: the R, G, B primaries as Option<[ChromaCoord; 3]> (index 0 = red, 1 = green, 2 = blue) and the reference white point as Option<ChromaCoord>, both in ChromaCoord's SMPTE ST 2086 fixed-point units (floating value = raw / 50000.0, so BT.709 red (0.640, 0.330) is (32000, 16500)). Values track FFmpeg's av_csp_primaries_desc (libavutil/csp.c) across BT.709 / sRGB, BT.470 M/BG, SMPTE 170M/240M, Film, BT.2020, SMPTE ST 428, DCI-P3 (RP 431-2), Display-P3 (EG 432-1), and EBU 3213-E, with white points D65 / CIE C / DCI / equal-energy E as each standard dictates. Unknown and Unspecified return None (no defined primaries); the within-crate match is exhaustive without a wildcard, so a future primaries variant cannot silently fall through. Puts the colorimetric reference data in the format authority so downstream crates (e.g. colconv) consume one table instead of re-hardcoding chromaticities, and unblocks chromaticity-derived matrix work. Note that SMPTE ST 428 mirrors FFmpeg's tabulated D-Cinema primaries (white point E), not the CIE XYZ identity that ITU-T H.273 Table 2 lists for ST 428-1. Additive and non-breaking.

[0.1.6]

Added

  • PixelFormat::V410Be — first-class big-endian counterpart of V410Le for the packed YUV 4:4:4 10-bit V410 layout (one 32-bit word per sample). The big-endian decode path already existed — the V410Frame<'a, true> / V410BeFrame borrow view, the V410<true> source marker, and the endian-generic v410_to::<true> walker — and is now exposed as a wire-stable enum variant (as_str() slug "v410be", discriminant 435). Additive and non-breaking.
  • PixelFormat::canonical()const fn resolving a deprecated / aliased pixel format to (canonical_format, Option<DynamicRange>): the non-deprecated format describing the same bytes, plus the dynamic range the alias pins (or None when the range is stream-driven). Centralises the alias table in the format authority so downstream crates (e.g. colconv) consume one mapping instead of each re-deriving it. Resolves the yuvj{411,420,422,440,444}p full-range aliases → their yuv*p base + DynamicRange::Full, Gray8a / Y400aYa8, and the XV30 byte-order pair onto its matching V410 variant — Xv30LeV410Le and Xv30BeV410Be (XV30 is the FFmpeg rename of the identical-bit-pattern V410; both endians resolve while preserving byte order). The match is exhaustive without a wildcard, so a future alias variant cannot silently fall through. Additive and non-breaking — every other format (including Unknown) returns (self, None).

[0.1.5]

Added

  • Pixel-format source coverage — frame types, source markers, and {fmt}_to walkers for a large batch of additional formats, each wired through its per-family feature flag:
    • NV20 (yuv-semi-planar) — 10-bit low-bit-packed semi-planar 4:2:2; the low-bit-aligned twin of P210 (one u16 per sample with the 10 active bits in the low positions).
    • Gray family (gray) — Gray32 (32-bit), Grayf16 (f16), Yaf16 / Yaf32 (f16 / f32 gray + alpha).
    • GBR family (gbr) — Gbrap32 (32-bit GBRA), Gbrp10Msb / Gbrp12Msb (MSB-packed — samples in the high bits).
    • RGB family (rgb / rgb-float) — Rgb96 / Rgba128 (32-bit-per-channel integer), Rgbaf16 / Rgbaf32 (f16 / f32 RGBA).
    • YUV 4:4:4 MSB (yuv-planar) — Yuv444p10Msb / Yuv444p12Msb (MSB-packed planar 4:4:4).
    • Packed 4:4:4 (yuv-444-packed) — Ayuv, Uyva, Vyu444.
    • Legacy bit-packed RGB (rgb-legacy) — Rgb4 / Rgb4Byte / Rgb8 and Bgr4 / Bgr4Byte / Bgr8.
    • Xv48 (yuv-444-packed) — 16-bit packed YUV 4:4:4 (FFmpeg AV_PIX_FMT_XV48LE / BE); the full-16-bit sibling of Xv36.
    • Yuva420p12 (yuva) — 12-bit low-bit-packed planar YUVA 4:2:0; a mediaframe extension (no FFmpeg pixel format) that non-FFmpeg decoders / WebCodecs emit.

Changed

  • High-bit Bayer is now endian-aware (bayer) — the Bayer source marker gains a trailing const BE: bool = false (source-compatible default), mirroring the Y2xx family, so the 10 / 12 / 14 / 16-bit Bayer formats (all four CFA patterns) support both little- and big-endian planes. The &[u16] plane is interpreted as wire bytes (LE for BE = false, BE for BE = true); FFmpeg defines the Bayer LE/BE split only at 16-bit, so the 10 / 12 / 14-bit forms are mediaframe extensions. Little-endian behavior is byte-identical on little-endian hosts.

[0.1.4]

Added

  • audio::ReplayGain — value object for container-tagged loudness- normalization recommendations (FFmpeg AV_PKT_DATA_REPLAYGAIN side data or the REPLAYGAIN_TRACK_* / REPLAYGAIN_ALBUM_* AVDictionary keys). Carries track_gain_db, track_peak, and the optional album-level album_gain_db / album_peak. Distinct from [audio::Loudness]: Loudness is the EBU R128 measurement of the signal; ReplayGain is the normalization recommendation a tagger wrote into the container (the delta from a −18 LUFS reference). Album-level numbers cannot be computed from a single track's loudness alone, so both are independently useful and not redundant. Buffa wire bridge: { float track_gain_db = 1; float track_peak = 2; optional float album_gain_db = 3; optional float album_peak = 4; }. Test helpers wired through quickcheck_helpers::composite::replay_gain + arbitrary_impls::composite.

[0.1.1] May 21, 2026

Added

  • serde feature — optional serde::{Serialize, Deserialize} for the whole descriptor vocabulary, gated behind --features serde (off by default). The wire shape mirrors what storage backends already use, so a serde-json value matches their representation:
    • Open codec / format enums (codec::{Video,Audio,Subtitle}Codec, container::Format, subtitle::Format, audio::{ChannelLayout, ContainerFormat}) serialize as their canonical as_str() slug — VideoCodec::H264"h264", Other("x265")"x265" (no {"Other": …} wrapper).
    • audio::SampleFormat — has BOTH an Unknown(u32) numeric escape AND an Other(SmolStr) string escape, so it gets a bespoke impl rather than the slug-only path. On human-readable formats (JSON / YAML / …): named + Other values serialize as their as_str() string, Unknown(v) as the bare numeric code v. On non-human-readable binary formats (bincode / postcard / …): an explicit tagged {Code(u32), Slug(Cow<str>)} wire enum, since deserialize_any is unavailable there. All three arms round-trip losslessly on both.
    • Closed FFmpeg-coded enums with a lossless Unknown(u32) escape (color::{Matrix, Primaries, Transfer, DynamicRange, ChromaLocation, DcpTargetGamut}, pixel_format::PixelFormat, frame::{Rotation, FieldOrder, StereoMode}) and disposition::TrackDisposition serialize as their to_u32() integer. Round-trip is total: an unrecognised code deserializes to Unknown(v). These accept only integers — there is no slug form.
    • Strictly-closed coded enums (no Unknown arm)subtitle::TrackOrigin (Embedded/Sidecar/External) and audio::BitRateMode (Cbr/Vbr/Abr) — serialize as their to_u32() integer but reject unrecognised wire codes as serde errors instead of silently collapsing them to the default variant. Both expose a try_from_u32(v: u32) -> Option<Self> method backing this behavior.
    • Plain structs (color::Info and its HDR/mastering sub-structs, frame::{Dimensions, Rect, Rational, SampleAspectRatio, FrameRate}, audio::{Loudness, Tags, Device}… ) derive serde directly.
    • Validated structs (capture::GeoLocation, audio::Fingerprint, audio::CoverArt) route deserialize through their checking constructors, so out-of-range / invariant-violating values are rejected rather than materialised.
    • lang::Language serializes as its canonical BCP-47 string ("en-US", "zh-Hant-TW", "und").
    • Works at every capability tier: the no-alloc Copy types gain serde under bare --features serde; the heap-tier types (codecs, formats, audio metadata, capture, language) when paired with alloc / std (forwarding serde to smol_str / bytes).

[0.1.0] May 19, 2026

Initial mediaframe release — this crate is a rename of the videoframe crate. It was previously published as videoframe (version line 0.1.x0.3.x); those videoframe crates.io versions are being yanked and superseded by mediaframe 0.1.0 (fresh crate identity).

Added

  • audio module — first cut of the audio-stream descriptor vocabulary (audio + container cluster of the 0.1.0 stream-vocab expansion):
    • audio::ChannelLayout#[non_exhaustive] closed enum of common FFmpeg AV_CH_LAYOUT_* shapes (Mono, Stereo, _2_1 through _7_1 with *Back side-vs-back variants, Hexagonal, Octagonal, Ambisonic1/2/3) plus Other(SmolStr) lossless escape; as_str() returns the FFmpeg-canonical slug, FromStr is total.
    • audio::BitRateMode — closed Cbr / Vbr / Abr trichotomy (default Cbr), to_u32/from_u32 for the wire codec.
    • audio::SampleFormat — sample-format vocabulary mirroring FFmpeg AVSampleFormat (U8/S16/S32/S64/Flt/Dbl packed + their *p planar twins), lossless Unknown(u32) + Other(SmolStr) escapes, to_u32/from_u32 per FFmpeg AV_SAMPLE_FMT_* enum indices, is_planar() predicate.
    • audio::ContainerFormat — audio-only container vocab (Mp3, Aac, Flac, Ogg, Opus, Wav, Aiff, Alac, Wma, Ape, Wv, Mka, M4a, Caf) plus Other(SmolStr).
    • audio::Loudness — EBU R128 / ITU-R BS.1770 measurement value object (integrated_lufs, range_lu, true_peak_dbtp, sample_peak_dbfs — all f32; no Eq/Hash).
    • audio::Fingerprint — algorithm-tagged opaque bytes ({ algorithm: SmolStr, value: bytes::Bytes } — O(1) clone), try_new rejects empty algorithm.
    • audio::CoverArt — embedded picture ({ mime: SmolStr, data: bytes::Bytes } — O(1) clone), try_new rejects empty mime / empty data.
    • audio::Tags — FFmpeg / Vorbis-Comment / iTunes-atom metadata: title, artist, album_artist, album, composer, genre, comment (SmolStr, "" = absent) + year, track / disc number + total (Option<u16>) + language (Option<SmolStr>, TODO(lang) — swap to Option<crate::Language> after the capture-lang cluster lands).
  • container::Format — top-level multimedia container vocabulary (Mov, Mp4, Mkv, Webm, Avi, Flv, MpegTs, Ogg, Asf, Rm, Wmv, Mxf, Gxf, Threegp.3gp digit- prefix-renamed) plus Other(SmolStr); audio-only containers live on [audio::ContainerFormat].
  • subtitle moduleFormat (file / demuxer-tag axis, #[non_exhaustive] + Other(SmolStr); named variants for the common text- and image-based formats — Srt / WebVtt / Ass / Ssa / Sub (MicroDVD) / Mpl2 / Lrc / Smi / Stl / Sbv / Ttml / MovText / DvdSub / PgsSub / HdmvPgs / DvbSub / XSub; as_str / total FromStr round-trip; is_image_based helper for mediaschema's REQUIRES_OCR derivation) and TrackOrigin (closed unit-only enum — Embedded / Sidecar / External; stable to_u32 / from_u32 ids 0 / 1 / 2; Default == Embedded). The module is gated on the alloc feature for the Other(SmolStr) escape.
  • disposition::TrackDisposition — FFmpeg AV_DISPOSITION_* bitflags from libavformat/avformat.h n8.1 (u32 backing). Shared across video / audio / subtitle tracks; ports the placeholder that used to live in mediaschema::domain::bitflags. to_u32 / from_u32 aliases for bits / from_bits_retain so unknown bits round-trip losslessly.
  • capture module (alloc-gated) — EXIF / capture-metadata vocabulary.
    • Device { make, model } (private SmolStr fields; empty string means absent, never Option<SmolStr>; builders / setters / is_empty).
    • GeoLocation { lat: f64, lon: f64, altitude: Option<f32> } with range-validating try_new, ISO-6709 degrees-only parse/format (from_iso6709 + to_iso6709, FromStr + Display, hand-rolled <200-line parser — no regex / no chrono). (0, 0) "Null Island" is accepted (it is a real, legal coordinate); only out-of-range lat/lon and structurally bad strings are rejected via GeoLocationError::{LatOutOfRange, LonOutOfRange, Iso6709Malformed}.
  • lang::Language (alloc-gated) — validated BCP-47 language tag wrapping icu_locale_core Language/Script/Region subtags (Copy, heap-free in-rust representation; the to_bcp47() -> String / Display surface needs the allocator). try_new(lang, script, region) + from_bcp47 / Default = "und" (ISO 639-3 undetermined) + is_undetermined + FromStr. LanguageError::{InvalidLanguage, InvalidScript, InvalidRegion, MalformedBcp47}.
  • buffa — hand-written Message / DefaultInstance wire support for every new type (see the ## Audio + container types, ## Subtitle + disposition, and ## Capture + language sub- sections of the buffa.rs module doc). GeoLocation always-encodes lat/lon (the (0, 0) "Null Island" default is a real coordinate — proto3 zero-elision would be unsound, same defensive stance as SampleAspectRatio); altitude is presence-encoded (field emitted iff Some, including for Some(0.0)). The buffa feature now implies alloc (string-bearing wire codecs pull in smol_str).
  • Deps — adds icu_locale_core = "2" and bytes = "1" (both optional, gated on the alloc feature; both no_std-friendly). bytes::Bytes backs the audio::CoverArt / audio::Fingerprint payloads so large blobs clone in O(1).

Changes

  • Crate renamevideoframemediaframe, version reset to 0.1.0. The contents are carried over verbatim: the pixel-format / colour / frame vocabulary plus Rational, FrameRate, FieldOrder, StereoMode, DolbyVisionConfig, and SampleAspectRatio represented via Rational. No types, logic, or API changed other than the crate name (and the buffa proto package identifier videoframe.v1mediaframe.v1).
  • Charter broadened — the crate is now a media-stream descriptor vocabulary for video + audio + subtitle, not video-only. Only the existing video vocabulary ships in 0.1.0; audio/subtitle descriptor types will be added incrementally in later releases.

— the following entries are from the crate's videoframe history —

videoframe 0.3.1 — May 19, 2026

Added

  • frameRational (generic exact num/den ratio, NonZeroU32 denominator, 1/1 default), FrameRate (exact fps Rational + is_vfr marker; deliberately not mediatime::Timebase), FieldOrder (FFmpeg AVFieldOrder, lossless Unknown(u32), Unknown(0) default), StereoMode (FFmpeg AVStereo3DType, lossless Unknown(u32), Mono default).
  • colorDolbyVisionConfig (FFmpeg AVDOVIDecoderConfigurationRecord; distinct from the HDR10 static HdrStaticMetadata).
  • buffa — hand-written Message/DefaultInstance wire support for Rational, FrameRate, FieldOrder, StereoMode, DolbyVisionConfig.
  • frameSampleAspectRatioRational interop (SampleAspectRatio::rational/as_rational, From<SampleAspectRatio> for Rational, From<Rational> for SampleAspectRatio).

Breakage

  • frame::SampleAspectRatio — now represented as a newtype over Rational (pub struct SampleAspectRatio(Rational)) instead of its own { num, den } fields, making Rational the single source of truth for "exact ratio with a non-zero denominator". The public method API (new/num/den/is_square/with_*/set_*/ Default/Display/derives) and the buffa wire format are byte-for-byte unchanged; only the internal representation and the From surface (added From<Rational> for SampleAspectRatio, added rational() alongside as_rational()) changed.

videoframe 0.3.0 — May 19, 2026

Added

  • buffa — optional buffa wire serialization for the colour / frame / HDR vocabulary (hand-written Message/DefaultInstance, no codegen); lets downstream proto schemas extern-map .videoframe.v1::videoframe.
  • color/frame — lossless Unknown(u32) catch-all on every colour enum, Rotation, and DcpTargetGamut: unrecognised / future / corrupt wire ids round-trip verbatim instead of collapsing to a default.
  • colorDOMAIN_EXT_BASE + Matrix::Bt601 (videoframe-domain superset id, disjoint from FFmpeg/H.273 codes).
  • color/frameContentLightLevel, ChromaCoord, MasteringDisplay, HdrStaticMetadata (SMPTE ST 2086 / FFmpeg HDR10 static side-data); Rotation; SampleAspectRatio.
  • xtaskcheck verifies colour-enum numbering against the pinned FFmpeg n8.1 header (vendored ffmpeg-color.txt).

Breakage

  • colorPrimaries/Transfer/Matrix/ DynamicRange/ChromaLocation renumbered to exact FFmpeg n8.1 / ITU-T H.273 code points; to_u32/from_u32 now lossless.
  • color::TransferBt470M/Bt470Bg renamed to Gamma22/Gamma28 (FFmpeg-canonical names for the identical transfer code 4/5; slugs / Display unchanged).
  • color::MatrixDefault changed Bt709Unspecified (FFmpeg AVCOL_SPC_UNSPECIFIED); Info default/UNSPECIFIED matrix likewise.
  • color::ChromaCoordx/y widened u16u32 so out-of-range wire values are preserved losslessly (no saturation).
  • frame::Rotation — no longer #[repr(u32)]; gains Unknown(u32).

Changes

  • buffa — standalone-enum codec elides on the type's Default (FFmpeg UNSPECIFIED), not proto3 wire-zero, so code 0 (e.g. Matrix::Rgb) is no longer conflated with "absent".
  • source::xyz12xyz12_to requires a concrete DcpTargetGamut; passing Unknown(_) panics with a descriptive message instead of silently decoding as DCI-P3.

videoframe 0.2.0 — May 12, 2026

Added

  • Add bayer structures

Breakage

  • cfa - remove cfa mod

Changes

  • Make all error enums follows tuple enum errors

videoframe 0.1.0 — May 11, 2026

This is the first release line. Nothing has been published to crates.io yet; everything below describes the shape of the forthcoming 0.1.0.

Added

  • color — ITU-T H.273 enums (Matrix, Primaries, Transfer, DynamicRange, ChromaLocation) bundled into Info. Plus DcpTargetGamut for DCI-XYZ target-gamut selection. Each enum exposes pub const fn as_str() -> &'static str returning the FFmpeg-style wire slug, and a derive_more::Display impl routes through as_str() so the two cannot drift.
  • cfa — Bayer mosaic descriptor (BayerPattern).
  • pixel_format — single PixelFormat enum covering every pixel format in FFmpeg n8.1's AVPixelFormat (254 variants excluding GPU-resident HW formats) plus cinema-RAW additions. ~270 variants total. Unknown(u32) preserves the raw wire value so from_u32(to_u32(x)) == x for every x: u32.
  • frame::Dimensions, frame::Rect, frame::Plane<B> — structural primitives (always available).
  • frame::VideoFrame<P, B> — runtime-tagged frame: dimensions, pixel format P, up to 4 Plane<B>, optional visible-rect crop, Info. No timestamp, no backend extras — pure pixel data. Generic over P (typically PixelFormat) and B (buffer type — &'a [u8] / Vec<u8> / Bytes / refcounted FFmpeg buffer).
  • frame::TimestampedFrame<F> — orthogonal time-carrying wrapper bundling Option<mediatime::Timestamp> PTS + duration around any inner F. Composition over inheritance: pixel data stays independent of any timekeeping convention. Use with VideoFrame<P, B> for runtime-tagged decoder output or with typed *Frame<'a, BE> borrow views for conversion pipelines.
  • Typed *Frame<'a, BE> borrow types (per-family feature-gated) — ~70 zero-copy validated borrow views covering planar YUV (4:2:0 / 4:2:2 / 4:4:4 / 4:4:0 / 4:1:1 / 4:1:0 at 8 / 9 / 10 / 12 / 14 / 16-bit), planar YUVA (same matrix), semi-planar YUV (NV12 / 16 / 21 / 24 / 42 + P010 / 210 / 410 families), packed YUV (YUYV422 / UYVY422 / YVYU422 / UYYVYY411 / V210 / V410 / XV30 / XV36 / AYUV64 / VUYA / VUYX / Y210 / Y212 / Y216), packed RGB (Rgb24 / Bgr24 / Rgba / Bgra / Argb / Abgr / Xrgb / Rgbx / Xbgr / Bgrx / Rgb48 / Bgr48 / Rgba64 / Bgra64 / X2Rgb10 / X2Bgr10), packed RGB float (Rgbf32 / Rgbf16), packed legacy RGB (Rgb444 / 555 / 565 + Bgr counterparts), planar GBR / GBRA at 8 / 9-16 / float, grayscale (Gray8 / 9-16 / f32 / Ya8 / Ya16), Bayer 8 / 10 / 12 / 14 / 16-bit × 4 patterns, Xyz12, and Pal8 / Monoblack / Monowhite. Each *Frame<'a, BE> carries a <const BE: bool = false> parameter selecting endianness; row kernels handle the byte-swap under the hood.
  • source — per-format marker ZSTs (Yuv420p, Nv12, Rgb24, …), *Row<'a> borrow types, *Sink subtraits, and *_to walker fns that iterate Frame → Row → PixelSink. The walker! macro generates the marker / Row / Sink / walker quartet uniformly per format. The companion marker! macro generates the canonical marker shape (pub struct Foo(()) with pub const fn new() constructor — private () field locks shape evolution to additive changes only).
  • PixelSink + SourceFormat sealed traits re-exported at the crate root.
  • xtask — dev-only Cargo subcommand. cargo xtask sync fetches FFmpeg's libavutil/pixfmt.h from the pinned release tag (currently n8.1) and writes the lowercase slug list to xtask/vendor/ffmpeg-pixfmts.txt. cargo xtask check diffs the vendored list against PixelFormat::as_str() and fails on any missing variant. Vendoring only the slug list (not the LGPL header verbatim) sidesteps the license question.

Conventions

  • No public fields anywhere. Every struct exposes private fields via pub const fn getters + pub const fn new(...) constructors
    • #[must_use] with_* consuming builders + set_* in-place setters. Applies to color types, frame primitives, all error payloads, and marker ZSTs.
  • Sealed-trait pattern on SourceFormat: external crates can introspect but not extend the format set.
  • Single-source-of-truth display strings: every enum's Display impl is derived through its pub const fn as_str() — no risk of drift between the two surfaces.
  • derive_more::IsVariant on every enum (color, cfa, pixel_format, every *FrameError). Callers get is_<variant>() predicates for free.

*FrameError shape

All 65 *FrameError enums use newtype-tuple variants wrapping private-field payload structs (no struct-style variants). Pattern:

pub enum FooFrameError {
    Bar(Bar),
    Baz(Baz),
}

Shared error payloads

Common shapes live at the top of videoframe::frame and are reused across every error enum that has the matching shape — variant names carry plane / unit semantics, payload carries shape-only data:

  • ZeroDimension { width, height }
  • DimensionOverflow { width, height }
  • InsufficientStride { stride, min } — wraps every Insufficient*Stride variant across the Y / U / V / A / G / B / R / Uv / Vu plane axes. Variant name conveys per-plane / per-unit semantics.
  • InsufficientPlane { expected, actual } — wraps every Insufficient*Plane variant.
  • GeometryOverflow { stride, rows }
  • OddWidth { width }
  • WidthNotMultipleOf4 { width }
  • WidthOverflow { width }
  • UnsupportedBits { bits }

Naming follows the Insufficient* family rather than the historical *TooShort / *TooSmall style (e.g. InsufficientYPlane, InsufficientYStride).

Rare / unique shapes get local payload structs adjacent to their consumer enum: Yuv420pFrame16SampleOutOfRange, Yuva420pFrame16SampleOutOfRange, Yuva422pFrame16SampleOutOfRange, Yuva444pFrame16SampleOutOfRange, BayerSampleOutOfRange, PnSampleLowBitsSet, Xv36SampleLowBitsSetAt, PnUvStrideOdd.

Display impls

Each payload struct derives thiserror::Error and owns its own #[error("...")] message. Enum variants delegate via #[error(transparent)] — display routes through the payload's own Display impl. Trade-off: per-enum format-identifying prefixes (e.g. "V210Frame: zero dimension width=X height=Y") drop in favor of canonical payload-owned messages; format identity lives on the typed enum (V210FrameError) itself.

Generated accessors

Every *FrameError derives derive_more::{IsVariant, TryUnwrap, Unwrap} with #[unwrap(ref, ref_mut)] + #[try_unwrap(ref, ref_mut)] modifiers. Each variant gets:

  • is_<variant>() -> bool
  • unwrap_<variant>(self) -> Payload
  • unwrap_<variant>_ref(&self) -> &Payload
  • unwrap_<variant>_mut(&mut self) -> &mut Payload
  • try_unwrap_<variant>(self) -> Result<Payload, Self>
  • try_unwrap_<variant>_ref(&self) -> Result<&Payload, &Self>
  • try_unwrap_<variant>_mut(&mut self) -> Result<&mut Payload, &mut Self>

Feature flags

  • default = ["std"]std and alloc features, mediatime, derive_more (is_variant + display), thiserror always pulled in (small, no_std-friendly).

  • Per-family feature flags gate the typed *Frame<'a, BE> validators and the matching source::* walker quartet so consumers compile only the formats they actually use:

    Feature Formats
    yuv-planar Yuv420p / 422p / 444p / 440p / 411p / 410p + 9-16 bit
    yuv-semi-planar NV12 / 16 / 21 / 24 / 42, P010 / 210 / 410 family
    yuva YUVA planar 8-bit + 9-16 bit
    yuv-packed YUYV422, UYVY422, YVYU422, UYYVYY411
    yuv-444-packed V410, XV30, XV36, AYUV64, VUYA, VUYX, V30X
    y2xx Y210 / Y212 / Y216
    v210 V210
    rgb Rgb24/Bgr24/Rgba/Bgra + 10-bit + 16-bit
    rgb-float Rgbf32 / Rgbf16 + Rgbaf16/f32
    rgb-legacy Rgb444 / 555 / 565 + Bgr counterparts
    gbr Gbrp / Gbrap + 9-16 bit + float
    gray Gray8 / 9-16 bit / f32, Ya8 / Ya16
    bayer Bayer 8 / 10 / 12 / 14 / 16-bit × 4 patterns
    xyz Xyz12 (DCI-XYZ)
    mono Monoblack / Monowhite / Pal8
    frame umbrella — enables every sub-feature above

    Deps pulled by family features:

    • halfrgb-float, gbr, gray (for half::f16)
    • derive_more try_unwrap / unwrap features — every per-family feature (so all *FrameError enums get the full unwrap accessor surface).

no_std

Default-feature std is on. --no-default-features builds pure no_std (enums + Copy types + marker ZSTs + frame primitives). Add alloc for the small set of Vec / String helpers used under no_std + alloc. The extern crate alloc as std aliasing pattern keeps std::vec::Vec / std::format! resolving uniformly across feature combos.

Verification matrix

  • Default features: 36 tests
  • --no-default-features --features alloc: 32 tests
  • --features frame: 656 tests
  • All 15 individual per-family standalone builds compile
  • cargo xtask check validates PixelFormat exhaustiveness against vendored FFmpeg n8.1 slugs