Skip to main content

mediadecode_ffmpeg/
image.rs

1//! [`mediadecode::decoder::ImageDecoder`] impl backed by
2//! `libavcodec` — the cover-art road.
3//!
4//! A container's still images are not video, and this crate's demux
5//! tier already says so: a video-shaped stream carrying
6//! `AV_DISPOSITION_ATTACHED_PIC` is reclassified to
7//! [`TrackKind::Attachment`](mediadecode::demuxer::TrackKind::Attachment),
8//! and its one packet — the picture, whole — is queued at open. What
9//! was missing was the other half: something to turn those bytes back
10//! into pixels.
11//!
12//! # The codec identity survives the reclassification
13//!
14//! Nothing about the reclassification loses the picture's codec.
15//! [`TrackParams::Attachment`](mediadecode::demuxer::TrackParams::Attachment)
16//! carries the `AVCodecParameters.codec_id` the stream declared
17//! (`mjpeg`, `png`, `bmp`, …), and
18//! [`TrackExtra`](crate::extras::TrackExtra) carries a deep,
19//! checked copy of the whole `AVCodecParameters` — width, height,
20//! pixel format and extradata included. Those parameters are exactly
21//! what [`FfmpegImageDecoder::open`] wants, so the road from a track
22//! row to a decoded picture has no gap in it:
23//!
24//! ```no_run
25//! use mediadecode::{
26//!   decoder::ImageDecoder,
27//!   demuxer::{Demuxer, DemuxedPacket, TrackKind},
28//! };
29//! use mediadecode_ffmpeg::{FfmpegOwnedDemuxer, FfmpegOwnedImageDecoder, DecoderLimits};
30//!
31//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! // The owned lane: this decoder takes owned attachments, and a
33//! // cover is a picture a caller usually keeps.
34//! let mut demuxer = FfmpegOwnedDemuxer::open("song.mp3")?;
35//! let cover_track = demuxer
36//!   .tracks()
37//!   .iter()
38//!   .position(|t| t.kind() == TrackKind::Attachment)
39//!   .expect("this file has cover art");
40//! let parameters = demuxer.tracks()[cover_track].extra().clone_parameters()?;
41//!
42//! while let Some(packet) = demuxer.next_packet()? {
43//!   if let DemuxedPacket::Attachment(attachment) = packet {
44//!     let mut decoder = FfmpegOwnedImageDecoder::open(parameters, DecoderLimits::default())?;
45//!     let image = decoder.decode(attachment.packet())?;
46//!     println!("{}x{}", image.width(), image.height());
47//!     break;
48//!   }
49//! }
50//! # Ok(())
51//! # }
52//! ```
53//!
54//! # One shot, and the decoder stays open
55//!
56//! [`ImageDecoder::decode`] takes a packet and answers a picture; there
57//! is no `send` / `receive` split, because an attachment track's
58//! contract is exactly one packet and a still codec's answer to it is
59//! exactly one frame. The `AVCodecContext` is nonetheless kept and
60//! reset between calls rather than reopened: a container with a dozen
61//! cover images (Matroska attachments, timed thumbnails exported by
62//! hand) decodes them through one decoder.
63
64use derive_more::{IsVariant, TryUnwrap, Unwrap};
65use ffmpeg_next::{codec::Parameters, frame};
66use mediadecode::{
67  PixelFormat, decoder::ImageDecoder, demuxer::AttachmentPacket, frame::ImageFrame,
68  packet::PacketFlags as MdPacketFlags,
69};
70
71use crate::{
72  DecoderLimits, Error, Ffmpeg, boundary,
73  convert::{self, ConvertError},
74  decoder::{build_codec_context, ensure_video_codec_type, find_decoder},
75  extras::{AttachmentPacketExtra, ImageFrameExtra},
76  frame::alloc_av_video_frame,
77};
78
79/// `mediadecode::ImageDecoder` impl wrapping `ffmpeg::decoder::Video`.
80///
81/// Opened from the codec parameters of an
82/// [`Attachment`](mediadecode::demuxer::TrackKind::Attachment) track —
83/// see the [module docs](self) for the whole road.
84pub struct CarrierImageDecoder<C: crate::FfmpegCarrier> {
85  decoder: ffmpeg_next::decoder::Video,
86  scratch: frame::Video,
87  limits: DecoderLimits,
88  /// Keeps the [`CallbackState`](crate::ffi::CallbackState) alive for as
89  /// long as the codec context that points at it.
90  ///
91  /// Declared **after** the decoder on purpose: struct fields drop in
92  /// declaration order, so the `AVCodecContext` is freed first and the
93  /// state it references outlives it.
94  _callback_state: Box<crate::ffi::CallbackState>,
95  /// The lane this decoder captures into. A marker: the carrier
96  /// appears in the frames it produces, not in its own state.
97  _carrier: core::marker::PhantomData<C>,
98}
99
100impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierImageDecoder<C> {
101  /// Opens a still-image decoder for the given codec parameters.
102  ///
103  /// The parameters come from an attachment track's
104  /// [`TrackExtra::clone_parameters`](crate::extras::TrackExtra::clone_parameters),
105  /// which is a checked deep copy with no tie back to the format
106  /// context — so the decoder outlives the demuxer that named it.
107  ///
108  /// A **still** codec is what this is for, but nothing here refuses a
109  /// motion one: `mjpeg`, `png` and `h264` open through the same call,
110  /// and a decoder opened on a motion codec will answer with that
111  /// codec's first picture. Refusing by codec id would mean minting a
112  /// roster of "image codecs" this crate has no business owning — the
113  /// container already said the track is an attachment, and that is
114  /// the judgement that matters.
115  ///
116  /// `limits` bounds what one decoded picture may cost — **a cover-art
117  /// bomb is the same attack as a video bomb**, and this seam is the
118  /// more exposed of the two: an attachment is decoded from a payload a
119  /// container handed over eagerly, often by a thumbnailer that never
120  /// asked for video at all. [`DecoderLimits::max_pixels`] is written
121  /// into the `AVCodecContext` opened here, so libavcodec refuses an
122  /// oversized still before allocating it; the byte half is checked
123  /// against what the planes would export, before this crate allocates
124  /// anything.
125  pub(crate) fn open_impl(
126    parameters: Parameters,
127    limits: DecoderLimits,
128  ) -> Result<Self, ImageDecodeError> {
129    // Use the checked codec-context builder — `Context::from_parameters`
130    // is OOM-UB-prone (see `crate::decoder::build_codec_context`).
131    let (ctx, callback_state) =
132      build_codec_context(&parameters, limits).map_err(ImageDecodeError::Decode)?;
133    // **Opened without ever forming a bindgen enum from FFmpeg memory.**
134    // `Context::decoder().video()` looks cheap and is not: it resolves
135    // the codec by reading `AVCodecParameters.codec_id` as the bindgen
136    // `AVCodecID`, and then `Opened::video()` reads
137    // `AVCodecContext.codec_type` as `AVMediaType`. Both are open C
138    // enums — FFmpeg adds members in ABI-compatible releases — and
139    // forming a Rust enum from a value outside this build's discriminant
140    // set is UB before any comparison can run. The hardware path has
141    // bypassed this API since it was written; the image road was still
142    // going through it, which is the one road a *file* chooses the
143    // codec id on.
144    //
145    // `find_decoder` does the lookup off a raw `u32`, and
146    // `ensure_video_codec_type` does the check off a raw `i32`; the
147    // `decoder::Video` wrapper is then constructed through its public
148    // tuple field, exactly as `Opened::video()` would have on success.
149    let codec = find_decoder(&parameters).map_err(ImageDecodeError::Decode)?;
150    let opened = ctx
151      .decoder()
152      .open_as(codec)
153      .map_err(|e| ImageDecodeError::Decode(Error::Ffmpeg(e)))?;
154    ensure_video_codec_type(&opened).map_err(ImageDecodeError::Decode)?;
155    let decoder = ffmpeg_next::decoder::Video(opened);
156    let scratch = alloc_av_video_frame().map_err(ImageDecodeError::Decode)?;
157    Ok(Self {
158      decoder,
159      scratch,
160      limits,
161      _callback_state: callback_state,
162      _carrier: core::marker::PhantomData,
163    })
164  }
165
166  /// The frame ceilings this decoder was opened with.
167  #[cfg_attr(not(tarpaulin), inline(always))]
168  pub(crate) const fn limits_impl(&self) -> DecoderLimits {
169    self.limits
170  }
171
172  /// Borrow the wrapped `ffmpeg::decoder::Video` (e.g. to query
173  /// `width()` / `height()` / `format()` before decoding).
174  #[cfg_attr(not(tarpaulin), inline(always))]
175  pub(crate) const fn inner_impl(&self) -> &ffmpeg_next::decoder::Video {
176    &self.decoder
177  }
178}
179
180impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierImageDecoder<C> {
181  pub(crate) fn decode_impl(
182    &mut self,
183    packet: &AttachmentPacket<AttachmentPacketExtra, C::Buffer>,
184  ) -> Result<ImageFrame<PixelFormat, ImageFrameExtra, C::Buffer>, ImageDecodeError> {
185    let bytes: &[u8] = packet.data().as_ref();
186    // An attachment with no bytes is no picture. The demuxer really
187    // does produce these — a cover-art stream that parks no payload,
188    // or an `AVMEDIA_TYPE_ATTACHMENT` track with empty extradata, both
189    // arrive as a `synthesized` packet with an empty body — and
190    // handing that to `avcodec_send_packet` would not be a decode but
191    // a *drain* signal, which is a different thing wearing the same
192    // shape.
193    if bytes.is_empty() {
194      return Err(ImageDecodeError::EmptyPayload);
195    }
196    // **The compressed-input ceiling, before the copy.** `decode` hands
197    // these bytes to `try_packet_copy`, which duplicates them into an
198    // `AVPacket` — a second full copy of whatever the caller holds,
199    // capped by nothing but `c_int::MAX`.
200    //
201    // When the payload arrives through the demuxer it was already
202    // charged against the attachment budget; this seat is what keeps
203    // the same ceiling in force on the road that skips the demux tier,
204    // where a caller builds the packet itself. Without it the direct
205    // road was a gigabyte more permissive than the demuxed one for the
206    // same bytes.
207    if bytes.len() > self.limits.max_image_input_bytes() {
208      return Err(ImageDecodeError::InputTooLarge(InputTooLarge::new(
209        bytes.len(),
210        self.limits.max_image_input_bytes(),
211      )));
212    }
213    // **The corruption refusal, before the ceiling and before the
214    // copy.** See [`Corrupt`] for the census behind it: forwarding this
215    // flag to libavcodec is measurably a no-op on the image road, and
216    // an `ImageFrame` cannot carry the fact, so refusing is the only
217    // handling that does not silently delete the caller's own warning.
218    // It costs one bit test, which is why it runs ahead of the budget.
219    // The fourth rebuild road, and it obeys the same refusal as the
220    // three stream families: see `crate::buffer::TrustedPayload`. This
221    // seam writes the flags onto a fresh `AVPacket` and hands it to a
222    // decoder, so a `TRUSTED` bit arriving here is a decoder being told
223    // it may dereference a body this crate copied by value.
224    if packet.flags().bits() & crate::buffer::TRUSTED_BIT != 0 {
225      return Err(ImageDecodeError::TrustedPayload(
226        crate::buffer::TrustedPayload::new(bytes.len()),
227      ));
228    }
229    if packet.flags().contains(MdPacketFlags::CORRUPT) {
230      return Err(ImageDecodeError::Corrupt(Corrupt::new(
231        CorruptSource::Packet,
232      )));
233    }
234    // The decoder may still hold state from a previous picture (or
235    // from a `decode` that failed part way). Reset before feeding, so
236    // one call's failure cannot become the next call's frame.
237    self.decoder.flush();
238
239    let mut av_pkt =
240      boundary::try_packet_copy(bytes).map_err(|e| ImageDecodeError::Decode(Error::Ffmpeg(e)))?;
241    // **The flags, through the crate's one flag writer.** This road
242    // rebuilt the `AVPacket` from bytes alone and left `flags` zeroed,
243    // so everything the portable packet said about itself stopped here:
244    // `DISCARD` — which libavcodec really does obey, measurably, by
245    // producing no frame — was silently ignored, and the attachment was
246    // decoded anyway. The three stream families have always gone
247    // through `write_md_flags`; the image road now does too rather than
248    // growing a second copy of the same three lines.
249    //
250    // SAFETY: `av_pkt` owns a live `AVPacket` returned by
251    // `try_packet_copy`.
252    unsafe { boundary::write_md_flags(&mut av_pkt, packet.flags()) };
253    // Through the software funnel: a frame the allocator judge refused
254    // surfaces named, not as the `EINVAL` a corrupt file also produces.
255    let state: *const crate::ffi::CallbackState = &*self._callback_state;
256    self
257      .decoder
258      .send_packet(&av_pkt)
259      .map_err(|e| ImageDecodeError::Decode(crate::decoder::software_exit(state, e)))?;
260    // Every still codec answers its one packet with its one frame, but
261    // it is entitled to want the end of the stream first — so the EOF
262    // goes in before the drain rather than after a speculative
263    // `receive_frame` that would have to interpret `EAGAIN`.
264    self
265      .decoder
266      .send_eof()
267      .map_err(|e| ImageDecodeError::Decode(crate::decoder::software_exit(state, e)))?;
268    if let Err(e) = self.decoder.receive_frame(&mut self.scratch) {
269      self.decoder.flush();
270      // Funnelled, then classified — the same two steps every receive
271      // road in this crate takes, so a recorded budget refusal is named
272      // rather than mistaken for a payload that held no picture.
273      //
274      // **And then the classification collapses, which is the one thing
275      // this road does differently.** A one-shot decode has no session
276      // states to report: there is no more input the caller could send
277      // and no stream to be at the end of. `EOF` from a decoder that
278      // never produced a picture, and `EAGAIN` from one that has already
279      // been told the stream ended, are therefore the same fact about
280      // the *payload* — it is not an image this codec reads — and that
281      // fact is an error, not a rhythm. (`EAGAIN` after `send_eof` is
282      // not a state `avcodec` documents; it is caught here so a codec
283      // that does it cannot surface as a raw errno the caller has to
284      // decode.)
285      return Err(
286        // `Draining` is the literal truth of this road: `send_eof` ran
287        // three lines up, and a one-shot decoder has no probe. Both
288        // flow signals therefore read as the end — which is exactly the
289        // collapse this road has always made by hand.
290        match crate::decoder::software_receive(state, e, crate::decoder::SessionPhase::Draining) {
291          Ok(_) => ImageDecodeError::NoImage,
292          Err(fault) => ImageDecodeError::Decode(fault),
293        },
294      );
295    }
296    // **The other road the same fact arrives on.** A decoder is entitled
297    // to hand back a picture and mark it corrupt — that is libavcodec
298    // saying it concealed errors rather than read the image — and
299    // `ImageFrame` has no more room for that fact than it had for the
300    // packet's. Closing one road and leaving its sibling open is the
301    // shape this release has already been caught by twice.
302    //
303    // SAFETY: `scratch` owns a live `AVFrame` just filled by
304    // `receive_frame`; `flags` is a plain `c_int` field.
305    let frame_flags = unsafe { (*self.scratch.as_ptr()).flags };
306    // **Two fields, one fact.** `AV_FRAME_FLAG_CORRUPT` is the loud one;
307    // `decode_error_flags` is the quiet one, and it is the one h264
308    // actually writes — `FF_DECODE_ERROR_INVALID_BITSTREAM` /
309    // `..._MISSING_REFERENCE` / `..._CONCEALMENT_ACTIVE` /
310    // `..._DECODE_SLICES` are set on a frame the decoder *concealed its
311    // way through*, with the frame flag left clear. A gate that reads
312    // only the flag passes exactly the frames a real decoder marks.
313    //
314    // Any nonzero value counts. The field is a bit set FFmpeg extends,
315    // so enumerating the members this build names would re-open the
316    // same door on the next release; "the decoder recorded an error"
317    // is the fact, and its spelling is FFmpeg's business.
318    //
319    // SAFETY: same live `AVFrame`; `decode_error_flags` is a public
320    // `c_int` field.
321    let decode_error_flags = unsafe { (*self.scratch.as_ptr()).decode_error_flags };
322    if frame_is_corrupt(frame_flags, decode_error_flags) {
323      self.decoder.flush();
324      return Err(ImageDecodeError::Corrupt(Corrupt::new(
325        CorruptSource::DecodedFrame,
326      )));
327    }
328    // **This road needs no parked-frame seat, and the reason is the
329    // signature.** The timed decoders advance libavcodec and hand the
330    // caller nothing to retry with, which is why they park a frame
331    // whose conversion could not commit. Here the caller still holds
332    // the attachment packet — it was *borrowed* — so a failure leaves
333    // them everything needed to call again, and the decoder is flushed
334    // on the way out so the next attempt starts from the same place
335    // this one did.
336    //
337    // SAFETY: `scratch` was just filled by `receive_frame`; the
338    // conversion copies every byte it takes, so the scratch frame can
339    // be reused on the next call and the produced `ImageFrame` outlives
340    // this decoder.
341    let image = unsafe {
342      convert::av_frame_to_image_frame_as::<C>(self.scratch.as_ptr(), self.limits.frame())
343    }
344    .map_err(|e| {
345      self.decoder.flush();
346      ImageDecodeError::Convert(e)
347    })?;
348    // Leave the decoder ready for the next attachment rather than
349    // drained-and-latched at EOF.
350    self.decoder.flush();
351    Ok(image)
352  }
353}
354
355macro_rules! image_lane_face {
356  ($($lane:ty),+ $(,)?) => { $(
357    impl CarrierImageDecoder<$lane> {
358      /// Opens a still-image decoder for `parameters`.
359      pub fn open(
360        parameters: Parameters,
361        limits: DecoderLimits,
362      ) -> Result<Self, ImageDecodeError> {
363        Self::open_impl(parameters, limits)
364      }
365
366      /// The budgets this decoder was opened with.
367      pub const fn limits(&self) -> DecoderLimits {
368        self.limits_impl()
369      }
370
371      /// The wrapped decoder context.
372      pub const fn inner(&self) -> &ffmpeg_next::decoder::Video {
373        self.inner_impl()
374      }
375    }
376
377    impl ImageDecoder for CarrierImageDecoder<$lane> {
378      type Adapter = Ffmpeg;
379      type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
380      type Error = ImageDecodeError;
381
382      fn decode(
383        &mut self,
384        packet: &AttachmentPacket<AttachmentPacketExtra, Self::Buffer>,
385      ) -> Result<ImageFrame<PixelFormat, ImageFrameExtra, Self::Buffer>, Self::Error> {
386        self.decode_impl(packet)
387      }
388    }
389  )+ };
390}
391
392image_lane_face!(crate::View, crate::Owned);
393
394/// Payload for [`ImageDecodeError::InputTooLarge`].
395///
396/// The compressed bytes handed to one decode exceed
397/// [`DecoderLimits::max_image_input_bytes`].
398#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
399#[error("a {bytes}-byte image payload exceeds the {limit}-byte input ceiling")]
400pub struct InputTooLarge {
401  bytes: usize,
402  limit: usize,
403}
404
405impl InputTooLarge {
406  /// Constructs an `InputTooLarge` payload.
407  #[cfg_attr(not(tarpaulin), inline(always))]
408  pub const fn new(bytes: usize, limit: usize) -> Self {
409    Self { bytes, limit }
410  }
411  /// The payload length the caller handed over.
412  #[cfg_attr(not(tarpaulin), inline(always))]
413  pub const fn bytes(&self) -> usize {
414    self.bytes
415  }
416  /// The ceiling in force.
417  #[cfg_attr(not(tarpaulin), inline(always))]
418  pub const fn limit(&self) -> usize {
419    self.limit
420  }
421}
422
423/// Errors from [`FfmpegImageDecoder`].
424///
425/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
426/// fail are discovered — a backend, a ceiling, a corruption a codec
427/// learns to report — and a consumer that meets one it has never heard
428/// of should take its generic-fault path. That is exactly what the
429/// wildcard arm this attribute forces is for. The two status
430/// vocabularies opposite it,
431/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
432/// are exhaustive for the mirror-image reason: their arms are the
433/// substrate's fixed state set, and there the wildcard would be dead
434/// weight hiding a state a consumer forgot.
435#[derive(thiserror::Error, Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
436#[unwrap(ref, ref_mut)]
437#[try_unwrap(ref, ref_mut)]
438#[non_exhaustive]
439pub enum ImageDecodeError {
440  /// The compressed payload is larger than the ceiling allows. Refused
441  /// **before** it is copied into an `AVPacket`.
442  #[error(transparent)]
443  InputTooLarge(#[from] InputTooLarge),
444
445  /// The wrapped `ffmpeg::decoder::Video` reported an error, or the
446  /// codec context could not be built.
447  #[error(transparent)]
448  Decode(#[from] Error),
449  /// Conversion from FFmpeg's `AVFrame` to mediadecode's `ImageFrame`
450  /// failed — an undeliverable pixel format, or a plane layout the
451  /// safe accessors refuse.
452  #[error(transparent)]
453  Convert(#[from] ConvertError),
454  /// The attachment carried no bytes, so there is nothing to decode.
455  ///
456  /// Distinct from [`Self::NoImage`]: this is an empty payload, which
457  /// the demuxer synthesizes for an attachment track whose picture the
458  /// container never stored.
459  #[error("the attachment carries no bytes; there is no image to decode")]
460  EmptyPayload,
461  /// The payload was accepted and produced no picture — it is not an
462  /// image this codec reads.
463  #[error("the attachment's bytes decoded to no image")]
464  NoImage,
465  /// Something on this road is marked corrupt, and an [`ImageFrame`]
466  /// has nowhere to say so.
467  #[error(transparent)]
468  Corrupt(#[from] Corrupt),
469  /// The attachment is marked `AV_PKT_FLAG_TRUSTED`, so its body may
470  /// hold pointers rather than bytes. See
471  /// [`crate::buffer::TrustedPayload`].
472  #[error(transparent)]
473  TrustedPayload(#[from] crate::buffer::TrustedPayload),
474}
475
476/// Whether libavcodec is telling us the picture it returned is damaged.
477///
478/// A named predicate rather than an inline condition, because it is the
479/// part of the corruption gate that can be exercised without a decoder
480/// that produces the input — see the note on `decode_error_flags`
481/// below.
482///
483/// Two fields, one fact:
484///
485/// * `AV_FRAME_FLAG_CORRUPT` is the loud one, and the only one the
486///   original gate read.
487/// * `decode_error_flags` is the quiet one, and it is the one that is
488///   actually written in practice: h264 records
489///   `FF_DECODE_ERROR_INVALID_BITSTREAM`, `..._MISSING_REFERENCE`,
490///   `..._CONCEALMENT_ACTIVE` and `..._DECODE_SLICES` there for a frame
491///   it *concealed its way through*, leaving the frame flag clear. A
492///   gate reading only the flag passes exactly the frames a real
493///   decoder marks.
494///
495/// Any nonzero value counts. The field is a bit set FFmpeg extends, so
496/// enumerating the members this build names would re-open the same door
497/// on the next release: "the decoder recorded an error" is the fact,
498/// and its spelling is FFmpeg's business.
499///
500/// **Coverage, stated honestly.** No codec reachable from this crate's
501/// test corpus produces a frame with `decode_error_flags` set — mjpeg,
502/// which is what cover art overwhelmingly is, either refuses a damaged
503/// payload outright at `send_packet` or decodes it cleanly, and the
504/// corpus has no h264 attachment to exercise concealment with. This
505/// predicate is unit-tested against both fields directly; the road from
506/// a real damaged h264 still to a nonzero `decode_error_flags` is
507/// FFmpeg's, not this crate's, and is not pinned here.
508#[inline]
509const fn frame_is_corrupt(frame_flags: i32, decode_error_flags: i32) -> bool {
510  frame_flags & ffmpeg_next::ffi::AV_FRAME_FLAG_CORRUPT != 0 || decode_error_flags != 0
511}
512
513/// Which side of the decode declared the corruption.
514///
515/// Two roads, one fact. A closed vocabulary owned by this crate, so it
516/// is an enum rather than a string.
517#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant)]
518pub enum CorruptSource {
519  /// The caller's packet arrived with `CORRUPT` set — the demuxer that
520  /// produced it found the payload damaged.
521  Packet,
522  /// libavcodec returned a picture and marked the frame itself corrupt
523  /// (`AV_FRAME_FLAG_CORRUPT`), which is a decoder saying it concealed
524  /// errors rather than read the image.
525  DecodedFrame,
526}
527
528impl core::fmt::Display for CorruptSource {
529  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
530    match self {
531      Self::Packet => f.write_str("the attachment packet"),
532      Self::DecodedFrame => f.write_str("the decoded frame"),
533    }
534  }
535}
536
537/// Payload for [`ImageDecodeError::Corrupt`].
538///
539/// # Why this is a refusal and not a forwarded flag
540///
541/// Measured, on this build, with a real cover-art payload:
542///
543/// * `AV_PKT_FLAG_CORRUPT` reaches libavcodec and libavcodec **does
544///   nothing with it** — the mjpeg decoder returns a full picture and
545///   leaves `AV_FRAME_FLAG_CORRUPT` clear. So forwarding the flag does
546///   not preserve the fact; it only moves where it is dropped.
547/// * `AV_PKT_FLAG_DISCARD`, by contrast, **is** honoured: the decoder
548///   produces no frame at all. That flag is therefore forwarded and
549///   nothing is decided here.
550///
551/// `ImageFrame` has no flag seat — deliberately, it is a picture and
552/// not a packet — so a corruption signal that survives the decode has
553/// nowhere left to go. Returning the picture anyway would hand a caller
554/// a possibly-garbage image with the one warning about it deleted en
555/// route. Named refusal, both roads. A caller who wants the bytes
556/// decoded regardless can clear the flag it set.
557// The field is `declared_by`, not `source`: `thiserror` reads a field
558// of that name as the error's `Error::source()` chain, and an inherent
559// accessor spelled `source` would shadow the trait method besides.
560#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
561#[error("{declared_by} is marked corrupt; this decoder will not return a picture from it")]
562pub struct Corrupt {
563  declared_by: CorruptSource,
564}
565
566impl Corrupt {
567  /// Constructs a `Corrupt` payload.
568  #[inline]
569  pub const fn new(declared_by: CorruptSource) -> Self {
570    Self { declared_by }
571  }
572  /// Which side declared the corruption.
573  #[inline]
574  pub const fn declared_by(&self) -> CorruptSource {
575    self.declared_by
576  }
577}
578
579#[cfg(test)]
580mod tests {
581  use super::*;
582
583  #[test]
584  fn the_corruption_gate_reads_both_fields() {
585    const CORRUPT: i32 = ffmpeg_next::ffi::AV_FRAME_FLAG_CORRUPT;
586    const KEY: i32 = 1 << 1;
587
588    // Clean is clean.
589    assert!(!frame_is_corrupt(0, 0));
590    assert!(!frame_is_corrupt(KEY, 0));
591
592    // The loud field.
593    assert!(frame_is_corrupt(CORRUPT, 0));
594    assert!(frame_is_corrupt(KEY | CORRUPT, 0));
595
596    // The quiet one, which is the one h264 actually writes — this is
597    // the case the gate used to pass.
598    assert!(frame_is_corrupt(
599      KEY,
600      ffmpeg_next::ffi::FF_DECODE_ERROR_CONCEALMENT_ACTIVE
601    ));
602    assert!(frame_is_corrupt(
603      KEY,
604      ffmpeg_next::ffi::FF_DECODE_ERROR_INVALID_BITSTREAM
605    ));
606    assert!(frame_is_corrupt(
607      KEY,
608      ffmpeg_next::ffi::FF_DECODE_ERROR_MISSING_REFERENCE
609    ));
610    assert!(frame_is_corrupt(
611      KEY,
612      ffmpeg_next::ffi::FF_DECODE_ERROR_DECODE_SLICES
613    ));
614
615    // And any bit a future FFmpeg adds, without this crate learning its
616    // name — which is the whole reason the test is `!= 0`.
617    assert!(frame_is_corrupt(0, 1 << 30));
618  }
619}