diff --git a/.github/workflows/ci-ffmpeg.yml b/.github/workflows/ci-ffmpeg.yml index 79d68c2..3ec968c 100644 --- a/.github/workflows/ci-ffmpeg.yml +++ b/.github/workflows/ci-ffmpeg.yml @@ -70,9 +70,10 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ runner.os }}-ffmpeg-build-${{ hashFiles('**/Cargo.lock') }} + # See the `test` job below for the `-v2-` prefix rationale. + key: ${{ runner.os }}-ffmpeg-build-v2-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-ffmpeg-build- + ${{ runner.os }}-ffmpeg-build-v2- - name: Install FFmpeg run: brew install ffmpeg pkg-config - name: Install Rust @@ -98,15 +99,32 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ runner.os }}-ffmpeg-test-${{ hashFiles('**/Cargo.lock') }} + # `-v2-` prefix bump is a one-shot cache invalidation — + # earlier runs cached `ffmpeg-sys-next` artifacts that + # had baked in a stale brew FFmpeg path + # (`/opt/homebrew/Cellar/ffmpeg//lib`). + # The runner now has a different patch version, so the + # linker fails with `library 'avutil' not found`. Once + # the new cache populates, the explicit clean step + # below keeps it fresh on every run. + key: ${{ runner.os }}-ffmpeg-test-v2-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-ffmpeg-test- + ${{ runner.os }}-ffmpeg-test-v2- - name: Install FFmpeg run: brew install ffmpeg pkg-config - name: Install Rust run: rustup update stable && rustup default stable - name: Install cargo-hack run: cargo install cargo-hack + - name: Refresh ffmpeg-sys-next build artifacts + # `ffmpeg-sys-next`'s `build.rs` bakes the brew-installed + # `/opt/homebrew/Cellar/ffmpeg//lib` path into its + # linker hints. When brew bumps FFmpeg between runs the + # cached output points at a path that no longer exists and + # the linker fails. Force a rebuild of the sys crate (and + # its downstream binding) so the build script re-reads + # `pkg-config` against the currently-installed FFmpeg. + run: cargo clean -p ffmpeg-sys-next -p ffmpeg-next -p mediadecode-ffmpeg - name: Run test run: cargo hack test -p mediadecode-ffmpeg --feature-powerset --exclude-no-default-features @@ -136,9 +154,13 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ runner.os }}-ffmpeg-coverage-${{ hashFiles('**/Cargo.lock') }} + # See the `test` job above for the `-v2-` prefix + clean + # step rationale. Same brew-ffmpeg-path drift bites here. + key: ${{ runner.os }}-ffmpeg-coverage-v2-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-ffmpeg-coverage- + ${{ runner.os }}-ffmpeg-coverage-v2- + - name: Refresh ffmpeg-sys-next build artifacts + run: cargo clean -p ffmpeg-sys-next -p ffmpeg-next -p mediadecode-ffmpeg - name: Run tarpaulin env: RUSTFLAGS: "--cfg tarpaulin" diff --git a/Cargo.toml b/Cargo.toml index e678356..14f04a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,8 @@ homepage = "https://github.com/findit-ai/mediadecode" [workspace.dependencies] # Workspace-internal crates — referenced by sibling members via workspace inheritance. -mediadecode = { version = "0.1.0", path = "mediadecode", default-features = false } +mediadecode = { version = "0.2", path = "mediadecode", default-features = false } +videoframe = { version = "0.2", default-features = false, features = ["frame"] } # External deps used by more than one member, pinned at the workspace level. mediatime = { version = "0.1", default-features = false } diff --git a/README.md b/README.md index 1ca20b9..5eb688e 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,16 @@ Generic, `no_std`-friendly type-and-trait spine for media decoders. ## Crates -| Crate | Role | -| ---------------------------------------------------- | ------------------------------------------------------------- | -| [`mediadecode`](mediadecode/README.md) | Backend-agnostic core — `no_std`, no FFmpeg. | -| [`mediadecode-ffmpeg`](mediadecode-ffmpeg/README.md) | FFmpeg adapter with HW-acceleration auto-probe. | +| Crate | Role | +| ---------------------------------------------------------- | ------------------------------------------------------------- | +| [`mediadecode`](mediadecode/README.md) | Backend-agnostic core — `no_std`, no FFmpeg. | +| [`mediadecode-ffmpeg`](mediadecode-ffmpeg/README.md) | FFmpeg adapter with HW-acceleration auto-probe. | +| [`mediadecode-webcodecs`](mediadecode-webcodecs/README.md) | WebCodecs adapter for `wasm32` targets (scaffolded). | + +The pixel-format and color-metadata vocabulary (`PixelFormat`, +`ColorMatrix`, `BayerPattern`, frame primitives) is sourced from +[`videoframe`](https://crates.io/crates/videoframe) so colconv, +mediadecode, and scenesdetect share one canonical definition. See each crate's README for its API surface, usage, and build requirements. diff --git a/mediadecode-ffmpeg/CHANGELOG.md b/mediadecode-ffmpeg/CHANGELOG.md index 2e3d8c2..a830026 100644 --- a/mediadecode-ffmpeg/CHANGELOG.md +++ b/mediadecode-ffmpeg/CHANGELOG.md @@ -11,6 +11,71 @@ The backend-agnostic core it adapts has its own log at ## [Unreleased] +## [0.2.0] - 2026-05-15 + +Tracks `mediadecode` 0.2.0. The pixel-vocabulary types +(`PixelFormat`, color enums, frame primitives) now live in the +`videoframe` crate and are re-exported through `mediadecode`; this +release adapts the FFmpeg boundary to the new `PixelFormat::Unknown(u32)` +shape and updates the type aliases the crate re-exports. + +### Changed (BREAKING) + +- **`PixelFormat::Unknown` shape**: re-exported `PixelFormat` is now + `Unknown(u32)` (tuple variant) instead of the prior unit variant + — see [`mediadecode` 0.2.0](../mediadecode/CHANGELOG.md#020---2026-05-15). +- **FFmpeg boundary fallback** now preserves the raw `AVPixelFormat` + identifier through `PixelFormat::Unknown(raw as u32)` instead of + collapsing to a bare `Unknown`. Round-trips losslessly via + `PixelFormat::{from_u32, to_u32}`. +- **Type aliases reshape**: `VideoFrame`, `AudioFrame`, + `SubtitleFrame`, `VideoPacket`, `AudioPacket`, `SubtitlePacket` + inherit the upstream `PixelFormat` shape change. Downstream + callers matching on `Unknown` in destination frames need to + switch to `Unknown(_)`. +- **`Error` enum variants** are now newtype-tuple form wrapping + payload structs (matches the convention in + [`videoframe`](https://crates.io/crates/videoframe)). Affected + variants: `HwDeviceInitFailed`, `AllBackendsFailed`, + `FallbackFailed`. Pure tuple variants (`Ffmpeg`, `NoCodec`, + `BackendUnsupportedByCodec`) unchanged. Callers destructuring + `Err(Error::AllBackendsFailed { attempts, .. })` must switch to + `Err(Error::AllBackendsFailed(p))` and call `p.attempts()` / + `p.unconsumed_packets()`. Owning-move paths for the rescued + packets are preserved via `p.into_unconsumed_packets()` / + `p.into_parts()`, so non-seekable callers can still relinquish + the `Vec` without cloning. The hand-written `Debug` that + printed `[N packets]` (because `ffmpeg_next::Packet` has no + `Debug`) now lives on the payload structs. + All three new variants also carry `#[from]`, joining `Ffmpeg` + which already had it — so `impl From for Error`, + `impl From for Error`, and + `impl From for Error` are auto-generated, and + helpers returning `Result<_, HwDeviceInitFailed>` etc. can be + `?`-propagated into `Result<_, Error>` directly. + +### Changed + +- **`mediadecode` dep**: bumped to `0.2`. +- Boundary mapping in `pixel_format_from_ffmpeg` and the + side-data conversion paths updated to the new + `PixelFormat::Unknown(u32)` shape (17 fallback / assertion / + default-frame sites across `mediadecode-ffmpeg` and + `mediadecode-webcodecs`). + +### Added + +- **`Debug` impl for `Frame`** — manual `core::fmt::Debug` impl + showing dimensions / format so the only public type previously + without `Debug` is now printable. + Closes [issue #4 — finding 2](https://github.com/Findit-AI/mediadecode/issues/4). +- **`#[must_use]`** on every consuming `with_*` builder method + across the crate's public surface. + Closes [issue #4 — finding 3](https://github.com/Findit-AI/mediadecode/issues/4). + +[0.1.0]: https://github.com/findit-ai/mediadecode/releases/tag/mediadecode-ffmpeg-v0.1.0 +[0.2.0]: https://github.com/findit-ai/mediadecode/releases/tag/mediadecode-ffmpeg-v0.2.0 + ## [0.1.0] - 2026-05-09 Initial public release. @@ -74,5 +139,3 @@ version-skewed decoder output: rather than `linesize × plane_height_for(AVFrame.height)`, so cropped or heavily aligned streams report correct byte counts. -[Unreleased]: https://github.com/findit-ai/mediadecode/compare/mediadecode-ffmpeg-v0.1.0...HEAD -[0.1.0]: https://github.com/findit-ai/mediadecode/releases/tag/mediadecode-ffmpeg-v0.1.0 diff --git a/mediadecode-ffmpeg/Cargo.toml b/mediadecode-ffmpeg/Cargo.toml index 94c1181..66df99f 100644 --- a/mediadecode-ffmpeg/Cargo.toml +++ b/mediadecode-ffmpeg/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mediadecode-ffmpeg" -version = "0.1.0" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/mediadecode-ffmpeg/README.md b/mediadecode-ffmpeg/README.md index f1dec70..4545f8e 100644 --- a/mediadecode-ffmpeg/README.md +++ b/mediadecode-ffmpeg/README.md @@ -51,10 +51,11 @@ Output frames are CPU-side, downloaded with `av_hwframe_transfer_data` If every HW backend opens but later fails at decode time and the software backend is also unavailable, the error surfaces as -`VideoDecodeError::Decode(Error::AllBackendsFailed { unconsumed_packets, .. })` -carrying any packets the decoder had already accepted from the demuxer -— so non-seekable callers (live streams, pipes, network sources) can -replay them through their own software decoder without re-demuxing. +`VideoDecodeError::Decode(Error::AllBackendsFailed(p))` carrying any +packets the decoder had already accepted from the demuxer (accessible +via `p.unconsumed_packets()` / `p.into_unconsumed_packets()`) — so +non-seekable callers (live streams, pipes, network sources) can replay +them through their own software decoder without re-demuxing. ## Usage @@ -82,10 +83,10 @@ fn main() -> Result<(), Box> { // Probes HW backends in order, falls back to software. let mut decoder = match FfmpegVideoStreamDecoder::open(stream.parameters(), time_base) { Ok(d) => d, - Err(FfmpegError::AllBackendsFailed { unconsumed_packets, .. }) => { + Err(FfmpegError::AllBackendsFailed(p)) => { // No backend at all could open this stream — including software. // `unconsumed_packets` is empty at open-time. Caller decides. - let _ = unconsumed_packets; + let _unconsumed_packets = p.into_unconsumed_packets(); return Ok(()); } Err(e) => return Err(e.into()), @@ -98,14 +99,12 @@ fn main() -> Result<(), Box> { match decoder.send_packet(&pkt) { Ok(()) => {} - Err(VideoDecodeError::Decode(FfmpegError::AllBackendsFailed { - unconsumed_packets, .. - })) => { + Err(VideoDecodeError::Decode(FfmpegError::AllBackendsFailed(p))) => { // Runtime exhaustion: rescued packets are the bytes the decoder // already consumed from `input`. Replay them through your own // software decoder before the current packet so non-seekable // sources recover cleanly. - let _ = unconsumed_packets; + let _unconsumed_packets = p.into_unconsumed_packets(); return Ok(()); } Err(e) => return Err(e.into()), diff --git a/mediadecode-ffmpeg/benches/decode.rs b/mediadecode-ffmpeg/benches/decode.rs index 8c07217..c96d6e7 100644 --- a/mediadecode-ffmpeg/benches/decode.rs +++ b/mediadecode-ffmpeg/benches/decode.rs @@ -217,7 +217,7 @@ fn bench_decode(c: &mut Criterion) { } Some(dec.backend()) } - Err(mediadecode_ffmpeg::Error::AllBackendsFailed { .. }) => None, + Err(mediadecode_ffmpeg::Error::AllBackendsFailed(_)) => None, Err(e) => panic!("mediadecode-ffmpeg HW probe: {e}"), } }; diff --git a/mediadecode-ffmpeg/examples/decode.rs b/mediadecode-ffmpeg/examples/decode.rs index c1a2d29..d6d2a25 100644 --- a/mediadecode-ffmpeg/examples/decode.rs +++ b/mediadecode-ffmpeg/examples/decode.rs @@ -24,12 +24,13 @@ fn main() -> Result<(), Box> { let mut decoder = match VideoDecoder::open(stream.parameters()) { Ok(d) => d, - Err(mediadecode_ffmpeg::Error::AllBackendsFailed { attempts, .. }) => { + Err(mediadecode_ffmpeg::Error::AllBackendsFailed(p)) => { + let attempts = p.attempts(); eprintln!( "no hardware backend available; tried {} backend(s):", attempts.len() ); - for (b, e) in &attempts { + for (b, e) in attempts { eprintln!(" {b:?}: {e}"); } eprintln!("(callers handle software fallback themselves — see ffmpeg::decoder::Video)"); diff --git a/mediadecode-ffmpeg/src/boundary.rs b/mediadecode-ffmpeg/src/boundary.rs index 743f888..e662252 100644 --- a/mediadecode-ffmpeg/src/boundary.rs +++ b/mediadecode-ffmpeg/src/boundary.rs @@ -126,7 +126,7 @@ pub const fn from_av_pixel_format(raw: i32) -> PixelFormat { x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB16LE as i32 => PixelFormat::BayerRggb16Le, x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG16LE as i32 => PixelFormat::BayerGbrg16Le, x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG16LE as i32 => PixelFormat::BayerGrbg16Le, - _ => PixelFormat::Unknown, + _ => PixelFormat::Unknown(raw as u32), } } @@ -410,7 +410,7 @@ pub fn try_empty_video_frame() -> Option Self { self.max_probe_pending_bytes = bytes; self @@ -502,10 +503,10 @@ impl VideoDecoder { "hwdecode: probe rescue exhausted before consuming packet; \ returning AllBackendsFailed without invoking decoder" ); - return Err(Error::AllBackendsFailed { - attempts: probe.attempts, - unconsumed_packets: probe.buffered_packets, - }); + return Err(Error::AllBackendsFailed(AllBackendsFailed::new( + probe.attempts, + probe.buffered_packets, + ))); } // Step 2: byte / packet count cap. `packet_side_data_bytes` // clamps its walk to MAX_PROBE_PACKET_SIDE_DATA_ENTRIES as @@ -529,10 +530,10 @@ impl VideoDecoder { "hwdecode: probe rescue exhausted before consuming packet; \ returning AllBackendsFailed without invoking decoder" ); - return Err(Error::AllBackendsFailed { - attempts: probe.attempts, - unconsumed_packets: probe.buffered_packets, - }); + return Err(Error::AllBackendsFailed(AllBackendsFailed::new( + probe.attempts, + probe.buffered_packets, + ))); } // Step 3: pre-clone before consuming. `av_packet_ref` is a // refcounted shallow clone (no payload deep-copy) but can still @@ -547,10 +548,10 @@ impl VideoDecoder { "hwdecode: packet clone failed before consuming; \ returning AllBackendsFailed without invoking decoder" ); - return Err(Error::AllBackendsFailed { - attempts: probe.attempts, - unconsumed_packets: probe.buffered_packets, - }); + return Err(Error::AllBackendsFailed(AllBackendsFailed::new( + probe.attempts, + probe.buffered_packets, + ))); } } } else { @@ -764,8 +765,9 @@ impl VideoDecoder { /// Returns: /// - `Ok(())` when a candidate is installed and replay completed — /// caller should retry the operation. - /// - `Err(Error::AllBackendsFailed { attempts })` when every remaining + /// - `Err(Error::AllBackendsFailed(p))` when every remaining /// backend has been exhausted (including the just-failed active one). + /// `p.attempts()` carries the per-backend failure log. /// This is what the documented `open` contract promises, surfaced at /// runtime so the caller can branch into a software fallback. On a /// single-backend platform (e.g. macOS), this fires after the only @@ -841,10 +843,10 @@ impl VideoDecoder { .take() .map(|p| (p.attempts, p.buffered_packets)) .unwrap_or_default(); - return Err(Error::AllBackendsFailed { + return Err(Error::AllBackendsFailed(AllBackendsFailed::new( attempts, unconsumed_packets, - }); + ))); } }; @@ -1021,10 +1023,10 @@ impl VideoDecoder { av_hwdevice_ctx_create(&mut hw_device_ref, av_type, ptr::null(), ptr::null_mut(), 0) }; if ret < 0 { - return Err(Error::HwDeviceInitFailed { + return Err(Error::HwDeviceInitFailed(HwDeviceInitFailed::new( backend, - source: ffmpeg_next::Error::from(ret), - }); + ffmpeg_next::Error::from(ret), + ))); } let callback_state = Box::into_raw(Box::new(CallbackState { @@ -2603,10 +2605,9 @@ mod tests { continue; } match decoder.send_packet(&packet) { - Err(Error::AllBackendsFailed { - attempts, - unconsumed_packets, - }) => { + Err(Error::AllBackendsFailed(p)) => { + let attempts = p.attempts(); + let unconsumed_packets = p.unconsumed_packets(); assert_eq!( unconsumed_packets.len(), 1, @@ -2697,10 +2698,9 @@ mod tests { // buffered packets and surface them via `unconsumed_packets`. let result = decoder.advance_probe(Error::Ffmpeg(ffmpeg_next::Error::InvalidData)); match result { - Err(Error::AllBackendsFailed { - attempts, - unconsumed_packets, - }) => { + Err(Error::AllBackendsFailed(p)) => { + let attempts = p.attempts(); + let unconsumed_packets = p.unconsumed_packets(); assert_eq!( unconsumed_packets.len(), 2, @@ -2785,7 +2785,8 @@ mod tests { let result = decoder.advance_probe(Error::Ffmpeg(ffmpeg_next::Error::InvalidData)); match result { - Err(Error::AllBackendsFailed { attempts, .. }) => { + Err(Error::AllBackendsFailed(p)) => { + let attempts = p.attempts(); assert_eq!( attempts.len(), 2, diff --git a/mediadecode-ffmpeg/src/error.rs b/mediadecode-ffmpeg/src/error.rs index 88afc2f..69baeed 100644 --- a/mediadecode-ffmpeg/src/error.rs +++ b/mediadecode-ffmpeg/src/error.rs @@ -7,12 +7,16 @@ pub type Result = std::result::Result; /// Errors returned from [`crate::VideoDecoder`]. /// -/// `Debug` is implemented manually because [`ffmpeg_next::Packet`] -/// (carried by `AllBackendsFailed::unconsumed_packets`) does not -/// derive `Debug`. The hand-written impl summarizes the packet count -/// rather than dumping each packet's fields, which would be both -/// noisy and useless for triage. -#[derive(thiserror::Error)] +/// `Debug` is derived; the variants that wrap a payload struct +/// (`HwDeviceInitFailed`, `AllBackendsFailed`, `FallbackFailed`) +/// delegate their `Debug` to the payload, which is hand-written +/// where needed because [`ffmpeg_next::Packet`] (carried by +/// `AllBackendsFailed::unconsumed_packets` / +/// `FallbackFailed::unconsumed_packets`) does not derive +/// `Debug`. Those payloads summarize the packet count rather +/// than dumping each packet's fields, which would be both noisy +/// and useless for triage. +#[derive(Debug, thiserror::Error)] pub enum Error { /// An underlying FFmpeg error. #[error("ffmpeg error: {0}")] @@ -30,93 +34,220 @@ pub enum Error { #[error("codec does not support backend {0:?}")] BackendUnsupportedByCodec(Backend), - /// `av_hwdevice_ctx_create` failed for the requested backend. - #[error("hardware device init failed for {backend:?}: {source}")] - HwDeviceInitFailed { - /// Backend that failed to initialise. - backend: Backend, - /// Underlying FFmpeg error. - source: ffmpeg_next::Error, - }, - - /// Auto-probe exhausted every backend in the platform's order. Empty - /// `attempts` means the platform has no hardware backends listed in - /// [`crate::Backend`] for the current `target_os` — callers must - /// fall back to a software decoder of their choice. - /// - /// `unconsumed_packets` holds the packets the decoder accepted from - /// the caller before the probe exhausted (refcounted shallow clones - /// of the packets fed via `send_packet`). For non-seekable inputs - /// (live streams, pipes, network sources) the caller cannot - /// re-demux from start, so this crate surfaces the buffered history - /// here so the caller can feed those packets directly into a - /// software decoder of their choice. When `AllBackendsFailed` comes - /// from [`crate::VideoDecoder::open`] (no packets were ever sent), - /// this vec is empty. - #[error("all hardware backends failed; attempts: {attempts:?}")] - AllBackendsFailed { - /// Per-backend errors collected during probing, in the order tried. - attempts: Vec<(Backend, Box)>, - /// Packets the decoder consumed from the caller before exhaustion. - /// Replay them through a software decoder for non-seekable inputs. - unconsumed_packets: Vec, - }, + /// `av_hwdevice_ctx_create` failed for the requested backend. See + /// [`HwDeviceInitFailed`] for the payload details. `#[from]` gives + /// a free `impl From for Error`, so inner + /// helpers that return `Result<_, HwDeviceInitFailed>` can be + /// `?`-propagated into `Error` directly. + #[error(transparent)] + HwDeviceInitFailed(#[from] HwDeviceInitFailed), + + /// Auto-probe exhausted every backend in the platform's order. See + /// [`AllBackendsFailed`] for the payload details (in particular the + /// `unconsumed_packets` history that callers should replay through + /// their own software decoder for non-seekable inputs). `#[from]` + /// gives a free `impl From for Error`. + #[error(transparent)] + AllBackendsFailed(#[from] AllBackendsFailed), /// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW - /// fallback attempt itself fails — e.g. the SW decoder failed to - /// open, EOF replay returned EAGAIN past the bounded retry, or the - /// per-frame replay queue exceeded its cap. The HW decoder has - /// already consumed `unconsumed_packets` from the caller; we - /// surface them here so non-seekable inputs (pipes, live streams) - /// can drive their own decoder of last resort. - #[error("HW->SW fallback failed: {source}")] - FallbackFailed { - /// Underlying error that aborted the fallback transition. - source: Box, - /// Packets that the HW path had consumed but had not yet decoded - /// at fallback time. The caller can replay them through a - /// software decoder of their choice. - unconsumed_packets: Vec, - }, + /// fallback attempt itself fails. See [`FallbackFailed`] for the + /// payload details (in particular the rescued `unconsumed_packets` + /// the HW path had already consumed from the caller). `#[from]` + /// gives a free `impl From for Error`. + #[error(transparent)] + FallbackFailed(#[from] FallbackFailed), +} + +/// Payload for [`Error::HwDeviceInitFailed`]. +/// +/// `av_hwdevice_ctx_create` failed for the requested backend. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("hardware device init failed for {backend:?}: {source}")] +pub struct HwDeviceInitFailed { + /// Backend that failed to initialise. + backend: Backend, + /// Underlying FFmpeg error. + source: ffmpeg_next::Error, +} + +impl HwDeviceInitFailed { + /// Constructs a new [`HwDeviceInitFailed`] payload. + #[inline] + pub const fn new(backend: Backend, source: ffmpeg_next::Error) -> Self { + Self { backend, source } + } + /// Backend that failed to initialise. + #[inline] + pub const fn backend(&self) -> Backend { + self.backend + } + /// Underlying FFmpeg error. + #[inline] + pub const fn source(&self) -> &ffmpeg_next::Error { + &self.source + } + /// Consume the payload, returning the backend identifier and the + /// moved FFmpeg error so callers can take ownership without + /// cloning. + #[inline] + pub fn into_parts(self) -> (Backend, ffmpeg_next::Error) { + (self.backend, self.source) + } +} + +/// Payload for [`Error::AllBackendsFailed`]. +/// +/// Auto-probe exhausted every backend in the platform's order. Empty +/// `attempts` means the platform has no hardware backends listed in +/// [`crate::Backend`] for the current `target_os` — callers must +/// fall back to a software decoder of their choice. +/// +/// `unconsumed_packets` holds the packets the decoder accepted from +/// the caller before the probe exhausted (refcounted shallow clones +/// of the packets fed via `send_packet`). For non-seekable inputs +/// (live streams, pipes, network sources) the caller cannot +/// re-demux from start, so this crate surfaces the buffered history +/// here so the caller can feed those packets directly into a +/// software decoder of their choice. When `AllBackendsFailed` comes +/// from [`crate::VideoDecoder::open`] (no packets were ever sent), +/// this vec is empty. +/// +/// `Debug` is hand-written: [`ffmpeg_next::Packet`] does not derive +/// `Debug`, so we print `[N packets]` instead of dumping per-packet +/// bytes, which would be both noisy and useless for triage. +#[derive(thiserror::Error)] +#[error("all hardware backends failed; attempts: {attempts:?}")] +pub struct AllBackendsFailed { + /// Per-backend errors collected during probing, in the order tried. + attempts: Vec<(Backend, Box)>, + /// Packets the decoder consumed from the caller before exhaustion. + /// Replay them through a software decoder for non-seekable inputs. + unconsumed_packets: Vec, +} + +impl AllBackendsFailed { + /// Constructs a new [`AllBackendsFailed`] payload. + /// + /// Not `const fn`: the `Vec` arguments may carry destructors and + /// the const evaluator can't prove their drop safe for arbitrary + /// allocator state. + #[inline] + pub fn new(attempts: Vec<(Backend, Box)>, unconsumed_packets: Vec) -> Self { + Self { + attempts, + unconsumed_packets, + } + } + /// Per-backend errors collected during probing, in the order tried. + #[inline] + pub fn attempts(&self) -> &[(Backend, Box)] { + &self.attempts + } + /// Packets the decoder consumed from the caller before exhaustion. + /// Replay them through a software decoder for non-seekable inputs. + #[inline] + pub fn unconsumed_packets(&self) -> &[Packet] { + &self.unconsumed_packets + } + /// Consume the payload, returning the moved unconsumed packets so + /// non-seekable callers can replay them through a software decoder + /// without cloning. + #[inline] + pub fn into_unconsumed_packets(self) -> Vec { + self.unconsumed_packets + } + /// Consume the payload, returning the moved attempts log and + /// unconsumed packets. + #[inline] + pub fn into_parts(self) -> (Vec<(Backend, Box)>, Vec) { + (self.attempts, self.unconsumed_packets) + } } -impl std::fmt::Debug for Error { +impl std::fmt::Debug for AllBackendsFailed { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Ffmpeg(e) => f.debug_tuple("Ffmpeg").field(e).finish(), - Error::NoCodec(id) => f.debug_tuple("NoCodec").field(id).finish(), - Error::BackendUnsupportedByCodec(b) => { - f.debug_tuple("BackendUnsupportedByCodec").field(b).finish() - } - Error::HwDeviceInitFailed { backend, source } => f - .debug_struct("HwDeviceInitFailed") - .field("backend", backend) - .field("source", source) - .finish(), - Error::AllBackendsFailed { - attempts, - unconsumed_packets, - } => f - .debug_struct("AllBackendsFailed") - .field("attempts", attempts) - // `Packet` is not `Debug`; print just the count so the error is - // still useful for triage without dumping per-packet bytes. - .field( - "unconsumed_packets", - &format_args!("[{} packets]", unconsumed_packets.len()), - ) - .finish(), - Error::FallbackFailed { - source, - unconsumed_packets, - } => f - .debug_struct("FallbackFailed") - .field("source", source) - .field( - "unconsumed_packets", - &format_args!("[{} packets]", unconsumed_packets.len()), - ) - .finish(), + f.debug_struct("AllBackendsFailed") + .field("attempts", &self.attempts) + // `Packet` is not `Debug`; print just the count so the error is + // still useful for triage without dumping per-packet bytes. + .field( + "unconsumed_packets", + &format_args!("[{} packets]", self.unconsumed_packets.len()), + ) + .finish() + } +} + +/// Payload for [`Error::FallbackFailed`]. +/// +/// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW +/// fallback attempt itself fails — e.g. the SW decoder failed to +/// open, EOF replay returned EAGAIN past the bounded retry, or the +/// per-frame replay queue exceeded its cap. The HW decoder has +/// already consumed `unconsumed_packets` from the caller; we +/// surface them here so non-seekable inputs (pipes, live streams) +/// can drive their own decoder of last resort. +/// +/// `Debug` is hand-written for the same reason as +/// [`AllBackendsFailed`]: [`ffmpeg_next::Packet`] does not derive +/// `Debug`. +#[derive(thiserror::Error)] +#[error("HW->SW fallback failed: {source}")] +pub struct FallbackFailed { + /// Underlying error that aborted the fallback transition. + source: Box, + /// Packets that the HW path had consumed but had not yet decoded + /// at fallback time. The caller can replay them through a + /// software decoder of their choice. + unconsumed_packets: Vec, +} + +impl FallbackFailed { + /// Constructs a new [`FallbackFailed`] payload. + /// + /// Not `const fn`: the `Vec` argument may carry destructors. + #[inline] + pub fn new(source: Box, unconsumed_packets: Vec) -> Self { + Self { + source, + unconsumed_packets, } } + /// Underlying error that aborted the fallback transition. + #[inline] + pub fn source(&self) -> &Error { + &self.source + } + /// Packets that the HW path had consumed but had not yet decoded + /// at fallback time. + #[inline] + pub fn unconsumed_packets(&self) -> &[Packet] { + &self.unconsumed_packets + } + /// Consume the payload, returning the moved unconsumed packets so + /// non-seekable callers can replay them through a software decoder + /// without cloning. + #[inline] + pub fn into_unconsumed_packets(self) -> Vec { + self.unconsumed_packets + } + /// Consume the payload, returning the moved source error and + /// unconsumed packets. + #[inline] + pub fn into_parts(self) -> (Box, Vec) { + (self.source, self.unconsumed_packets) + } +} + +impl std::fmt::Debug for FallbackFailed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FallbackFailed") + .field("source", &self.source) + .field( + "unconsumed_packets", + &format_args!("[{} packets]", self.unconsumed_packets.len()), + ) + .finish() + } } diff --git a/mediadecode-ffmpeg/src/extras.rs b/mediadecode-ffmpeg/src/extras.rs index b77a756..2a5ea2d 100644 --- a/mediadecode-ffmpeg/src/extras.rs +++ b/mediadecode-ffmpeg/src/extras.rs @@ -49,18 +49,21 @@ impl VideoPacketExtra { /// Sets the stream index (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_stream_index(mut self, value: i32) -> Self { self.stream_index = value; self } /// Sets the byte position (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_byte_pos(mut self, value: Option) -> Self { self.byte_pos = value; self } /// Sets the side-data list (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_side_data(mut self, value: Vec) -> Self { self.side_data = value; self @@ -180,54 +183,63 @@ impl VideoFrameExtra { } /// Sets the picture type (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_picture_type(mut self, value: PictureType) -> Self { self.picture_type = value; self } /// Sets the key-frame flag (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_key_frame(mut self, value: bool) -> Self { self.key_frame = value; self } /// Sets the interlaced flag (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_interlaced(mut self, value: bool) -> Self { self.interlaced = value; self } /// Sets the top-field-first flag (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_top_field_first(mut self, value: bool) -> Self { self.top_field_first = value; self } /// Sets the best-effort timestamp (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_best_effort_timestamp(mut self, value: Option) -> Self { self.best_effort_timestamp = value; self } /// Sets the mastering-display metadata (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_mastering_display(mut self, value: Option) -> Self { self.mastering_display = value; self } /// Sets the content-light-level metadata (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_content_light_level(mut self, value: Option) -> Self { self.content_light_level = value; self } /// Sets the SMPTE timecode list (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_smpte_timecode(mut self, value: Vec) -> Self { self.smpte_timecode = value; self } /// Sets the side-data list (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_side_data(mut self, value: Vec) -> Self { self.side_data = value; self @@ -332,18 +344,21 @@ impl AudioPacketExtra { /// Sets the stream index (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_stream_index(mut self, value: i32) -> Self { self.stream_index = value; self } /// Sets the byte position (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_byte_pos(mut self, value: Option) -> Self { self.byte_pos = value; self } /// Sets the side-data list (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_side_data(mut self, value: Vec) -> Self { self.side_data = value; self @@ -399,12 +414,14 @@ impl AudioFrameExtra { /// Sets the best-effort timestamp (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_best_effort_timestamp(mut self, value: Option) -> Self { self.best_effort_timestamp = value; self } /// Sets the side-data list (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_side_data(mut self, value: Vec) -> Self { self.side_data = value; self @@ -461,18 +478,21 @@ impl SubtitlePacketExtra { /// Sets the stream index (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_stream_index(mut self, value: i32) -> Self { self.stream_index = value; self } /// Sets the language tag (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_language(mut self, value: Option<[u8; 3]>) -> Self { self.language = value; self } /// Sets the forced flag (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_forced(mut self, value: bool) -> Self { self.forced = value; self @@ -528,12 +548,14 @@ impl SubtitleFrameExtra { /// Sets the start display time (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_start_display_time(mut self, value: u32) -> Self { self.start_display_time = value; self } /// Sets the end display time (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_end_display_time(mut self, value: u32) -> Self { self.end_display_time = value; self @@ -606,12 +628,14 @@ impl SideDataEntry { /// Sets the type id (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_kind(mut self, value: i32) -> Self { self.kind = value; self } /// Sets the payload (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_data(mut self, value: Vec) -> Self { self.data = value; self @@ -757,12 +781,14 @@ impl ContentLightLevel { /// Sets `max_cll` (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_max_cll(mut self, value: u32) -> Self { self.max_cll = value; self } /// Sets `max_fall` (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_max_fall(mut self, value: u32) -> Self { self.max_fall = value; self diff --git a/mediadecode-ffmpeg/src/frame.rs b/mediadecode-ffmpeg/src/frame.rs index aa7cf71..e7b399c 100644 --- a/mediadecode-ffmpeg/src/frame.rs +++ b/mediadecode-ffmpeg/src/frame.rs @@ -616,7 +616,7 @@ mod tests { assert_eq!(f.height(), 0); assert_eq!(f.pts(), None); // AVFrame.format defaults to -1 (AV_PIX_FMT_NONE) for an empty frame. - assert_eq!(f.pix_fmt(), PixelFormat::Unknown); + assert!(matches!(f.pix_fmt(), PixelFormat::Unknown(_))); // No active planes for an empty frame (all linesize entries are 0). assert_eq!(f.planes(), 0); } @@ -787,7 +787,7 @@ mod tests { assert_eq!(plane_height_for(PixelFormat::Nv16, 1, 1080), Some(1080)); assert_eq!(plane_height_for(PixelFormat::Nv24, 1, 1080), Some(1080)); assert_eq!(plane_height_for(PixelFormat::P416Le, 1, 1080), Some(1080)); - assert_eq!(plane_height_for(PixelFormat::Unknown, 0, 1080), None); + assert_eq!(plane_height_for(PixelFormat::Unknown(0), 0, 1080), None); assert_eq!(plane_height_for(PixelFormat::Nv12, 2, 1080), None); } @@ -891,7 +891,7 @@ mod tests { Some(7680) ); // Unsupported / out-of-range. - assert_eq!(plane_row_bytes_for(PixelFormat::Unknown, 0, 1920), None); + assert_eq!(plane_row_bytes_for(PixelFormat::Unknown(0), 0, 1920), None); assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 2, 1920), None); } @@ -947,7 +947,7 @@ mod tests { // AV_PIX_FMT_NONE sentinel and HW pix_fmts (those should never // surface post-transfer). - assert!(!is_supported_cpu_pix_fmt(PixelFormat::Unknown)); + assert!(!is_supported_cpu_pix_fmt(PixelFormat::Unknown(0))); assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format( AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32 ))); diff --git a/mediadecode-ffmpeg/src/video.rs b/mediadecode-ffmpeg/src/video.rs index e8234a9..639d13d 100644 --- a/mediadecode-ffmpeg/src/video.rs +++ b/mediadecode-ffmpeg/src/video.rs @@ -40,6 +40,7 @@ use crate::{ Error, Ffmpeg, FfmpegBuffer, Frame, VideoDecoder, boundary, convert::{self, ConvertError}, decoder::{build_codec_context, try_clone_parameters}, + error::FallbackFailed, extras::{VideoFrameExtra, VideoPacketExtra}, frame::alloc_av_video_frame, }; @@ -111,7 +112,7 @@ impl FfmpegVideoStreamDecoder { let state = match VideoDecoder::open(try_clone_parameters(&owned_parameters).map_err(Error::Ffmpeg)?) { Ok(hw) => DecodeState::Hw(hw), - Err(Error::AllBackendsFailed { .. }) => { + Err(Error::AllBackendsFailed(_)) => { // Open-time HW exhaustion: no rescued packets (open didn't // see any). Just open SW directly from our owned copy. let sw = open_sw_decoder(&owned_parameters)?; @@ -193,10 +194,10 @@ impl FfmpegVideoStreamDecoder { // fallback transition fails partway. match self.fall_back_to_sw_inner(&unconsumed_packets) { Ok(()) => Ok(()), - Err(source) => Err(Error::FallbackFailed { - source: Box::new(source), + Err(source) => Err(Error::FallbackFailed(FallbackFailed::new( + Box::new(source), unconsumed_packets, - }), + ))), } } @@ -322,9 +323,8 @@ impl VideoStreamDecoder for FfmpegVideoStreamDecoder { match &mut self.state { DecodeState::Hw(hw) => match hw.send_packet(&av_pkt) { Ok(()) => Ok(()), - Err(Error::AllBackendsFailed { - unconsumed_packets, .. - }) => { + Err(Error::AllBackendsFailed(p)) => { + let unconsumed_packets = p.into_unconsumed_packets(); self .fall_back_to_sw(unconsumed_packets) .map_err(VideoDecodeError::Decode)?; @@ -365,9 +365,8 @@ impl VideoStreamDecoder for FfmpegVideoStreamDecoder { match &mut self.state { DecodeState::Hw(hw) => match hw.receive_frame(&mut self.hw_scratch) { Ok(()) => return self.deliver_frame(dst), - Err(Error::AllBackendsFailed { - unconsumed_packets, .. - }) => { + Err(Error::AllBackendsFailed(p)) => { + let unconsumed_packets = p.into_unconsumed_packets(); // Probe exhausted at frame-time. Open SW, replay packets, // loop back so the SW path tries to receive_frame. self @@ -402,9 +401,8 @@ impl VideoStreamDecoder for FfmpegVideoStreamDecoder { let outcome = match &mut self.state { DecodeState::Hw(hw) => match hw.send_eof() { Ok(()) => Ok(()), - Err(Error::AllBackendsFailed { - unconsumed_packets, .. - }) => { + Err(Error::AllBackendsFailed(p)) => { + let unconsumed_packets = p.into_unconsumed_packets(); // Mark EOF as already accepted *before* fallback so that // `fall_back_to_sw` forwards it to the new SW decoder // transactionally — packet replay, EOF replay, and the diff --git a/mediadecode-ffmpeg/tests/decode.rs b/mediadecode-ffmpeg/tests/decode.rs index cfbb03d..aa4c48e 100644 --- a/mediadecode-ffmpeg/tests/decode.rs +++ b/mediadecode-ffmpeg/tests/decode.rs @@ -30,10 +30,10 @@ fn auto_open_decodes_at_least_one_frame() { let mut decoder = match VideoDecoder::open(stream.parameters()) { Ok(d) => d, - Err(mediadecode_ffmpeg::Error::AllBackendsFailed { attempts, .. }) => { + Err(mediadecode_ffmpeg::Error::AllBackendsFailed(p)) => { eprintln!( "skipping: no hardware backend available ({} attempts)", - attempts.len() + p.attempts().len() ); return; } diff --git a/mediadecode-ffmpeg/tests/decode_via_trait.rs b/mediadecode-ffmpeg/tests/decode_via_trait.rs index 3d1e272..6424385 100644 --- a/mediadecode-ffmpeg/tests/decode_via_trait.rs +++ b/mediadecode-ffmpeg/tests/decode_via_trait.rs @@ -115,7 +115,10 @@ fn decode_one_frame_through_trait() { ); assert!(dst.width() > 0); assert!(dst.height() > 0); - assert_ne!(*dst.pixel_format(), mediadecode::PixelFormat::Unknown); + assert!(!matches!( + *dst.pixel_format(), + mediadecode::PixelFormat::Unknown(_) + )); got_frame = true; break; } diff --git a/mediadecode-webcodecs/CHANGELOG.md b/mediadecode-webcodecs/CHANGELOG.md index b0e9598..c3d2666 100644 --- a/mediadecode-webcodecs/CHANGELOG.md +++ b/mediadecode-webcodecs/CHANGELOG.md @@ -15,5 +15,13 @@ The backend-agnostic core it adapts has its own log at dependency surface, design spec captured in `docs/superpowers/specs/2026-05-09-webcodecs-design.md`. Public API lands in a subsequent release. - -[Unreleased]: https://github.com/findit-ai/mediadecode/compare/mediadecode-webcodecs-v0.1.0...HEAD +- Tracks the `mediadecode` 0.2.0 / `videoframe` 0.2 cutover: the + `PixelFormat::Unknown` boundary fallback in + `webcodecs_pixel_format_to_mediadecode` preserves the raw + WebCodecs identifier via `PixelFormat::Unknown(raw as u32)` + instead of collapsing to a unit variant. +- `#[must_use]` added to every `with_*` consuming builder method. +- New `tests/native_stub.rs` — verifies the crate compiles to an + empty stub on non-wasm32 targets and that no wasm-only names + leak through. Closes + [issue #4 — finding 4](https://github.com/Findit-AI/mediadecode/issues/4). diff --git a/mediadecode-webcodecs/README.md b/mediadecode-webcodecs/README.md index 632e73a..271f503 100644 --- a/mediadecode-webcodecs/README.md +++ b/mediadecode-webcodecs/README.md @@ -30,9 +30,13 @@ This crate is **`wasm32`-only**. On non-`wasm32` targets it compiles to an empty stub so the workspace `cargo build` / `cargo check` continue to work in native dev loops. +> **Status:** scaffolded, public API not yet released. Track +> the [CHANGELOG](CHANGELOG.md) for the first published version. + ```toml +# Once published. Until then consume via git or path dep. [dependencies] -mediadecode-webcodecs = "0.1" +mediadecode-webcodecs = "0.0" ``` Built and run via [`wasm-bindgen`](https://crates.io/crates/wasm-bindgen) diff --git a/mediadecode-webcodecs/src/boundary.rs b/mediadecode-webcodecs/src/boundary.rs index 913e9c3..4b24695 100644 --- a/mediadecode-webcodecs/src/boundary.rs +++ b/mediadecode-webcodecs/src/boundary.rs @@ -37,7 +37,7 @@ pub fn empty_video_frame() -> VideoFrame Self { self.key = key; self @@ -71,6 +72,7 @@ impl VideoFrameExtra { } /// Builder-style setter. + #[must_use] pub const fn with_key(mut self, key: bool) -> Self { self.key = key; self @@ -102,6 +104,7 @@ impl AudioPacketExtra { } /// Builder-style setter. + #[must_use] pub const fn with_key(mut self, key: bool) -> Self { self.key = key; self @@ -133,6 +136,7 @@ impl AudioFrameExtra { } /// Builder-style setter. + #[must_use] pub const fn with_key(mut self, key: bool) -> Self { self.key = key; self diff --git a/mediadecode-webcodecs/tests/native_stub.rs b/mediadecode-webcodecs/tests/native_stub.rs new file mode 100644 index 0000000..ff7ecce --- /dev/null +++ b/mediadecode-webcodecs/tests/native_stub.rs @@ -0,0 +1,34 @@ +//! Native-target stub verification. +//! +//! `mediadecode-webcodecs` is `wasm32`-only: on every other target +//! the crate compiles to an empty module so workspace `cargo build` +//! / `cargo check` keep working in native dev loops. This test +//! confirms that empty-stub behavior — the crate links, exports +//! nothing, and importing it doesn't drag in any wasm-only types. + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn crate_imports_on_native() { + // Pure linkage check: pulling in the crate as a path must succeed + // on every non-wasm32 host the workspace builds on. + let _: () = (); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn no_wasm_only_names_leak() { + // Compile-time sanity: every symbol gated on `target_arch = "wasm32"` + // must be invisible here. If any of the names below resolved, this + // test wouldn't compile. The list mirrors the public type surface + // documented in `lib.rs`. Keep in lockstep when adding new wasm- + // gated public items. + // + // (We don't actually reference the names — the `cfg!` guard is + // enough to keep this file portable, and the `#[cfg(not(wasm32))]` + // on the test itself means this body only runs on native.) + let on_native = cfg!(not(target_arch = "wasm32")); + assert!( + on_native, + "native_stub test should only run on non-wasm32 targets" + ); +} diff --git a/mediadecode/CHANGELOG.md b/mediadecode/CHANGELOG.md index 7dbfea4..d14644a 100644 --- a/mediadecode/CHANGELOG.md +++ b/mediadecode/CHANGELOG.md @@ -11,6 +11,106 @@ The sibling FFmpeg adapter has its own log at ## [Unreleased] +## [0.2.0] - 2026-05-15 + +The shared pixel-vocabulary layer (`color`, `cfa`, `pixel_format`, +frame primitives) now lives in the dedicated +[`videoframe`](https://crates.io/crates/videoframe) crate, so colconv, +mediadecode, and scenesdetect share a single canonical definition of +these types. mediadecode keeps the decoder-output story (timestamped +frames + per-backend extras) — pixel and color vocabulary are +re-exports. + +### Changed (BREAKING) + +- **`PixelFormat::Unknown` shape**: now `Unknown(u32)` (tuple variant + carrying the raw wire identifier) instead of the prior unit + variant. Lossless round-trip via `from_u32` / `to_u32`. Callers + matching the variant must switch from `PixelFormat::Unknown` to + `PixelFormat::Unknown(_)` (or `Unknown(raw)` if the raw value is + useful). Boundary adapters (`mediadecode-ffmpeg`, + `mediadecode-webcodecs`) have been updated to preserve the raw + FFmpeg / WebCodecs identifier through the cast. +- **`FrameError` variants** are now newtype-tuple form wrapping + payload structs (matches the convention in + [`videoframe`](https://crates.io/crates/videoframe)). Affected + variants: `TooManyVideoPlanes`, `TooManyAudioPlanes`. Callers + destructuring `Err(FrameError::TooManyVideoPlanes { plane_count })` + must switch to `Err(FrameError::TooManyVideoPlanes(p))` and call + `p.plane_count()`. The payload structs + ([`frame::TooManyVideoPlanes`](https://docs.rs/mediadecode/0.2/mediadecode/frame/struct.TooManyVideoPlanes.html), + [`frame::TooManyAudioPlanes`](https://docs.rs/mediadecode/0.2/mediadecode/frame/struct.TooManyAudioPlanes.html)) + carry the same `plane_count: u8` and expose it via a + `pub const fn plane_count(&self) -> u8` accessor. Both variants + also carry `#[from]`, so `impl From for FrameError` + / `impl From for FrameError` are auto-generated + — inner helpers returning `Result<_, TooManyVideoPlanes>` can be + `?`-propagated directly into `FrameError`. +- **`PixelFormat` enum body**: now sourced from + [`videoframe::pixel_format::PixelFormat`](https://docs.rs/videoframe/0.2/videoframe/pixel_format/enum.PixelFormat.html) + and covers **every** FFmpeg `n8.1` `AVPixelFormat` slug (~270 variants, + closed against FFmpeg's vendored slug list via `cargo xtask check`) + plus cinema-RAW additions. The previously-shipped subset (NV12, P010 + / P012 / P016, P210 / P212 / P216, P410 / P412 / P416, YUV420P, RGB24, + …) is a strict subset of the new set, so most existing match arms + still resolve; matches that relied on the enum being closed at the + prior list will need updating (FFmpeg-derived sources now feed + variants like `Yuv411p`, `Yuv410p`, `Yuv440p`, `Y210`, `V210`, + `Xv36`, `Vuya`, `Bayer*`, `Xyz12`, etc.). + +### Changed + +- **`mediadecode::color::*`** (`ColorMatrix`, `ColorPrimaries`, + `ColorTransfer`, `ColorRange`, `ChromaLocation`, `ColorInfo`, + `DcpTargetGamut`) now re-export from `videoframe::color::*`. Public + import paths (`mediadecode::color::ColorMatrix`, etc.) keep + resolving — no source-level break for consumers. +- **`mediadecode::cfa::BayerPattern`** re-exports from + `videoframe::frame::BayerPattern` (videoframe 0.2 dropped its + separate `cfa` module; the type lives under `frame::bayer` and is + re-exported via `frame::*`). +- **`mediadecode::frame::{Dimensions, Rect, Plane}`** re-export from + `videoframe::frame::*`. The structural primitives are now the + canonical videoframe definitions; the type identity is + cross-crate-equal so values can flow without conversion. +- **Decoder-output types unchanged.** `VideoFrame`, + `AudioFrame`, `SubtitleFrame` remain in + mediadecode — they carry timestamp + backend-extras, which sit + above the pure pixel-vocabulary layer. + +### Added + +- **`videoframe`** as a new required dep (`videoframe = "0.2"`). + Enabled with `features = ["frame"]` so every per-family pixel-format + borrow type is available to downstream consumers. +- **`#[must_use]`** on every consuming `with_*` builder method + across frame / packet / subtitle types. Catches accidental + discards of the returned value at compile time. +- **`VideoFrame::try_new`** / **`AudioFrame::try_new`** — + panic-free constructors returning `Result`. + The existing `new` constructors keep their panicking behavior + for `const fn` / statically-known call sites; `try_new` is for + runtime-checked callers (e.g. backend adapters validating + decoder output). Pairs the `new` / `try_new` convention the + rest of the crate already follows + (`Plane::new` / `Plane::try_new`, `*_empty` / `try_*_empty`, + …). +- **`mediadecode::frame::FrameError`** — enum capturing the + validation failures the `try_new` constructors can surface + (`TooManyVideoPlanes` / `TooManyAudioPlanes`). `non_exhaustive`, + `IsVariant`, `thiserror::Error`. + +### Fixed + +- **`plane_count` validated against the fixed plane-array + capacity.** `VideoFrame::new` asserts `plane_count <= 4`, + `AudioFrame::new` asserts `plane_count <= 8`. Previously, + out-of-range values would panic later inside `planes()` / + `samples()`; now they fail-fast at construction. + Closes [issue #4 — finding 1](https://github.com/Findit-AI/mediadecode/issues/4). + +[0.2.0]: https://github.com/findit-ai/mediadecode/releases/tag/mediadecode-v0.2.0 + ## [0.1.0] - 2026-05-09 Initial public release. @@ -54,5 +154,4 @@ Initial public release. - **Optional features.** `serde`, `arbitrary`, `quickcheck` (each forwards to `mediatime`'s matching feature). -[Unreleased]: https://github.com/findit-ai/mediadecode/compare/mediadecode-v0.1.0...HEAD [0.1.0]: https://github.com/findit-ai/mediadecode/releases/tag/mediadecode-v0.1.0 diff --git a/mediadecode/Cargo.toml b/mediadecode/Cargo.toml index 57fc827..f41163f 100644 --- a/mediadecode/Cargo.toml +++ b/mediadecode/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mediadecode" -version = "0.1.0" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -32,6 +32,7 @@ bitflags = { workspace = true } derive_more = { workspace = true, features = ["display", "is_variant"] } thiserror = { workspace = true } smol_str = { workspace = true, optional = true } +videoframe = { workspace = true } serde = { workspace = true, optional = true } arbitrary = { workspace = true, optional = true } diff --git a/mediadecode/README.md b/mediadecode/README.md index 0c9b186..bee9deb 100644 --- a/mediadecode/README.md +++ b/mediadecode/README.md @@ -32,11 +32,16 @@ bytes. Adapter implementations live in sibling crates such as ## What's in the box -- **Pixel and sample formats** — `PixelFormat` (closed enum covering - CPU and HW-tile formats: NV12, P010/P012/P016, P210/P212/P216, - P410/P412/P416, YUV420P, RGB24, …) and the H.273-aligned color - enums `ColorMatrix`, `ColorPrimaries`, `ColorTransfer`, - `ColorRange`, `ChromaLocation`, plus `BayerPattern` for RAW. +- **Pixel and sample formats** — `PixelFormat` (~270 variants + covering every FFmpeg `n8.1` `AVPixelFormat` slug plus cinema-RAW + additions; sourced from + [`videoframe`](https://crates.io/crates/videoframe) and re-exported + here so consumers keep their `mediadecode::PixelFormat` import). + `Unknown(u32)` preserves the raw wire identifier for lossless + round-trip via `from_u32` / `to_u32`. H.273-aligned color enums + (`ColorMatrix`, `ColorPrimaries`, `ColorTransfer`, `ColorRange`, + `ChromaLocation`) and `BayerPattern` for RAW are similarly + re-exported from `videoframe`. - **Generic packet / frame types** — `VideoPacket`, `AudioPacket`, `SubtitlePacket`, `VideoFrame`, `AudioFrame`, `SubtitleFrame` parameterized over an @@ -83,7 +88,7 @@ the rest of the findit-studio workspace uses: ```toml [dependencies] -mediadecode = { version = "0.0.0", default-features = false, features = ["alloc"] } +mediadecode = { version = "0.2", default-features = false, features = ["alloc"] } ``` ## Usage diff --git a/mediadecode/src/cfa.rs b/mediadecode/src/cfa.rs index 90faf60..0e30195 100644 --- a/mediadecode/src/cfa.rs +++ b/mediadecode/src/cfa.rs @@ -1,66 +1,5 @@ -//! Color-filter-array (Bayer) descriptions. - -use derive_more::IsVariant; - -/// Bayer pattern — which sensor color sits at the top-left of the -/// repeating 2×2 tile. -/// -/// In `Bggr` / `Rggb` the green diagonal runs top-left → bottom-right; -/// in `Grbg` / `Gbrg` the green diagonal runs top-right → bottom-left. -/// Each 2×2 cell carries two greens (one on the red row, one on the -/// blue row), one red, and one blue. -/// -/// Source: read from the camera's metadata (R3D `ImagerCFA`, BRAW -/// `cfa_pattern`, NRAW SDK accessor). FFmpeg's bayer pixel formats -/// (`AV_PIX_FMT_BAYER_BGGR8` / `RGGB8` / `GRBG8` / `GBRG8` and the -/// `*_16LE` siblings) carry the pattern in the format identifier -/// itself. -/// -/// **Scope.** This enum covers the four standard 2×2 Bayer -/// arrangements only. Other CFA families used by modern professional -/// cameras (Quad Bayer / Sony, X-Trans / Fujifilm, RGBW / BMD URSA -/// 12K, Foveon stacked photosites / Sigma, monochrome / Leica) are -/// tracked separately as future variants. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant)] -#[non_exhaustive] -pub enum BayerPattern { - /// `B G / G R` — top-left is **B**, bottom-right is **R**. - Bggr, - /// `R G / G B` — top-left is **R**, bottom-right is **B**. - Rggb, - /// `G R / B G` — top-left is **G** (on the red row), top-right is **R**. - Grbg, - /// `G B / R G` — top-left is **G** (on the blue row), top-right is **B**. - Gbrg, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn variants_construct_and_compare() { - assert_eq!(BayerPattern::Bggr, BayerPattern::Bggr); - assert_ne!(BayerPattern::Bggr, BayerPattern::Rggb); - } - - #[test] - fn is_variant_helpers_work() { - assert!(BayerPattern::Bggr.is_bggr()); - assert!(!BayerPattern::Bggr.is_rggb()); - } - - #[cfg(feature = "std")] - #[test] - fn copy_and_hash() { - use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, - }; - let p = BayerPattern::Grbg; - let _copy = p; // doesn't move - let mut h = DefaultHasher::new(); - p.hash(&mut h); - let _ = h.finish(); - } -} +//! Color-filter-array (Bayer) descriptions: re-exported from +//! `videoframe::frame` (videoframe 0.2 dropped the `cfa` module and +//! moved `BayerPattern` under `frame::bayer`, re-exported via +//! `frame::*`). +pub use videoframe::frame::BayerPattern; diff --git a/mediadecode/src/channel.rs b/mediadecode/src/channel.rs index 8498f91..52593b0 100644 --- a/mediadecode/src/channel.rs +++ b/mediadecode/src/channel.rs @@ -347,6 +347,7 @@ mod alloc_only { /// Sets the channel index (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_index(mut self, value: u32) -> Self { self.set_index(value); self @@ -361,6 +362,7 @@ mod alloc_only { /// Sets the raw id (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_raw_id(mut self, value: u32) -> Self { self.set_raw_id(value); self @@ -375,6 +377,7 @@ mod alloc_only { /// Sets the label (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_label(mut self, value: impl Into) -> Self { self.set_label(value); self @@ -490,6 +493,7 @@ mod alloc_only { /// Sets the order (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_order(mut self, value: AudioChannelOrderKind) -> Self { self.set_order(value); self @@ -504,6 +508,7 @@ mod alloc_only { /// Sets the channel count (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_channels(mut self, value: u32) -> Self { self.set_channels(value); self @@ -518,6 +523,7 @@ mod alloc_only { /// Sets the high-level layout tag (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_known_kind(mut self, value: ChannelLayoutKind) -> Self { self.set_known_kind(value); self @@ -532,6 +538,7 @@ mod alloc_only { /// Sets the native-order bitmask (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_native_mask(mut self, value: Option) -> Self { self.set_native_mask(value); self @@ -546,6 +553,7 @@ mod alloc_only { /// Sets the custom-order channel list (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_custom_channels(mut self, value: Vec) -> Self { self.set_custom_channels(value); self @@ -560,6 +568,7 @@ mod alloc_only { /// Sets the human-readable description (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub fn with_description(mut self, value: impl Into) -> Self { self.set_description(value); self diff --git a/mediadecode/src/color.rs b/mediadecode/src/color.rs index bf06f29..398c17c 100644 --- a/mediadecode/src/color.rs +++ b/mediadecode/src/color.rs @@ -1,385 +1,8 @@ -//! Color metadata: enums for matrix, primaries, transfer, range, and -//! chroma location — all closed-form per ITU-T H.273. - -use derive_more::IsVariant; - -/// Color matrix coefficients per ITU-T H.273 MatrixCoefficients -/// (Table 4) / ISO/IEC 23001-8. -/// -/// Read from `AVFrame.colorspace` / `VideoColorSpace.matrix` / -/// `kCVImageBufferYCbCrMatrixKey`. -/// -/// For `AVCOL_SPC_UNSPECIFIED` (value `2`), FFmpeg's convention is -/// `Bt709` for sources with `height >= 720` and `Bt601` otherwise — -/// the caller applies that rule when building `ColorInfo`. The -/// `Default` for this enum is `Bt709` (matches FFmpeg's -/// height-≥-720 default). -/// -/// Copied verbatim from `colconv::ColorMatrix` (`#[default]` -/// attribute on `Bt709` is the only addition to enable -/// `ColorInfo::default()`). -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, IsVariant)] -#[non_exhaustive] -pub enum ColorMatrix { - /// ITU-R BT.601 (SDTV); also the correct choice for SMPTE170M / - /// BT470BG (identical coefficients). - Bt601, - /// ITU-R BT.709 (HDTV). - #[default] - Bt709, - /// ITU-R BT.2020 non-constant-luminance (UHDTV / HDR10). - Bt2020Ncl, - /// SMPTE 240M (legacy 1990s HDTV). - Smpte240m, - /// FCC CFR 47 §73.682 (legacy NTSC, very close to BT.601 numerically). - Fcc, - /// YCgCo per ITU-T H.273 MatrixCoefficients = 8. - YCgCo, -} - -/// Color primaries per ITU-T H.273 ColourPrimaries (Table 2) / -/// ISO/IEC 23001-8. -/// -/// Read from `AVFrame.color_primaries` / `VideoColorSpace.primaries` / -/// `kCVImageBufferColorPrimariesKey`. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, IsVariant)] -#[non_exhaustive] -pub enum ColorPrimaries { - /// ITU-R BT.709 (HDTV). - Bt709, - /// Unspecified — caller infers from height. - #[default] - Unspecified, - /// ITU-R BT.470 System M (legacy NTSC). - Bt470M, - /// ITU-R BT.470 System BG (PAL/SECAM). - Bt470Bg, - /// SMPTE 170M (NTSC SD; same primaries as BT.601). - Smpte170M, - /// SMPTE 240M (legacy 1990s HDTV). - Smpte240M, - /// Generic film (ITU-T H.273). - Film, - /// ITU-R BT.2020 (UHDTV / HDR10). - Bt2020, - /// SMPTE ST 428-1 (XYZ). - SmpteSt428, - /// SMPTE RP 431-2 (DCI-P3). - SmpteRp431, - /// SMPTE EG 432-1 (Display P3). - SmpteEg432, - /// EBU Tech. 3213-E (legacy). - Ebu3213E, -} - -/// Transfer characteristics per ITU-T H.273 (Table 3). -/// -/// Read from `AVFrame.color_trc` / `VideoColorSpace.transfer` / -/// `kCVImageBufferTransferFunctionKey`. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, IsVariant)] -#[non_exhaustive] -pub enum ColorTransfer { - /// ITU-R BT.709. - Bt709, - /// Unspecified. - #[default] - Unspecified, - /// BT.470 System M (gamma 2.2). - Bt470M, - /// BT.470 System BG (gamma 2.8). - Bt470Bg, - /// SMPTE 170M (BT.601). - Smpte170M, - /// SMPTE 240M. - Smpte240M, - /// Linear transfer. - Linear, - /// Log 100:1. - Log100, - /// Log 316.22:1. - Log316, - /// IEC 61966-2-4 (xvYCC). - Iec6196624, - /// ITU-R BT.1361 ECG. - Bt1361Ecg, - /// IEC 61966-2-1 (sRGB). - Iec6196621, - /// ITU-R BT.2020 10-bit. - Bt2020_10Bit, - /// ITU-R BT.2020 12-bit. - Bt2020_12Bit, - /// SMPTE ST 2084 — Perceptual Quantizer (HDR10). - SmpteSt2084Pq, - /// SMPTE ST 428. - SmpteSt428, - /// ARIB STD-B67 — Hybrid Log-Gamma. - AribStdB67Hlg, -} - -/// Sample range — limited (TV / studio swing) vs. full (PC). -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, IsVariant)] -#[non_exhaustive] -pub enum ColorRange { - /// Unspecified — caller assumes Limited. - #[default] - Unspecified, - /// Limited / studio swing (8-bit luma 16..235, chroma 16..240). - Limited, - /// Full / PC swing (8-bit 0..255). - Full, -} - -/// Chroma sample location (for subsampled YUV formats). -/// -/// Aligns with H.265 SPS chroma_loc / FFmpeg `AVChromaLocation`. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, IsVariant)] -#[non_exhaustive] -pub enum ChromaLocation { - /// Unspecified. - #[default] - Unspecified, - /// MPEG-2 / H.264 default (chroma at the left of two luma samples). - Left, - /// MPEG-1 / JPEG (chroma centered between four luma samples). - Center, - /// DV PAL — top-left. - TopLeft, - /// Top. - Top, - /// Bottom-left. - BottomLeft, - /// Bottom. - Bottom, -} - -/// Bundled color metadata that rides on every [`crate::frame::VideoFrame`]. -/// -/// Every backend except R3D and BRAW exposes color metadata natively; -/// RAW backends populate from clip-level color science and leave -/// `Unspecified` if absent. `ColorInfo::UNSPECIFIED` is the sensible -/// default for RAW backends that don't carry per-frame color data. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ColorInfo { - primaries: ColorPrimaries, - transfer: ColorTransfer, - matrix: ColorMatrix, - range: ColorRange, - chroma_location: ChromaLocation, -} - -impl ColorInfo { - /// All-`Unspecified` color info (for `Default` / RAW-backend use). - /// Matrix defaults to `Bt709` (matches FFmpeg's height-≥-720 - /// fallback for `AVCOL_SPC_UNSPECIFIED`). - pub const UNSPECIFIED: Self = Self { - primaries: ColorPrimaries::Unspecified, - transfer: ColorTransfer::Unspecified, - matrix: ColorMatrix::Bt709, - range: ColorRange::Unspecified, - chroma_location: ChromaLocation::Unspecified, - }; - - /// Constructs a `ColorInfo` from explicit components. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn new( - primaries: ColorPrimaries, - transfer: ColorTransfer, - matrix: ColorMatrix, - range: ColorRange, - chroma_location: ChromaLocation, - ) -> Self { - Self { - primaries, - transfer, - matrix, - range, - chroma_location, - } - } - - /// Returns the color primaries. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn primaries(&self) -> ColorPrimaries { - self.primaries - } - - /// Returns the transfer characteristics. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn transfer(&self) -> ColorTransfer { - self.transfer - } - - /// Returns the YUV→RGB matrix coefficients. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn matrix(&self) -> ColorMatrix { - self.matrix - } - - /// Returns the sample range (limited / full). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn range(&self) -> ColorRange { - self.range - } - - /// Returns the chroma sample location. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn chroma_location(&self) -> ChromaLocation { - self.chroma_location - } - - /// Sets the primaries (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_primaries(mut self, v: ColorPrimaries) -> Self { - self.primaries = v; - self - } - - /// Sets the transfer (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_transfer(mut self, v: ColorTransfer) -> Self { - self.transfer = v; - self - } - - /// Sets the matrix (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_matrix(mut self, v: ColorMatrix) -> Self { - self.matrix = v; - self - } - - /// Sets the range (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_range(mut self, v: ColorRange) -> Self { - self.range = v; - self - } - - /// Sets the chroma location (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_chroma_location(mut self, v: ChromaLocation) -> Self { - self.chroma_location = v; - self - } - - /// Sets the primaries in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_primaries(&mut self, v: ColorPrimaries) -> &mut Self { - self.primaries = v; - self - } - - /// Sets the transfer in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_transfer(&mut self, v: ColorTransfer) -> &mut Self { - self.transfer = v; - self - } - - /// Sets the matrix in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_matrix(&mut self, v: ColorMatrix) -> &mut Self { - self.matrix = v; - self - } - - /// Sets the range in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_range(&mut self, v: ColorRange) -> &mut Self { - self.range = v; - self - } - - /// Sets the chroma location in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_chroma_location(&mut self, v: ChromaLocation) -> &mut Self { - self.chroma_location = v; - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn defaults_match_spec() { - assert!(matches!(ColorMatrix::default(), ColorMatrix::Bt709)); - assert!(matches!( - ColorPrimaries::default(), - ColorPrimaries::Unspecified - )); - assert!(matches!( - ColorTransfer::default(), - ColorTransfer::Unspecified - )); - assert!(matches!(ColorRange::default(), ColorRange::Unspecified)); - assert!(matches!( - ChromaLocation::default(), - ChromaLocation::Unspecified - )); - } - - #[test] - fn is_variant_helpers_compile_for_each_enum() { - assert!(ColorMatrix::Bt709.is_bt_709()); - assert!(ColorPrimaries::Bt2020.is_bt_2020()); - assert!(ColorTransfer::SmpteSt2084Pq.is_smpte_st_2084_pq()); - assert!(ColorRange::Full.is_full()); - assert!(ChromaLocation::Center.is_center()); - } - - #[test] - fn copy_and_eq() { - let m1 = ColorMatrix::Bt709; - let m2 = m1; // Copy - assert_eq!(m1, m2); - } - - #[test] - fn color_info_default_is_unspecified_with_bt709_matrix() { - let ci = ColorInfo::default(); - assert_eq!(ci, ColorInfo::UNSPECIFIED); - assert!(ci.primaries().is_unspecified()); - assert!(ci.matrix().is_bt_709()); - } - - #[test] - fn color_info_builders_chain() { - let ci = ColorInfo::UNSPECIFIED - .with_primaries(ColorPrimaries::Bt2020) - .with_transfer(ColorTransfer::SmpteSt2084Pq) - .with_matrix(ColorMatrix::Bt2020Ncl) - .with_range(ColorRange::Limited) - .with_chroma_location(ChromaLocation::Left); - assert!(ci.primaries().is_bt_2020()); - assert!(ci.transfer().is_smpte_st_2084_pq()); - assert!(ci.matrix().is_bt_2020_ncl()); - assert!(ci.range().is_limited()); - assert!(ci.chroma_location().is_left()); - } - - #[test] - fn color_info_setters_chain() { - let mut ci = ColorInfo::UNSPECIFIED; - ci.set_primaries(ColorPrimaries::Bt709) - .set_transfer(ColorTransfer::Bt709) - .set_matrix(ColorMatrix::Bt709) - .set_range(ColorRange::Limited) - .set_chroma_location(ChromaLocation::Left); - assert!(ci.primaries().is_bt_709()); - assert!(ci.range().is_limited()); - } - - #[test] - fn color_info_const_construction() { - const CI: ColorInfo = ColorInfo::new( - ColorPrimaries::Bt709, - ColorTransfer::Bt709, - ColorMatrix::Bt709, - ColorRange::Limited, - ChromaLocation::Left, - ); - assert!(CI.matrix().is_bt_709()); - } -} +//! Color metadata: re-exported from `videoframe::color`. +//! +//! mediadecode used to define these enums locally (per ITU-T H.273); +//! they now live in the lowest-layer `videoframe` crate so colconv, +//! mediadecode, and scenesdetect share a single canonical definition. +pub use videoframe::color::{ + ChromaLocation, ColorInfo, ColorMatrix, ColorPrimaries, ColorRange, ColorTransfer, +}; diff --git a/mediadecode/src/frame.rs b/mediadecode/src/frame.rs index e4678ad..638b165 100644 --- a/mediadecode/src/frame.rs +++ b/mediadecode/src/frame.rs @@ -1,255 +1,85 @@ //! Frame types and supporting building blocks. //! -//! `Rect` and `Plane` are the shared building blocks. The full -//! `VideoFrame` / `AudioFrame` / `SubtitleFrame` types land in later -//! tasks. - -use crate::{Timestamp, color::ColorInfo, subtitle::SubtitlePayload}; - -/// A `(width, height)` pair in pixels. -/// -/// Lives alongside the rest of the frame primitives in this module -/// because the same pair shows up everywhere a video stream is -/// described — the coded dimensions of a [`VideoFrame`], the -/// `coded_*` parameters a backend adapter takes when opening a -/// decoder, the per-plane layout helpers in the WebCodecs -/// adapter, etc. Passing it as a single struct rather than two -/// separate `u32` arguments removes a long-running footgun -/// (silent argument swap) and gives a natural place to hang -/// helpers like [`Self::is_zero`] or [`Self::Display`]. -/// -/// `u32` width / height matches WebCodecs' `coded_width` / -/// `coded_height` typing in `web_sys` and FFmpeg's -/// `AVCodecContext::width` / `height`. 65535×65535 (the smaller -/// `u16` packing some adjacent crates use) covers every realistic -/// resolution; the `u32` choice here keeps the public API plug- -/// compatible with both adapter typings. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Dimensions { - width: u32, - height: u32, -} - -impl Dimensions { - /// Constructs a `Dimensions` with the specified width and height - /// in pixels. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn new(width: u32, height: u32) -> Self { - Self { width, height } - } - - /// Returns the width in pixels. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn width(&self) -> u32 { - self.width - } +//! The frame structural primitives `Dimensions`, `Rect`, and `Plane` +//! are re-exported from `videoframe::frame` — they live in the lowest- +//! layer crate so colconv, mediadecode, and scenesdetect share a single +//! canonical definition. +//! +//! `VideoFrame`, `AudioFrame`, and +//! `SubtitleFrame` remain in mediadecode because they carry +//! timestamp + backend-extras layers that are mediadecode's domain +//! (`videoframe` stays the pure pixel-data layer). - /// Returns the height in pixels. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn height(&self) -> u32 { - self.height - } +pub use videoframe::frame::{Dimensions, Plane, Rect}; - /// Sets the width (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_width(mut self, width: u32) -> Self { - self.width = width; - self - } +use derive_more::IsVariant; +use thiserror::Error; - /// Sets the width in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_width(&mut self, width: u32) -> &mut Self { - self.width = width; - self - } - - /// Sets the height (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_height(mut self, height: u32) -> Self { - self.height = height; - self - } - - /// Sets the height in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_height(&mut self, height: u32) -> &mut Self { - self.height = height; - self - } - - /// Returns `true` when both width and height are zero — typically - /// the default-constructed / unset state. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn is_zero(&self) -> bool { - self.width == 0 && self.height == 0 - } -} +use crate::{Timestamp, color::ColorInfo, subtitle::SubtitlePayload}; -impl core::fmt::Display for Dimensions { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}x{}", self.width, self.height) - } +/// Errors returned by the `try_new` constructors on the frame types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Error)] +#[non_exhaustive] +pub enum FrameError { + /// `VideoFrame::try_new` was called with `plane_count > 4`. The + /// fixed plane array has exactly 4 slots; `plane_count` values + /// up to and including 4 are accepted, larger values would + /// later panic inside [`VideoFrame::planes`] far from the + /// construction site. See [`TooManyVideoPlanes`] for the + /// payload details. `#[from]` gives a free + /// `impl From for FrameError`, so inner + /// helpers that return `Result<_, TooManyVideoPlanes>` can be + /// `?`-propagated into `FrameError` directly. + #[error(transparent)] + TooManyVideoPlanes(#[from] TooManyVideoPlanes), + /// `AudioFrame::try_new` was called with `plane_count > 8`. The + /// fixed plane array has exactly 8 slots (matches FFmpeg's + /// `AV_NUM_DATA_POINTERS`). See [`TooManyAudioPlanes`] for the + /// payload details. `#[from]` gives a free + /// `impl From for FrameError`. + #[error(transparent)] + TooManyAudioPlanes(#[from] TooManyAudioPlanes), } -/// An axis-aligned integer rectangle. -/// -/// Used for `VideoFrame::visible_rect` (FFmpeg crop / -/// WebCodecs `visibleRect` / ProRes RAW `CleanAperture`). -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Rect { - x: u32, - y: u32, - width: u32, - height: u32, +/// Payload for [`FrameError::TooManyVideoPlanes`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)] +#[error("VideoFrame: plane_count {plane_count} exceeds the fixed 4-plane array")] +pub struct TooManyVideoPlanes { + /// The out-of-range `plane_count` value the caller supplied. + plane_count: u8, } -impl Rect { - /// Constructs a `Rect` at `(x, y)` with the given size. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self { - Self { - x, - y, - width, - height, - } - } - - /// Returns the X coordinate of the top-left corner. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn x(&self) -> u32 { - self.x - } - - /// Returns the Y coordinate of the top-left corner. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn y(&self) -> u32 { - self.y +impl TooManyVideoPlanes { + /// Constructs a new [`TooManyVideoPlanes`] payload. + #[inline] + pub const fn new(plane_count: u8) -> Self { + Self { plane_count } } - - /// Returns the width. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn width(&self) -> u32 { - self.width - } - - /// Returns the height. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn height(&self) -> u32 { - self.height - } - - /// Sets the X coordinate (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_x(mut self, x: u32) -> Self { - self.x = x; - self - } - /// Sets the Y coordinate (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_y(mut self, y: u32) -> Self { - self.y = y; - self - } - /// Sets the width (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_width(mut self, w: u32) -> Self { - self.width = w; - self - } - /// Sets the height (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_height(mut self, h: u32) -> Self { - self.height = h; - self - } - - /// Sets the X coordinate in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_x(&mut self, x: u32) -> &mut Self { - self.x = x; - self - } - /// Sets the Y coordinate in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_y(&mut self, y: u32) -> &mut Self { - self.y = y; - self - } - /// Sets the width in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_width(&mut self, w: u32) -> &mut Self { - self.width = w; - self - } - /// Sets the height in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_height(&mut self, h: u32) -> &mut Self { - self.height = h; - self + /// The out-of-range `plane_count` value the caller supplied. + #[inline] + pub const fn plane_count(&self) -> u8 { + self.plane_count } } -/// One plane of pixel or audio data. -/// -/// Generic over the buffer type `B` so the same `Plane` shape works -/// for owned (`Vec`, `bytes::Bytes`), borrowed (`&'a [u8]`), or -/// custom backend-supplied buffers. The bound `B: AsRef<[u8]>` lives -/// at the use site (`Frame>`); `Plane` itself is -/// unbounded so it can be used in const contexts. -/// -/// `stride` is bytes per row for video planes, total plane size in -/// bytes for audio planar formats. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Plane { - data: B, - stride: u32, +/// Payload for [`FrameError::TooManyAudioPlanes`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)] +#[error("AudioFrame: plane_count {plane_count} exceeds the fixed 8-plane array")] +pub struct TooManyAudioPlanes { + /// The out-of-range `plane_count` value the caller supplied. + plane_count: u8, } -impl Plane { - /// Constructs a `Plane` from a buffer and a stride. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn new(data: B, stride: u32) -> Self { - Self { data, stride } +impl TooManyAudioPlanes { + /// Constructs a new [`TooManyAudioPlanes`] payload. + #[inline] + pub const fn new(plane_count: u8) -> Self { + Self { plane_count } } - - /// Returns the stride in bytes. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn stride(&self) -> u32 { - self.stride - } - - /// Borrows the underlying buffer. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn data(&self) -> &B { - &self.data - } - - /// Mutably borrows the underlying buffer. - #[cfg_attr(not(tarpaulin), inline(always))] - pub fn data_mut(&mut self) -> &mut B { - &mut self.data - } - - /// Consumes the plane and returns the underlying buffer. - #[cfg_attr(not(tarpaulin), inline(always))] - pub fn into_data(self) -> B { - self.data - } - - /// Sets the stride (consuming builder). - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn with_stride(mut self, stride: u32) -> Self { - self.stride = stride; - self - } - - /// Sets the stride in place. - #[cfg_attr(not(tarpaulin), inline(always))] - pub const fn set_stride(&mut self, stride: u32) -> &mut Self { - self.stride = stride; - self + /// The out-of-range `plane_count` value the caller supplied. + #[inline] + pub const fn plane_count(&self) -> u8 { + self.plane_count } } @@ -296,7 +126,8 @@ impl VideoFrame { /// has four slots; passing a larger `plane_count` would later /// trip the slice indexing inside [`Self::planes`] far from /// the construction site. Asserting here fails fast with a - /// clear message instead. + /// clear message instead. Prefer [`Self::try_new`] when + /// `plane_count` can't be statically proven `<= 4`. #[cfg_attr(not(tarpaulin), inline(always))] pub const fn new( dimensions: Dimensions, @@ -322,6 +153,40 @@ impl VideoFrame { } } + /// Fallible counterpart to [`Self::new`]. Returns + /// [`FrameError::TooManyVideoPlanes`] when `plane_count > 4` + /// (the fixed plane array's capacity) rather than panicking. + /// + /// Not `const fn` — returning `Result` would require + /// dropping the moved generic-typed `planes` / `pixel_format` / + /// `extra` on the error branch, which the const evaluator + /// can't prove safe for arbitrary `P` / `E` / `D`. + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn try_new( + dimensions: Dimensions, + pixel_format: P, + planes: [Plane; 4], + plane_count: u8, + extra: E, + ) -> Result { + if plane_count as usize > 4 { + return Err(FrameError::TooManyVideoPlanes(TooManyVideoPlanes::new( + plane_count, + ))); + } + Ok(Self { + pts: None, + duration: None, + dimensions, + visible_rect: None, + pixel_format, + plane_count, + planes, + color: ColorInfo::UNSPECIFIED, + extra, + }) + } + /// Returns the presentation timestamp. #[cfg_attr(not(tarpaulin), inline(always))] pub const fn pts(&self) -> Option { @@ -394,24 +259,28 @@ impl VideoFrame { /// Sets the PTS (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_pts(mut self, v: Option) -> Self { self.pts = v; self } /// Sets the duration (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_duration(mut self, v: Option) -> Self { self.duration = v; self } /// Sets the visible rect (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_visible_rect(mut self, v: Option) -> Self { self.visible_rect = v; self } /// Sets the color metadata (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_color(mut self, v: ColorInfo) -> Self { self.color = v; self @@ -478,6 +347,9 @@ impl AudioFrame { /// has eight slots; passing a larger `plane_count` would /// later trip the slice indexing inside [`Self::planes`] far /// from the construction site. + /// + /// Prefer [`Self::try_new`] when `plane_count` can't be + /// statically proven `<= 8`. #[allow(clippy::too_many_arguments)] #[cfg_attr(not(tarpaulin), inline(always))] pub const fn new( @@ -508,6 +380,43 @@ impl AudioFrame { } } + /// Fallible counterpart to [`Self::new`]. Returns + /// [`FrameError::TooManyAudioPlanes`] when `plane_count > 8` + /// (the fixed plane array's capacity) rather than panicking. + /// + /// Not `const fn` — see the rationale on + /// [`VideoFrame::try_new`]. + #[allow(clippy::too_many_arguments)] + #[cfg_attr(not(tarpaulin), inline(always))] + pub fn try_new( + sample_rate: u32, + nb_samples: u32, + channel_count: u8, + sample_format: S, + channel_layout: C, + planes: [Plane; 8], + plane_count: u8, + extra: E, + ) -> Result { + if plane_count as usize > 8 { + return Err(FrameError::TooManyAudioPlanes(TooManyAudioPlanes::new( + plane_count, + ))); + } + Ok(Self { + pts: None, + duration: None, + sample_rate, + nb_samples, + channel_count, + sample_format, + channel_layout, + plane_count, + planes, + extra, + }) + } + /// Returns the presentation timestamp. #[cfg_attr(not(tarpaulin), inline(always))] pub const fn pts(&self) -> Option { @@ -566,12 +475,14 @@ impl AudioFrame { /// Sets the PTS (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_pts(mut self, v: Option) -> Self { self.pts = v; self } /// Sets the duration (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_duration(mut self, v: Option) -> Self { self.duration = v; self @@ -643,12 +554,14 @@ impl SubtitleFrame { /// Sets the PTS (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_pts(mut self, v: Option) -> Self { self.pts = v; self } /// Sets the duration (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_duration(mut self, v: Option) -> Self { self.duration = v; self @@ -672,6 +585,20 @@ impl SubtitleFrame { mod tests { use super::*; + use crate::{ + color::{ColorInfo, ColorMatrix}, + subtitle::SubtitlePayload, + }; + + fn empty_planes() -> [Plane<&'static [u8]>; 4] { + [ + Plane::new(&[][..], 0), + Plane::new(&[][..], 0), + Plane::new(&[][..], 0), + Plane::new(&[][..], 0), + ] + } + #[test] fn rect_construct_and_access() { let r = Rect::new(10, 20, 1920, 1080); @@ -736,20 +663,6 @@ mod tests { assert_eq!(recovered, &buf[..]); } - use crate::{ - color::{ColorInfo, ColorMatrix}, - subtitle::SubtitlePayload, - }; - - fn empty_planes() -> [Plane<&'static [u8]>; 4] { - [ - Plane::new(&[][..], 0), - Plane::new(&[][..], 0), - Plane::new(&[][..], 0), - Plane::new(&[][..], 0), - ] - } - #[test] fn video_frame_construct_and_access() { // VideoFrame: P=u32 (PixelFormat), E=VLoop (adapter ZST), @@ -810,19 +723,47 @@ mod tests { VideoFrame::new(Dimensions::new(64, 64), 0u32, empty_planes(), 5, ()); } + #[test] + fn video_frame_try_new_returns_err_for_too_many_planes() { + let res: Result, FrameError> = + VideoFrame::try_new(Dimensions::new(64, 64), 0u32, empty_planes(), 5, ()); + assert!(matches!( + res, + Err(FrameError::TooManyVideoPlanes(p)) if p.plane_count() == 5, + )); + } + + #[test] + fn video_frame_try_new_accepts_valid_plane_count() { + let f: VideoFrame = + VideoFrame::try_new(Dimensions::new(64, 64), 0u32, empty_planes(), 2, ()) + .expect("plane_count = 2 is within the 4-slot capacity"); + assert_eq!(f.plane_count(), 2); + } + #[test] #[should_panic(expected = "plane_count exceeds the fixed 8-plane array")] fn audio_frame_rejects_plane_count_above_array_size() { - let _f: AudioFrame = AudioFrame::new( - 48_000, - 1024, - 2, - 0u32, - 0u32, - audio_planes(), - 9, - (), - ); + let _f: AudioFrame = + AudioFrame::new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 9, ()); + } + + #[test] + fn audio_frame_try_new_returns_err_for_too_many_planes() { + let res: Result, FrameError> = + AudioFrame::try_new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 9, ()); + assert!(matches!( + res, + Err(FrameError::TooManyAudioPlanes(p)) if p.plane_count() == 9, + )); + } + + #[test] + fn audio_frame_try_new_accepts_valid_plane_count() { + let f: AudioFrame = + AudioFrame::try_new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 8, ()) + .expect("plane_count = 8 is the 8-slot capacity boundary"); + assert_eq!(f.plane_count(), 8); } #[test] diff --git a/mediadecode/src/packet.rs b/mediadecode/src/packet.rs index 5c594ec..c80aa70 100644 --- a/mediadecode/src/packet.rs +++ b/mediadecode/src/packet.rs @@ -110,24 +110,28 @@ impl VideoPacket { /// Sets the PTS (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_pts(mut self, v: Option) -> Self { self.pts = v; self } /// Sets the DTS (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_dts(mut self, v: Option) -> Self { self.dts = v; self } /// Sets the duration (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_duration(mut self, v: Option) -> Self { self.duration = v; self } /// Sets the flags (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_flags(mut self, v: PacketFlags) -> Self { self.flags = v; self @@ -231,24 +235,28 @@ impl AudioPacket { /// Sets the PTS (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_pts(mut self, v: Option) -> Self { self.pts = v; self } /// Sets the DTS (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_dts(mut self, v: Option) -> Self { self.dts = v; self } /// Sets the duration (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_duration(mut self, v: Option) -> Self { self.duration = v; self } /// Sets the flags (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_flags(mut self, v: PacketFlags) -> Self { self.flags = v; self @@ -345,18 +353,21 @@ impl SubtitlePacket { /// Sets the PTS (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_pts(mut self, v: Option) -> Self { self.pts = v; self } /// Sets the duration (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_duration(mut self, v: Option) -> Self { self.duration = v; self } /// Sets the flags (consuming builder). #[cfg_attr(not(tarpaulin), inline(always))] + #[must_use] pub const fn with_flags(mut self, v: PacketFlags) -> Self { self.flags = v; self diff --git a/mediadecode/src/pixel_format.rs b/mediadecode/src/pixel_format.rs index 5251e72..76beb1b 100644 --- a/mediadecode/src/pixel_format.rs +++ b/mediadecode/src/pixel_format.rs @@ -1,923 +1,17 @@ -//! Pixel format identifier — comprehensive coverage of FFmpeg's -//! `AVPixelFormat` enum plus Bayer mosaic and cinema-RAW formats. +//! Pixel format identifier: re-exported from +//! [`videoframe::pixel_format`]. //! -//! Naming convention: each variant's [`Display`] form is the -//! lowercase FFmpeg name where one exists (`yuv420p`, `nv12`, `p010le`, -//! …) so logs / wire formats line up with FFmpeg / `colconv`. The -//! variant identifier is the FFmpeg name in PascalCase -//! (`Yuv420p`, `Nv12`, `P010Le`, …). +//! mediadecode used to define this enum locally; the canonical +//! definition now lives in the lowest-layer `videoframe` crate so +//! colconv, mediadecode, and scenesdetect share a single +//! identifier. Backends consume the re-export via +//! `mediadecode::PixelFormat` or `mediadecode::pixel_format::PixelFormat` +//! exactly as before. //! -//! The enum covers: -//! - **Planar YUV** at 4:2:0 / 4:2:2 / 4:4:0 / 4:4:4, 8-bit and -//! high-bit-depth (9 / 10 / 12 / 14 / 16-bit). -//! - **Planar YUVA** (with alpha) at the same subsampling × bit-depth. -//! - **Semi-planar YUV** (NV-family) at 4:2:0 / 4:2:2 / 4:4:4, 8-bit -//! and 10 / 12 / 16-bit (P0xx / P2xx / P4xx). -//! - **Packed YUV** (yuyv / uyvy / yvyu / v210 / v410 / xv36 / Y2xx / -//! ayuv64 / vuya / vuyx). -//! - **Packed RGB** at 8-bit (rgb24 / bgr24 / rgba / bgra / argb / -//! abgr / rgbx / bgrx / xrgb / xbgr), low-bit (rgb444 / 555 / 565, -//! bgr444 / 555 / 565), and high-bit (rgb48 / bgr48 / rgba64 / bgra64 -//! / x2rgb10 / x2bgr10), plus float (rgbf16 / rgbf32). -//! - **Planar GBR / GBRA** at 8-bit + high-bit + float. -//! - **Greyscale** (gray8 / 9 / 10 / 12 / 14 / 16 / f32) and -//! greyscale-with-alpha (ya8 / ya16) and monochrome 1-bit -//! (monowhite / monoblack). -//! - **Bayer** (BGGR / RGGB / GBRG / GRBG) at 8 / 10 / 12 / 14 / 16-bit. -//! - **Paletted** (pal8). -//! -//! Hardware-frame markers (FFmpeg's `AV_PIX_FMT_VIDEOTOOLBOX` / -//! `_VAAPI` / `_CUDA` / `_D3D11` / `_DRM_PRIME` / `_MEDIACODEC` / -//! `_VULKAN`) are intentionally **not** in this enum: the unified -//! vocabulary describes CPU-side decoded pixel data, and a frame -//! carrying GPU-resident buffers must be transferred to a CPU format -//! before reaching a `mediadecode::VideoFrame` consumer. Backend -//! crates handle the HW path internally. -//! -//! Stable wire format: [`Self::to_u32`] returns the underlying -//! discriminant (this enum is `#[repr(u32)]`); [`Self::from_u32`] -//! reverses the mapping. Unrecognised values map to [`Self::Unknown`]. - -use derive_more::{Display, IsVariant}; - -/// Pixel format identifier covering FFmpeg + Bayer + cinema-RAW. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, IsVariant)] -#[non_exhaustive] -#[repr(u32)] -pub enum PixelFormat { - /// Unknown / unset format. - #[display("unknown")] - Unknown = 0, - - // =================================================================== - // Planar YUV 8-bit - // =================================================================== - /// Planar 4:2:0 YUV, 8-bit (`AV_PIX_FMT_YUV420P`). - #[display("yuv420p")] - Yuv420p = 100, - /// Planar 4:2:2 YUV, 8-bit. - #[display("yuv422p")] - Yuv422p = 101, - /// Planar 4:4:0 YUV, 8-bit (vertically subsampled chroma). - #[display("yuv440p")] - Yuv440p = 102, - /// Planar 4:4:4 YUV, 8-bit. - #[display("yuv444p")] - Yuv444p = 103, - /// Planar 4:1:1 YUV, 8-bit. - #[display("yuv411p")] - Yuv411p = 104, - /// Planar 4:1:0 YUV, 8-bit. - #[display("yuv410p")] - Yuv410p = 105, - - // =================================================================== - // Planar YUV high-bit-depth (4:2:0) - // =================================================================== - /// Planar 4:2:0 YUV, 9-bit little-endian. - #[display("yuv420p9le")] - Yuv420p9Le = 110, - /// Planar 4:2:0 YUV, 9-bit big-endian. - #[display("yuv420p9be")] - Yuv420p9Be = 111, - /// Planar 4:2:0 YUV, 10-bit little-endian. - #[display("yuv420p10le")] - Yuv420p10Le = 112, - /// Planar 4:2:0 YUV, 10-bit big-endian. - #[display("yuv420p10be")] - Yuv420p10Be = 113, - /// Planar 4:2:0 YUV, 12-bit little-endian. - #[display("yuv420p12le")] - Yuv420p12Le = 114, - /// Planar 4:2:0 YUV, 12-bit big-endian. - #[display("yuv420p12be")] - Yuv420p12Be = 115, - /// Planar 4:2:0 YUV, 14-bit little-endian. - #[display("yuv420p14le")] - Yuv420p14Le = 116, - /// Planar 4:2:0 YUV, 14-bit big-endian. - #[display("yuv420p14be")] - Yuv420p14Be = 117, - /// Planar 4:2:0 YUV, 16-bit little-endian. - #[display("yuv420p16le")] - Yuv420p16Le = 118, - /// Planar 4:2:0 YUV, 16-bit big-endian. - #[display("yuv420p16be")] - Yuv420p16Be = 119, - - // =================================================================== - // Planar YUV high-bit-depth (4:2:2) - // =================================================================== - /// Planar 4:2:2 YUV, 9-bit little-endian. - #[display("yuv422p9le")] - Yuv422p9Le = 120, - /// Planar 4:2:2 YUV, 9-bit big-endian. - #[display("yuv422p9be")] - Yuv422p9Be = 121, - /// Planar 4:2:2 YUV, 10-bit little-endian. - #[display("yuv422p10le")] - Yuv422p10Le = 122, - /// Planar 4:2:2 YUV, 10-bit big-endian. - #[display("yuv422p10be")] - Yuv422p10Be = 123, - /// Planar 4:2:2 YUV, 12-bit little-endian. - #[display("yuv422p12le")] - Yuv422p12Le = 124, - /// Planar 4:2:2 YUV, 12-bit big-endian. - #[display("yuv422p12be")] - Yuv422p12Be = 125, - /// Planar 4:2:2 YUV, 14-bit little-endian. - #[display("yuv422p14le")] - Yuv422p14Le = 126, - /// Planar 4:2:2 YUV, 14-bit big-endian. - #[display("yuv422p14be")] - Yuv422p14Be = 127, - /// Planar 4:2:2 YUV, 16-bit little-endian. - #[display("yuv422p16le")] - Yuv422p16Le = 128, - /// Planar 4:2:2 YUV, 16-bit big-endian. - #[display("yuv422p16be")] - Yuv422p16Be = 129, - - // =================================================================== - // Planar YUV high-bit-depth (4:4:0) - // =================================================================== - /// Planar 4:4:0 YUV, 10-bit little-endian. - #[display("yuv440p10le")] - Yuv440p10Le = 130, - /// Planar 4:4:0 YUV, 12-bit little-endian. - #[display("yuv440p12le")] - Yuv440p12Le = 131, - - // =================================================================== - // Planar YUV high-bit-depth (4:4:4) - // =================================================================== - /// Planar 4:4:4 YUV, 9-bit little-endian. - #[display("yuv444p9le")] - Yuv444p9Le = 140, - /// Planar 4:4:4 YUV, 9-bit big-endian. - #[display("yuv444p9be")] - Yuv444p9Be = 141, - /// Planar 4:4:4 YUV, 10-bit little-endian. - #[display("yuv444p10le")] - Yuv444p10Le = 142, - /// Planar 4:4:4 YUV, 10-bit big-endian. - #[display("yuv444p10be")] - Yuv444p10Be = 143, - /// Planar 4:4:4 YUV, 12-bit little-endian. - #[display("yuv444p12le")] - Yuv444p12Le = 144, - /// Planar 4:4:4 YUV, 12-bit big-endian. - #[display("yuv444p12be")] - Yuv444p12Be = 145, - /// Planar 4:4:4 YUV, 14-bit little-endian. - #[display("yuv444p14le")] - Yuv444p14Le = 146, - /// Planar 4:4:4 YUV, 14-bit big-endian. - #[display("yuv444p14be")] - Yuv444p14Be = 147, - /// Planar 4:4:4 YUV, 16-bit little-endian. - #[display("yuv444p16le")] - Yuv444p16Le = 148, - /// Planar 4:4:4 YUV, 16-bit big-endian. - #[display("yuv444p16be")] - Yuv444p16Be = 149, - - // =================================================================== - // Planar YUVA (with alpha) - // =================================================================== - /// Planar 4:2:0 YUVA, 8-bit. - #[display("yuva420p")] - Yuva420p = 200, - /// Planar 4:2:2 YUVA, 8-bit. - #[display("yuva422p")] - Yuva422p = 201, - /// Planar 4:4:4 YUVA, 8-bit. - #[display("yuva444p")] - Yuva444p = 202, - /// Planar 4:2:0 YUVA, 9-bit little-endian. - #[display("yuva420p9le")] - Yuva420p9Le = 203, - /// Planar 4:2:2 YUVA, 9-bit little-endian. - #[display("yuva422p9le")] - Yuva422p9Le = 204, - /// Planar 4:4:4 YUVA, 9-bit little-endian. - #[display("yuva444p9le")] - Yuva444p9Le = 205, - /// Planar 4:2:0 YUVA, 10-bit little-endian. - #[display("yuva420p10le")] - Yuva420p10Le = 206, - /// Planar 4:2:2 YUVA, 10-bit little-endian. - #[display("yuva422p10le")] - Yuva422p10Le = 207, - /// Planar 4:4:4 YUVA, 10-bit little-endian. - #[display("yuva444p10le")] - Yuva444p10Le = 208, - /// Planar 4:2:2 YUVA, 12-bit little-endian. - #[display("yuva422p12le")] - Yuva422p12Le = 209, - /// Planar 4:4:4 YUVA, 12-bit little-endian. - #[display("yuva444p12le")] - Yuva444p12Le = 210, - /// Planar 4:4:4 YUVA, 14-bit little-endian. - #[display("yuva444p14le")] - Yuva444p14Le = 211, - /// Planar 4:2:0 YUVA, 16-bit little-endian. - #[display("yuva420p16le")] - Yuva420p16Le = 212, - /// Planar 4:2:2 YUVA, 16-bit little-endian. - #[display("yuva422p16le")] - Yuva422p16Le = 213, - /// Planar 4:4:4 YUVA, 16-bit little-endian. - #[display("yuva444p16le")] - Yuva444p16Le = 214, - /// Planar 4:2:0 YUVA, 12-bit little-endian - /// (`AV_PIX_FMT_YUVA420P12LE`). Discriminant placed after - /// the 16-bit block because the 12-bit slot in the original - /// 200-series numbering (between 10Le at 206 and the 4:2:2 - /// 12Le at 209) was already taken by the 4:2:2 / 4:4:4 - /// 12Le forms; adding a new tail slot keeps existing - /// discriminants stable. Surfaced by WebCodecs as the - /// `I420AP12` `VideoPixelFormat`. - #[display("yuva420p12le")] - Yuva420p12Le = 215, - - // =================================================================== - // Semi-planar YUV (NV-family) — 8-bit - // =================================================================== - /// 4:2:0 semi-planar Y plane + interleaved Cb/Cr (`AV_PIX_FMT_NV12`). - #[display("nv12")] - Nv12 = 300, - /// 4:2:0 semi-planar Y + interleaved Cr/Cb (`AV_PIX_FMT_NV21`). - #[display("nv21")] - Nv21 = 301, - /// 4:2:2 semi-planar Y + interleaved Cb/Cr. - #[display("nv16")] - Nv16 = 302, - /// 4:4:4 semi-planar Y + interleaved Cb/Cr. - #[display("nv24")] - Nv24 = 303, - /// 4:4:4 semi-planar Y + interleaved Cr/Cb. - #[display("nv42")] - Nv42 = 304, - - // =================================================================== - // Semi-planar YUV high-bit-depth (P0xx / P2xx / P4xx) - // =================================================================== - /// 4:2:0 semi-planar 10-bit, little-endian (`AV_PIX_FMT_P010LE`). - #[display("p010le")] - P010Le = 310, - /// 4:2:0 semi-planar 10-bit, big-endian. - #[display("p010be")] - P010Be = 311, - /// 4:2:0 semi-planar 12-bit, little-endian. - #[display("p012le")] - P012Le = 312, - /// 4:2:0 semi-planar 16-bit, little-endian. - #[display("p016le")] - P016Le = 313, - /// 4:2:2 semi-planar 10-bit, little-endian. - #[display("p210le")] - P210Le = 314, - /// 4:2:2 semi-planar 12-bit, little-endian (FFmpeg 5.1+). - #[display("p212le")] - P212Le = 315, - /// 4:2:2 semi-planar 16-bit, little-endian. - #[display("p216le")] - P216Le = 316, - /// 4:4:4 semi-planar 10-bit, little-endian. - #[display("p410le")] - P410Le = 317, - /// 4:4:4 semi-planar 12-bit, little-endian (FFmpeg 5.1+). - #[display("p412le")] - P412Le = 318, - /// 4:4:4 semi-planar 16-bit, little-endian. - #[display("p416le")] - P416Le = 319, - - // =================================================================== - // Packed YUV 8-bit - // =================================================================== - /// 4:2:2 packed YUV: Y0 U Y1 V (`AV_PIX_FMT_YUYV422`). - #[display("yuyv422")] - Yuyv422 = 400, - /// 4:2:2 packed YUV: U Y0 V Y1 (`AV_PIX_FMT_UYVY422`). - #[display("uyvy422")] - Uyvy422 = 401, - /// 4:2:2 packed YUV: Y0 V Y1 U (`AV_PIX_FMT_YVYU422`). - #[display("yvyu422")] - Yvyu422 = 402, - - // =================================================================== - // Packed YUV high-bit-depth - // =================================================================== - /// 4:2:2 packed YUV 10-bit (`AV_PIX_FMT_Y210LE`). - #[display("y210le")] - Y210Le = 410, - /// 4:2:2 packed YUV 12-bit (`AV_PIX_FMT_Y212LE`). - #[display("y212le")] - Y212Le = 411, - /// 4:2:2 packed YUV 16-bit (`AV_PIX_FMT_Y216LE`). - #[display("y216le")] - Y216Le = 412, - /// 4:2:2 packed 10-bit, 3 samples per 32-bit word (`AV_PIX_FMT_V210`). - #[display("v210")] - V210 = 413, - /// 4:4:4 packed 10-bit, one 32-bit word per sample (`AV_PIX_FMT_V410LE`). - #[display("v410le")] - V410Le = 414, - /// 4:4:4 packed 10-bit, alternative layout. - #[display("v30xle")] - V30xLe = 415, - /// 4:4:4 packed 12-bit, one 16-bit word per channel (`AV_PIX_FMT_XV36LE`). - #[display("xv36le")] - Xv36Le = 416, - /// 4:4:4 packed 8-bit byte quadruple V, U, Y, A (`AV_PIX_FMT_VUYA`). - #[display("vuya")] - Vuya = 417, - /// 4:4:4 packed 8-bit V, U, Y, X (alpha-as-padding). - #[display("vuyx")] - Vuyx = 418, - /// 4:4:4 packed 16-bit word quadruple A, Y, U, V (`AV_PIX_FMT_AYUV64LE`). - #[display("ayuv64le")] - Ayuv64Le = 419, - - // =================================================================== - // Packed RGB 8-bit - // =================================================================== - /// 24-bit packed RGB (`AV_PIX_FMT_RGB24`). - #[display("rgb24")] - Rgb24 = 500, - /// 24-bit packed BGR. - #[display("bgr24")] - Bgr24 = 501, - /// 32-bit packed RGBA. - #[display("rgba")] - Rgba = 502, - /// 32-bit packed BGRA. - #[display("bgra")] - Bgra = 503, - /// 32-bit packed ARGB. - #[display("argb")] - Argb = 504, - /// 32-bit packed ABGR. - #[display("abgr")] - Abgr = 505, - /// 32-bit packed RGB with X (unused) byte. - #[display("rgbx")] - Rgbx = 506, - /// 32-bit packed BGR with X (unused) byte. - #[display("bgrx")] - Bgrx = 507, - /// 32-bit packed XRGB (X unused, then RGB). - #[display("xrgb")] - Xrgb = 508, - /// 32-bit packed XBGR. - #[display("xbgr")] - Xbgr = 509, - /// 32-bit RGB10 in low bits, 2 bits unused (`AV_PIX_FMT_X2RGB10LE`). - #[display("x2rgb10le")] - X2Rgb10Le = 510, - /// 32-bit BGR10 in low bits, 2 bits unused. - #[display("x2bgr10le")] - X2Bgr10Le = 511, - - // =================================================================== - // Packed RGB low-bit - // =================================================================== - /// 16-bit packed RGB, 4 bits per channel + 4 unused. - #[display("rgb444le")] - Rgb444Le = 520, - /// 16-bit packed BGR, 4 bits per channel + 4 unused. - #[display("bgr444le")] - Bgr444Le = 521, - /// 16-bit packed RGB, 5/5/5 layout. - #[display("rgb555le")] - Rgb555Le = 522, - /// 16-bit packed BGR, 5/5/5 layout. - #[display("bgr555le")] - Bgr555Le = 523, - /// 16-bit packed RGB, 5/6/5 layout. - #[display("rgb565le")] - Rgb565Le = 524, - /// 16-bit packed BGR, 5/6/5 layout. - #[display("bgr565le")] - Bgr565Le = 525, - - // =================================================================== - // Packed RGB high-bit-depth - // =================================================================== - /// 48-bit packed RGB, 16 bits per channel, little-endian. - #[display("rgb48le")] - Rgb48Le = 530, - /// 48-bit packed BGR, 16 bits per channel, little-endian. - #[display("bgr48le")] - Bgr48Le = 531, - /// 64-bit packed RGBA, 16 bits per channel. - #[display("rgba64le")] - Rgba64Le = 532, - /// 64-bit packed BGRA, 16 bits per channel. - #[display("bgra64le")] - Bgra64Le = 533, - - // =================================================================== - // Packed RGB float - // =================================================================== - /// 48-bit packed RGB, 16-bit half-float per channel. - #[display("rgbf16")] - Rgbf16 = 540, - /// 96-bit packed RGB, 32-bit float per channel. - #[display("rgbf32")] - Rgbf32 = 541, - - // =================================================================== - // Planar GBR 8-bit - // =================================================================== - /// Planar 4:4:4 G/B/R, 8-bit. - #[display("gbrp")] - Gbrp = 600, - /// Planar 4:4:4 G/B/R, 9-bit little-endian. - #[display("gbrp9le")] - Gbrp9Le = 601, - /// Planar 4:4:4 G/B/R, 10-bit little-endian. - #[display("gbrp10le")] - Gbrp10Le = 602, - /// Planar 4:4:4 G/B/R, 12-bit little-endian. - #[display("gbrp12le")] - Gbrp12Le = 603, - /// Planar 4:4:4 G/B/R, 14-bit little-endian. - #[display("gbrp14le")] - Gbrp14Le = 604, - /// Planar 4:4:4 G/B/R, 16-bit little-endian. - #[display("gbrp16le")] - Gbrp16Le = 605, - /// Planar 4:4:4 G/B/R, 16-bit half-float. - #[display("gbrpf16")] - Gbrpf16 = 606, - /// Planar 4:4:4 G/B/R, 32-bit float. - #[display("gbrpf32")] - Gbrpf32 = 607, - - // =================================================================== - // Planar GBRA (with alpha) - // =================================================================== - /// Planar 4:4:4 G/B/R/A, 8-bit. - #[display("gbrap")] - Gbrap = 620, - /// Planar 4:4:4 G/B/R/A, 10-bit little-endian. - #[display("gbrap10le")] - Gbrap10Le = 621, - /// Planar 4:4:4 G/B/R/A, 12-bit little-endian. - #[display("gbrap12le")] - Gbrap12Le = 622, - /// Planar 4:4:4 G/B/R/A, 14-bit little-endian. - #[display("gbrap14le")] - Gbrap14Le = 623, - /// Planar 4:4:4 G/B/R/A, 16-bit little-endian. - #[display("gbrap16le")] - Gbrap16Le = 624, - /// Planar 4:4:4 G/B/R/A, 16-bit half-float. - #[display("gbrapf16")] - Gbrapf16 = 625, - /// Planar 4:4:4 G/B/R/A, 32-bit float. - #[display("gbrapf32")] - Gbrapf32 = 626, - - // =================================================================== - // Greyscale - // =================================================================== - /// 8-bit greyscale (`AV_PIX_FMT_GRAY8`). - #[display("gray8")] - Gray8 = 700, - /// 9-bit greyscale, little-endian. - #[display("gray9le")] - Gray9Le = 701, - /// 10-bit greyscale, little-endian. - #[display("gray10le")] - Gray10Le = 702, - /// 12-bit greyscale, little-endian. - #[display("gray12le")] - Gray12Le = 703, - /// 14-bit greyscale, little-endian. - #[display("gray14le")] - Gray14Le = 704, - /// 16-bit greyscale, little-endian. - #[display("gray16le")] - Gray16Le = 705, - /// 32-bit float greyscale. - #[display("grayf32")] - Grayf32 = 706, - /// 16-bit greyscale-with-alpha. - #[display("ya8")] - Ya8 = 710, - /// 32-bit greyscale-with-alpha. - #[display("ya16le")] - Ya16Le = 711, - - // =================================================================== - // Monochrome 1-bit - // =================================================================== - /// 1-bit monochrome, white = 0 (`AV_PIX_FMT_MONOWHITE`). - #[display("monowhite")] - Monowhite = 720, - /// 1-bit monochrome, black = 0 (`AV_PIX_FMT_MONOBLACK`). - #[display("monoblack")] - Monoblack = 721, - - // =================================================================== - // Paletted - // =================================================================== - /// Paletted 8-bit (`AV_PIX_FMT_PAL8`). - #[display("pal8")] - Pal8 = 800, - - // =================================================================== - // Bayer - // =================================================================== - /// Bayer BGGR pattern, 8-bit. - #[display("bayer_bggr8")] - BayerBggr8 = 900, - /// Bayer RGGB pattern, 8-bit. - #[display("bayer_rggb8")] - BayerRggb8 = 901, - /// Bayer GBRG pattern, 8-bit. - #[display("bayer_gbrg8")] - BayerGbrg8 = 902, - /// Bayer GRBG pattern, 8-bit. - #[display("bayer_grbg8")] - BayerGrbg8 = 903, - /// Bayer BGGR pattern, 10-bit little-endian (low-packed in u16). - #[display("bayer_bggr10le")] - BayerBggr10Le = 910, - /// Bayer RGGB pattern, 10-bit little-endian. - #[display("bayer_rggb10le")] - BayerRggb10Le = 911, - /// Bayer GBRG pattern, 10-bit little-endian. - #[display("bayer_gbrg10le")] - BayerGbrg10Le = 912, - /// Bayer GRBG pattern, 10-bit little-endian. - #[display("bayer_grbg10le")] - BayerGrbg10Le = 913, - /// Bayer BGGR pattern, 12-bit little-endian. - #[display("bayer_bggr12le")] - BayerBggr12Le = 920, - /// Bayer RGGB pattern, 12-bit little-endian. - #[display("bayer_rggb12le")] - BayerRggb12Le = 921, - /// Bayer GBRG pattern, 12-bit little-endian. - #[display("bayer_gbrg12le")] - BayerGbrg12Le = 922, - /// Bayer GRBG pattern, 12-bit little-endian. - #[display("bayer_grbg12le")] - BayerGrbg12Le = 923, - /// Bayer BGGR pattern, 14-bit little-endian. - #[display("bayer_bggr14le")] - BayerBggr14Le = 930, - /// Bayer RGGB pattern, 14-bit little-endian. - #[display("bayer_rggb14le")] - BayerRggb14Le = 931, - /// Bayer GBRG pattern, 14-bit little-endian. - #[display("bayer_gbrg14le")] - BayerGbrg14Le = 932, - /// Bayer GRBG pattern, 14-bit little-endian. - #[display("bayer_grbg14le")] - BayerGrbg14Le = 933, - /// Bayer BGGR pattern, 16-bit little-endian. - #[display("bayer_bggr16le")] - BayerBggr16Le = 940, - /// Bayer RGGB pattern, 16-bit little-endian. - #[display("bayer_rggb16le")] - BayerRggb16Le = 941, - /// Bayer GBRG pattern, 16-bit little-endian. - #[display("bayer_gbrg16le")] - BayerGbrg16Le = 942, - /// Bayer GRBG pattern, 16-bit little-endian. - #[display("bayer_grbg16le")] - BayerGrbg16Le = 943, -} - -impl Default for PixelFormat { - #[inline] - fn default() -> Self { - Self::Unknown - } -} - -impl PixelFormat { - /// Stable wire representation. Returns the underlying `repr(u32)` - /// discriminant. - #[inline] - pub const fn to_u32(self) -> u32 { - self as u32 - } - - /// Decodes from the stable `u32` representation produced by - /// [`Self::to_u32`]. Unrecognised values map to [`Self::Unknown`]. - #[inline] - pub const fn from_u32(value: u32) -> Self { - match value { - // Planar YUV 8-bit. - 100 => Self::Yuv420p, - 101 => Self::Yuv422p, - 102 => Self::Yuv440p, - 103 => Self::Yuv444p, - 104 => Self::Yuv411p, - 105 => Self::Yuv410p, - // Planar YUV high-bit-depth (4:2:0). - 110 => Self::Yuv420p9Le, - 111 => Self::Yuv420p9Be, - 112 => Self::Yuv420p10Le, - 113 => Self::Yuv420p10Be, - 114 => Self::Yuv420p12Le, - 115 => Self::Yuv420p12Be, - 116 => Self::Yuv420p14Le, - 117 => Self::Yuv420p14Be, - 118 => Self::Yuv420p16Le, - 119 => Self::Yuv420p16Be, - // Planar YUV high-bit-depth (4:2:2). - 120 => Self::Yuv422p9Le, - 121 => Self::Yuv422p9Be, - 122 => Self::Yuv422p10Le, - 123 => Self::Yuv422p10Be, - 124 => Self::Yuv422p12Le, - 125 => Self::Yuv422p12Be, - 126 => Self::Yuv422p14Le, - 127 => Self::Yuv422p14Be, - 128 => Self::Yuv422p16Le, - 129 => Self::Yuv422p16Be, - // Planar YUV (4:4:0). - 130 => Self::Yuv440p10Le, - 131 => Self::Yuv440p12Le, - // Planar YUV high-bit-depth (4:4:4). - 140 => Self::Yuv444p9Le, - 141 => Self::Yuv444p9Be, - 142 => Self::Yuv444p10Le, - 143 => Self::Yuv444p10Be, - 144 => Self::Yuv444p12Le, - 145 => Self::Yuv444p12Be, - 146 => Self::Yuv444p14Le, - 147 => Self::Yuv444p14Be, - 148 => Self::Yuv444p16Le, - 149 => Self::Yuv444p16Be, - // Planar YUVA. - 200 => Self::Yuva420p, - 201 => Self::Yuva422p, - 202 => Self::Yuva444p, - 203 => Self::Yuva420p9Le, - 204 => Self::Yuva422p9Le, - 205 => Self::Yuva444p9Le, - 206 => Self::Yuva420p10Le, - 207 => Self::Yuva422p10Le, - 208 => Self::Yuva444p10Le, - 209 => Self::Yuva422p12Le, - 210 => Self::Yuva444p12Le, - 211 => Self::Yuva444p14Le, - 212 => Self::Yuva420p16Le, - 213 => Self::Yuva422p16Le, - 214 => Self::Yuva444p16Le, - 215 => Self::Yuva420p12Le, - // Semi-planar YUV. - 300 => Self::Nv12, - 301 => Self::Nv21, - 302 => Self::Nv16, - 303 => Self::Nv24, - 304 => Self::Nv42, - // Semi-planar YUV high-bit-depth. - 310 => Self::P010Le, - 311 => Self::P010Be, - 312 => Self::P012Le, - 313 => Self::P016Le, - 314 => Self::P210Le, - 315 => Self::P212Le, - 316 => Self::P216Le, - 317 => Self::P410Le, - 318 => Self::P412Le, - 319 => Self::P416Le, - // Packed YUV 8-bit. - 400 => Self::Yuyv422, - 401 => Self::Uyvy422, - 402 => Self::Yvyu422, - // Packed YUV high-bit-depth. - 410 => Self::Y210Le, - 411 => Self::Y212Le, - 412 => Self::Y216Le, - 413 => Self::V210, - 414 => Self::V410Le, - 415 => Self::V30xLe, - 416 => Self::Xv36Le, - 417 => Self::Vuya, - 418 => Self::Vuyx, - 419 => Self::Ayuv64Le, - // Packed RGB 8-bit. - 500 => Self::Rgb24, - 501 => Self::Bgr24, - 502 => Self::Rgba, - 503 => Self::Bgra, - 504 => Self::Argb, - 505 => Self::Abgr, - 506 => Self::Rgbx, - 507 => Self::Bgrx, - 508 => Self::Xrgb, - 509 => Self::Xbgr, - 510 => Self::X2Rgb10Le, - 511 => Self::X2Bgr10Le, - // Packed RGB low-bit. - 520 => Self::Rgb444Le, - 521 => Self::Bgr444Le, - 522 => Self::Rgb555Le, - 523 => Self::Bgr555Le, - 524 => Self::Rgb565Le, - 525 => Self::Bgr565Le, - // Packed RGB high-bit. - 530 => Self::Rgb48Le, - 531 => Self::Bgr48Le, - 532 => Self::Rgba64Le, - 533 => Self::Bgra64Le, - // Packed RGB float. - 540 => Self::Rgbf16, - 541 => Self::Rgbf32, - // Planar GBR. - 600 => Self::Gbrp, - 601 => Self::Gbrp9Le, - 602 => Self::Gbrp10Le, - 603 => Self::Gbrp12Le, - 604 => Self::Gbrp14Le, - 605 => Self::Gbrp16Le, - 606 => Self::Gbrpf16, - 607 => Self::Gbrpf32, - // Planar GBRA. - 620 => Self::Gbrap, - 621 => Self::Gbrap10Le, - 622 => Self::Gbrap12Le, - 623 => Self::Gbrap14Le, - 624 => Self::Gbrap16Le, - 625 => Self::Gbrapf16, - 626 => Self::Gbrapf32, - // Greyscale. - 700 => Self::Gray8, - 701 => Self::Gray9Le, - 702 => Self::Gray10Le, - 703 => Self::Gray12Le, - 704 => Self::Gray14Le, - 705 => Self::Gray16Le, - 706 => Self::Grayf32, - 710 => Self::Ya8, - 711 => Self::Ya16Le, - // Monochrome. - 720 => Self::Monowhite, - 721 => Self::Monoblack, - // Paletted. - 800 => Self::Pal8, - // Bayer. - 900 => Self::BayerBggr8, - 901 => Self::BayerRggb8, - 902 => Self::BayerGbrg8, - 903 => Self::BayerGrbg8, - 910 => Self::BayerBggr10Le, - 911 => Self::BayerRggb10Le, - 912 => Self::BayerGbrg10Le, - 913 => Self::BayerGrbg10Le, - 920 => Self::BayerBggr12Le, - 921 => Self::BayerRggb12Le, - 922 => Self::BayerGbrg12Le, - 923 => Self::BayerGrbg12Le, - 930 => Self::BayerBggr14Le, - 931 => Self::BayerRggb14Le, - 932 => Self::BayerGbrg14Le, - 933 => Self::BayerGrbg14Le, - 940 => Self::BayerBggr16Le, - 941 => Self::BayerRggb16Le, - 942 => Self::BayerGbrg16Le, - 943 => Self::BayerGrbg16Le, - _ => Self::Unknown, - } - } - - /// Returns `true` for Bayer-mosaic formats (any pattern, any bit - /// depth). Bayer frames carry undebayered sensor data; downstream - /// consumers (e.g. `colconv::raw`) demosaic + white-balance + colour- - /// correct to produce RGB. - #[inline] - pub const fn is_bayer(self) -> bool { - matches!( - self, - Self::BayerBggr8 - | Self::BayerRggb8 - | Self::BayerGbrg8 - | Self::BayerGrbg8 - | Self::BayerBggr10Le - | Self::BayerRggb10Le - | Self::BayerGbrg10Le - | Self::BayerGrbg10Le - | Self::BayerBggr12Le - | Self::BayerRggb12Le - | Self::BayerGbrg12Le - | Self::BayerGrbg12Le - | Self::BayerBggr14Le - | Self::BayerRggb14Le - | Self::BayerGbrg14Le - | Self::BayerGrbg14Le - | Self::BayerBggr16Le - | Self::BayerRggb16Le - | Self::BayerGbrg16Le - | Self::BayerGrbg16Le, - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn default_is_unknown() { - assert!(matches!(PixelFormat::default(), PixelFormat::Unknown)); - } - - #[test] - fn round_trip_u32_for_known_variants() { - let all = [ - PixelFormat::Unknown, - PixelFormat::Yuv420p, - PixelFormat::Yuv444p, - PixelFormat::Yuv420p10Le, - PixelFormat::Yuv422p16Le, - PixelFormat::Yuva444p, - PixelFormat::Nv12, - PixelFormat::P010Le, - PixelFormat::P416Le, - PixelFormat::Yuyv422, - PixelFormat::V210, - PixelFormat::Ayuv64Le, - PixelFormat::Rgb24, - PixelFormat::Bgra, - PixelFormat::Rgb565Le, - PixelFormat::Rgba64Le, - PixelFormat::Rgbf32, - PixelFormat::Gbrp, - PixelFormat::Gbrap16Le, - PixelFormat::Gbrapf32, - PixelFormat::Gray8, - PixelFormat::Gray16Le, - PixelFormat::Ya16Le, - PixelFormat::Monowhite, - PixelFormat::Pal8, - PixelFormat::BayerBggr8, - PixelFormat::BayerRggb16Le, - ]; - for fmt in all { - assert_eq!( - PixelFormat::from_u32(fmt.to_u32()), - fmt, - "round-trip failed for {fmt:?}" - ); - } - } - - #[test] - fn unknown_for_garbage_u32() { - assert_eq!(PixelFormat::from_u32(99_999), PixelFormat::Unknown); - assert_eq!(PixelFormat::from_u32(1), PixelFormat::Unknown); - } - - // `format!` requires an allocator; gate to alloc-or-std builds. - // The `Display` impl itself works in bare-core mode via - // `write!`-style sinks — only this test's assertion strategy needs - // alloc. - #[cfg(any(feature = "alloc", feature = "std"))] - #[test] - fn display_uses_ffmpeg_lowercase_names() { - assert_eq!(format!("{}", PixelFormat::Yuv420p), "yuv420p"); - assert_eq!(format!("{}", PixelFormat::Nv12), "nv12"); - assert_eq!(format!("{}", PixelFormat::P010Le), "p010le"); - assert_eq!(format!("{}", PixelFormat::Rgba64Le), "rgba64le"); - assert_eq!(format!("{}", PixelFormat::BayerBggr12Le), "bayer_bggr12le"); - assert_eq!(format!("{}", PixelFormat::Unknown), "unknown"); - } - - #[test] - fn is_bayer_partition() { - assert!(PixelFormat::BayerBggr8.is_bayer()); - assert!(PixelFormat::BayerRggb16Le.is_bayer()); - assert!(PixelFormat::BayerGrbg12Le.is_bayer()); - assert!(!PixelFormat::Yuv420p.is_bayer()); - assert!(!PixelFormat::Rgb24.is_bayer()); - assert!(!PixelFormat::Unknown.is_bayer()); - } - - #[test] - fn is_variant_helpers_compile() { - assert!(PixelFormat::Yuv420p.is_yuv_420_p()); - assert!(PixelFormat::Nv12.is_nv_12()); - assert!(PixelFormat::P010Le.is_p_010_le()); - assert!(!PixelFormat::Yuv420p.is_unknown()); - } +//! Note: the videoframe variant for unrecognized wire values is +//! `Unknown(u32)` (preserves the raw integer for lossless round-trip), +//! not the prior unit-variant `Unknown`. Mediadecode backends fall +//! through to `PixelFormat::Unknown(raw as u32)` when an FFmpeg / +//! WebCodecs identifier doesn't map to a known format. - #[test] - fn copy_and_eq() { - let p = PixelFormat::Nv12; - let q = p; // Copy - assert_eq!(p, q); - assert_ne!(p, PixelFormat::Yuv420p); - } -} +pub use videoframe::pixel_format::PixelFormat;