All notable changes to this crate are documented here. Format follows Keep a Changelog; the project adheres to Semantic Versioning.
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.
-
frame::Rationalis nowi64/NonZeroI64(wasu32/NonZeroU32), and [Rational::new] is checked rather than total: it panics onnum < 0orden < 0, with a newRational::try_new -> Option<Self>as the fallible form.SampleAspectRatio(a newtype overRational) andFrameRate(which composes it) follow automatically and carry no width of their own;SampleAspectRatio::newpanics the same way, and its fallible route is the existingRational::try_new(..).map(SampleAspectRatio::from).Why
i64, and whymediatime::Timebasestaysi32.mediaframeis a pure receiver — nothing here is handed back to a decoder SDK — so "must round-trip into anAVRational", the reasonTimebasewent toi32, does not apply. What does apply is storage (sqlxhas noType<Postgres>foru32, so au32widens toi64to be stored regardless) and ingest (R3D metadata returnsunsigned int, ISO BMFFpaspisunsigned int(32)— valuesi32would have to reject).Timebaseis additionally an arithmetic operand whose rescale overflow proofs neednum < 2^32;Rationalnever 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 throughnew, so the sign invariants have exactly one enforcement site rather than a mutator hole.Deserializewas the other unguarded construction path — the derive assigns fields directly, and the field types no longer carry the invariant — so each field gained adeserialize_withguard;{"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 declaring2/4reads back as2/4.The wire format does not change.
SampleAspectRatioandRationalmove fromuint32 num/dentoint64 num/den, which is the same plain non-ZigZag varint over every value the old representation could hold. Proven, not inferred: 680 payloads acrossRational,SampleAspectRatioandFrameRate— spanning every varint continuation boundary andu32::MAX— encode to identical bytes under both representations, and thei64build cross-decodes all 680uint32-era payloads back to the same values and the same bytes. (sint32/sint64would 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 to0and a zero-or-negative denominator to1, matchingmediatime::Timebase's decode policy. -
xtask:syn2 → 3,prettyplease0.2 → 0.3 — a coupled bump (prettyplease0.3 requiressyn ^3, so neither moves alone). Dev-only:xtaskispublish = false, so nothing here reaches the publishedmediaframeartifact.syn3's breaking change isSignature::unsafety: Option<Token![unsafe]>→ the tri-stateSignature::safety: Safety(Rust 2024unsafe extern);xtasknames onlysyn::Identandsyn::parse2::<syn::File>and never inspects a signature, so it compiles unchanged.prettyplease0.3 emits byte-identical output to 0.2 for the generatedmediaframe/src/codec.rs(89,303 bytes pre-rustfmt), socargo xtask check's byte-for-byte freshness diff stays green and the committed file needs no regeneration. -
quickcheck-richderive0.3 → 0.4 (quickcheckfeature) — upstream is a dependency-only release (its ownsyn2 → 3 migration); the derive, the accepted attribute keys, and the emitted impls are unchanged. Re-verified against this crate rather than inherited:-Zunpretty=expandedover--features quickcheck,frame,buffa,serde,arbitraryis 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 apub(crate) fn(&mut Gen) -> Tinquickcheck_helpers, so none of them is the siblingwith = "…"key (which names a module supplying botharbitraryandshrink). No consumer-visible change. -
buffa0.8 → 0.9 (buffafeature) —Message::write_tonow takes&mut impl EncodeSinkin place of&mut impl BufMut, so all 26write_tosignatures insrc/buffa.rsmove (the trait method's parameter type is what changed, so keepingBufMutis anE0276"impl has stricter requirements"). Nothing else in the module changes: no body touches aBufMutmethod directly — every byte goes throughbuffa'sencode_*helpers, whose bodies are unchanged — andbuffacarries a blanketimpl<T: BufMut + ?Sized> EncodeSink for T, so every existing caller still passes aVec<u8>/BytesMut. The wire format does not change. Established on this crate's own types rather than inherited: all 37Messageimpls were driven over 400 deterministicarbitraryvalues 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 areaudio::SampleFormatonly, are present identically in both runs, and are the documentedOther(SmolStr)→Unknown(u32::MAX)collapse, not a regression.EncodeSink's segmentedRopesink is not adopted here. -
mediatime0.1 → 0.2 —mediatime::Timebase'snum/denbecamei32/NonZeroI32(matching ffmpeg'sAVRational, which is{int num; int den;}),Timebase::newnow panics on a negative numerator or denominator withtry_newreturningOption, and itsDeserializegained a range guard. The surface this crate touches is small:mediatime::Timestamp— notTimebase— is whatframe::TimestampedFramecarries, andTimestamp::new(i64, Timebase)is unchanged, so the single site that moves is one test'sNonZeroU32→NonZeroI32denominator literal. Every otherTimebasemention in this crate is prose, and each statement it makes (non-proto-zero1/1default; a frame rate is deliberately not a PTS timebase) is still true of 0.2. Also collapses the transient duplicate from the previous commit:mediatime0.2 requiresbuffa0.9, so the graph carries onebuffaagain.
Primaries::chromaticities()/Primaries::white_point()—const fns exposing the per-standard CIE 1931xyreference data for each definedPrimariesvariant: the R, G, B primaries asOption<[ChromaCoord; 3]>(index0= red,1= green,2= blue) and the reference white point asOption<ChromaCoord>, both inChromaCoord'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'sav_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.UnknownandUnspecifiedreturnNone(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.
PixelFormat::V410Be— first-class big-endian counterpart ofV410Lefor the packed YUV 4:4:4 10-bitV410layout (one 32-bit word per sample). The big-endian decode path already existed — theV410Frame<'a, true>/V410BeFrameborrow view, theV410<true>source marker, and the endian-genericv410_to::<true>walker — and is now exposed as a wire-stable enum variant (as_str()slug"v410be", discriminant435). Additive and non-breaking.PixelFormat::canonical()—const fnresolving 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 (orNonewhen 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 theyuvj{411,420,422,440,444}pfull-range aliases → theiryuv*pbase +DynamicRange::Full,Gray8a/Y400a→Ya8, and theXV30byte-order pair onto its matchingV410variant —Xv30Le→V410LeandXv30Be→V410Be(XV30is the FFmpeg rename of the identical-bit-patternV410; 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 (includingUnknown) returns(self, None).
- Pixel-format source coverage — frame types, source markers, and
{fmt}_towalkers 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 ofP210(oneu16per sample with the 10 active bits in the low positions). - Gray family (
gray) —Gray32(32-bit),Grayf16(f16),Yaf16/Yaf32(f16/f32gray + 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/f32RGBA). - 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/Rgb8andBgr4/Bgr4Byte/Bgr8. Xv48(yuv-444-packed) — 16-bit packed YUV 4:4:4 (FFmpegAV_PIX_FMT_XV48LE/BE); the full-16-bit sibling ofXv36.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.
- NV20 (
- High-bit Bayer is now endian-aware (
bayer) — the Bayer source marker gains a trailingconst BE: bool = false(source-compatible default), mirroring theY2xxfamily, 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 forBE = false, BE forBE = 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.
audio::ReplayGain— value object for container-tagged loudness- normalization recommendations (FFmpegAV_PKT_DATA_REPLAYGAINside data or theREPLAYGAIN_TRACK_*/REPLAYGAIN_ALBUM_*AVDictionarykeys). Carriestrack_gain_db,track_peak, and the optional album-levelalbum_gain_db/album_peak. Distinct from [audio::Loudness]:Loudnessis the EBU R128 measurement of the signal;ReplayGainis 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 throughquickcheck_helpers::composite::replay_gain+arbitrary_impls::composite.
serdefeature — optionalserde::{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-jsonvalue matches their representation:- Open codec / format enums (
codec::{Video,Audio,Subtitle}Codec,container::Format,subtitle::Format,audio::{ChannelLayout, ContainerFormat}) serialize as their canonicalas_str()slug —VideoCodec::H264⇄"h264",Other("x265")⇄"x265"(no{"Other": …}wrapper). audio::SampleFormat— has BOTH anUnknown(u32)numeric escape AND anOther(SmolStr)string escape, so it gets a bespoke impl rather than the slug-only path. On human-readable formats (JSON / YAML / …): named +Othervalues serialize as theiras_str()string,Unknown(v)as the bare numeric codev. On non-human-readable binary formats (bincode / postcard / …): an explicit tagged{Code(u32), Slug(Cow<str>)}wire enum, sincedeserialize_anyis 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}) anddisposition::TrackDispositionserialize as theirto_u32()integer. Round-trip is total: an unrecognised code deserializes toUnknown(v). These accept only integers — there is no slug form. - Strictly-closed coded enums (no
Unknownarm) —subtitle::TrackOrigin(Embedded/Sidecar/External) andaudio::BitRateMode(Cbr/Vbr/Abr) — serialize as theirto_u32()integer but reject unrecognised wire codes as serde errors instead of silently collapsing them to the default variant. Both expose atry_from_u32(v: u32) -> Option<Self>method backing this behavior. - Plain structs (
color::Infoand 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::Languageserializes 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 withalloc/std(forwardingserdetosmol_str/bytes).
- Open codec / format enums (
Initial mediaframe release — this crate is a rename of the
videoframe crate. It was previously published as videoframe
(version line 0.1.x–0.3.x); those videoframe crates.io versions
are being yanked and superseded by mediaframe 0.1.0 (fresh crate
identity).
audiomodule — first cut of the audio-stream descriptor vocabulary (audio + container cluster of the0.1.0stream-vocab expansion):audio::ChannelLayout—#[non_exhaustive]closed enum of common FFmpegAV_CH_LAYOUT_*shapes (Mono,Stereo,_2_1through_7_1with*Backside-vs-back variants,Hexagonal,Octagonal,Ambisonic1/2/3) plusOther(SmolStr)lossless escape;as_str()returns the FFmpeg-canonical slug,FromStris total.audio::BitRateMode— closedCbr/Vbr/Abrtrichotomy (defaultCbr),to_u32/from_u32for the wire codec.audio::SampleFormat— sample-format vocabulary mirroring FFmpegAVSampleFormat(U8/S16/S32/S64/Flt/Dblpacked + their*pplanar twins), losslessUnknown(u32)+Other(SmolStr)escapes,to_u32/from_u32per FFmpegAV_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) plusOther(SmolStr).audio::Loudness— EBU R128 / ITU-R BS.1770 measurement value object (integrated_lufs,range_lu,true_peak_dbtp,sample_peak_dbfs— allf32; noEq/Hash).audio::Fingerprint— algorithm-tagged opaque bytes ({ algorithm: SmolStr, value: bytes::Bytes }— O(1) clone),try_newrejects empty algorithm.audio::CoverArt— embedded picture ({ mime: SmolStr, data: bytes::Bytes }— O(1) clone),try_newrejects 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 toOption<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—.3gpdigit- prefix-renamed) plusOther(SmolStr); audio-only containers live on [audio::ContainerFormat].subtitlemodule —Format(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/ totalFromStrround-trip;is_image_basedhelper for mediaschema'sREQUIRES_OCRderivation) andTrackOrigin(closed unit-only enum —Embedded/Sidecar/External; stableto_u32/from_u32ids0/1/2;Default == Embedded). The module is gated on theallocfeature for theOther(SmolStr)escape.disposition::TrackDisposition— FFmpegAV_DISPOSITION_*bitflags fromlibavformat/avformat.hn8.1 (u32backing). Shared across video / audio / subtitle tracks; ports the placeholder that used to live inmediaschema::domain::bitflags.to_u32/from_u32aliases forbits/from_bits_retainso unknown bits round-trip losslessly.capturemodule (alloc-gated) — EXIF / capture-metadata vocabulary.Device { make, model }(privateSmolStrfields; empty string means absent, neverOption<SmolStr>; builders / setters /is_empty).GeoLocation { lat: f64, lon: f64, altitude: Option<f32> }with range-validatingtry_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 viaGeoLocationError::{LatOutOfRange, LonOutOfRange, Iso6709Malformed}.
lang::Language(alloc-gated) — validated BCP-47 language tag wrappingicu_locale_coreLanguage/Script/Regionsubtags (Copy, heap-free in-rust representation; theto_bcp47() -> String/Displaysurface 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-writtenMessage/DefaultInstancewire support for every new type (see the## Audio + container types,## Subtitle + disposition, and## Capture + languagesub- sections of thebuffa.rsmodule doc).GeoLocationalways-encodeslat/lon(the(0, 0)"Null Island" default is a real coordinate — proto3 zero-elision would be unsound, same defensive stance asSampleAspectRatio);altitudeis presence-encoded (field emitted iffSome, including forSome(0.0)). Thebuffafeature now impliesalloc(string-bearing wire codecs pull insmol_str).- Deps — adds
icu_locale_core = "2"andbytes = "1"(both optional, gated on theallocfeature; bothno_std-friendly).bytes::Bytesbacks theaudio::CoverArt/audio::Fingerprintpayloads so large blobs clone in O(1).
- Crate rename —
videoframe→mediaframe, version reset to0.1.0. The contents are carried over verbatim: the pixel-format / colour / frame vocabulary plusRational,FrameRate,FieldOrder,StereoMode,DolbyVisionConfig, andSampleAspectRatiorepresented viaRational. No types, logic, or API changed other than the crate name (and thebuffaproto package identifiervideoframe.v1→mediaframe.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 —
frame—Rational(generic exactnum/denratio,NonZeroU32denominator,1/1default),FrameRate(exact fpsRational+is_vfrmarker; deliberately notmediatime::Timebase),FieldOrder(FFmpegAVFieldOrder, losslessUnknown(u32),Unknown(0)default),StereoMode(FFmpegAVStereo3DType, losslessUnknown(u32),Monodefault).color—DolbyVisionConfig(FFmpegAVDOVIDecoderConfigurationRecord; distinct from the HDR10 staticHdrStaticMetadata).buffa— hand-writtenMessage/DefaultInstancewire support forRational,FrameRate,FieldOrder,StereoMode,DolbyVisionConfig.frame—SampleAspectRatio→Rationalinterop (SampleAspectRatio::rational/as_rational,From<SampleAspectRatio> for Rational,From<Rational> for SampleAspectRatio).
frame::SampleAspectRatio— now represented as a newtype overRational(pub struct SampleAspectRatio(Rational)) instead of its own{ num, den }fields, makingRationalthe 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 thebuffawire format are byte-for-byte unchanged; only the internal representation and theFromsurface (addedFrom<Rational> for SampleAspectRatio, addedrational()alongsideas_rational()) changed.
buffa— optionalbuffawire serialization for the colour / frame / HDR vocabulary (hand-writtenMessage/DefaultInstance, no codegen); lets downstream proto schemas extern-map.videoframe.v1→::videoframe.color/frame— losslessUnknown(u32)catch-all on every colour enum,Rotation, andDcpTargetGamut: unrecognised / future / corrupt wire ids round-trip verbatim instead of collapsing to a default.color—DOMAIN_EXT_BASE+Matrix::Bt601(videoframe-domain superset id, disjoint from FFmpeg/H.273 codes).color/frame—ContentLightLevel,ChromaCoord,MasteringDisplay,HdrStaticMetadata(SMPTE ST 2086 / FFmpeg HDR10 static side-data);Rotation;SampleAspectRatio.- xtask —
checkverifies colour-enum numbering against the pinned FFmpeg n8.1 header (vendoredffmpeg-color.txt).
color—Primaries/Transfer/Matrix/DynamicRange/ChromaLocationrenumbered to exact FFmpeg n8.1 / ITU-T H.273 code points;to_u32/from_u32now lossless.color::Transfer—Bt470M/Bt470Bgrenamed toGamma22/Gamma28(FFmpeg-canonical names for the identical transfer code 4/5; slugs /Displayunchanged).color::Matrix—DefaultchangedBt709→Unspecified(FFmpegAVCOL_SPC_UNSPECIFIED);Infodefault/UNSPECIFIEDmatrixlikewise.color::ChromaCoord—x/ywidenedu16→u32so out-of-range wire values are preserved losslessly (no saturation).frame::Rotation— no longer#[repr(u32)]; gainsUnknown(u32).
buffa— standalone-enum codec elides on the type'sDefault(FFmpegUNSPECIFIED), not proto3 wire-zero, so code0(e.g.Matrix::Rgb) is no longer conflated with "absent".source::xyz12—xyz12_torequires a concreteDcpTargetGamut; passingUnknown(_)panics with a descriptive message instead of silently decoding as DCI-P3.
- Add bayer structures
cfa- remove cfa mod
- Make all error enums follows tuple enum errors
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.
color— ITU-T H.273 enums (Matrix,Primaries,Transfer,DynamicRange,ChromaLocation) bundled intoInfo. PlusDcpTargetGamutfor DCI-XYZ target-gamut selection. Each enum exposespub const fn as_str() -> &'static strreturning the FFmpeg-style wire slug, and aderive_more::Displayimpl routes throughas_str()so the two cannot drift.cfa— Bayer mosaic descriptor (BayerPattern).pixel_format— singlePixelFormatenum covering every pixel format in FFmpegn8.1'sAVPixelFormat(254 variants excluding GPU-resident HW formats) plus cinema-RAW additions. ~270 variants total.Unknown(u32)preserves the raw wire value sofrom_u32(to_u32(x)) == xfor everyx: u32.frame::Dimensions,frame::Rect,frame::Plane<B>— structural primitives (always available).frame::VideoFrame<P, B>— runtime-tagged frame: dimensions, pixel formatP, up to 4Plane<B>, optional visible-rect crop,Info. No timestamp, no backend extras — pure pixel data. Generic overP(typicallyPixelFormat) andB(buffer type —&'a [u8]/Vec<u8>/Bytes/ refcounted FFmpeg buffer).frame::TimestampedFrame<F>— orthogonal time-carrying wrapper bundlingOption<mediatime::Timestamp>PTS + duration around any innerF. Composition over inheritance: pixel data stays independent of any timekeeping convention. Use withVideoFrame<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,*Sinksubtraits, and*_towalker fns that iterate Frame → Row →PixelSink. Thewalker!macro generates the marker / Row / Sink / walker quartet uniformly per format. The companionmarker!macro generates the canonical marker shape (pub struct Foo(())withpub const fn new()constructor — private()field locks shape evolution to additive changes only).PixelSink+SourceFormatsealed traits re-exported at the crate root.xtask— dev-only Cargo subcommand.cargo xtask syncfetches FFmpeg'slibavutil/pixfmt.hfrom the pinned release tag (currentlyn8.1) and writes the lowercase slug list toxtask/vendor/ffmpeg-pixfmts.txt.cargo xtask checkdiffs the vendored list againstPixelFormat::as_str()and fails on any missing variant. Vendoring only the slug list (not the LGPL header verbatim) sidesteps the license question.
- No public fields anywhere. Every struct exposes private fields
via
pub const fngetters +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
Displayimpl is derived through itspub const fn as_str()— no risk of drift between the two surfaces. derive_more::IsVarianton every enum (color, cfa, pixel_format, every*FrameError). Callers getis_<variant>()predicates for free.
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),
}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 everyInsufficient*Stridevariant 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 everyInsufficient*Planevariant.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.
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.
Every *FrameError derives derive_more::{IsVariant, TryUnwrap, Unwrap} with #[unwrap(ref, ref_mut)] + #[try_unwrap(ref, ref_mut)] modifiers. Each variant gets:
is_<variant>() -> boolunwrap_<variant>(self) -> Payloadunwrap_<variant>_ref(&self) -> &Payloadunwrap_<variant>_mut(&mut self) -> &mut Payloadtry_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>
-
default = ["std"]—stdandallocfeatures, 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 matchingsource::*walker quartet so consumers compile only the formats they actually use:Feature Formats yuv-planarYuv420p / 422p / 444p / 440p / 411p / 410p + 9-16 bit yuv-semi-planarNV12 / 16 / 21 / 24 / 42, P010 / 210 / 410 family yuvaYUVA planar 8-bit + 9-16 bit yuv-packedYUYV422, UYVY422, YVYU422, UYYVYY411 yuv-444-packedV410, XV30, XV36, AYUV64, VUYA, VUYX, V30X y2xxY210 / Y212 / Y216 v210V210 rgbRgb24/Bgr24/Rgba/Bgra + 10-bit + 16-bit rgb-floatRgbf32 / Rgbf16 + Rgbaf16/f32 rgb-legacyRgb444 / 555 / 565 + Bgr counterparts gbrGbrp / Gbrap + 9-16 bit + float grayGray8 / 9-16 bit / f32, Ya8 / Ya16 bayerBayer 8 / 10 / 12 / 14 / 16-bit × 4 patterns xyzXyz12 (DCI-XYZ) monoMonoblack / Monowhite / Pal8 frameumbrella — enables every sub-feature above Deps pulled by family features:
half—rgb-float,gbr,gray(forhalf::f16)derive_moretry_unwrap/unwrapfeatures — every per-family feature (so all*FrameErrorenums get the full unwrap accessor surface).
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.
- 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 checkvalidatesPixelFormatexhaustiveness against vendored FFmpegn8.1slugs