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    match self.decoder.receive_frame(&mut self.scratch) {
269      Ok(()) => {}
270      // The bytes were accepted and yielded nothing. Named rather than
271      // passed through as an FFmpeg errno: `EOF` from a decoder that
272      // never produced a picture, and `EAGAIN` from one that has
273      // already been told the stream ended, are the same fact about the
274      // payload — it is not an image this codec reads. (`EAGAIN` after
275      // `send_eof` is not a state `avcodec` documents; it is caught
276      // here so that a codec which does it cannot surface as a raw
277      // errno the caller has to decode.) Spelled through
278      // `ffmpeg_next::error::EAGAIN`, as the rest of this crate does.
279      Err(e)
280        if matches!(e, ffmpeg_next::Error::Eof)
281          || matches!(e, ffmpeg_next::Error::Other { errno }
282            if errno == ffmpeg_next::error::EAGAIN) =>
283      {
284        self.decoder.flush();
285        return Err(ImageDecodeError::NoImage);
286      }
287      Err(e) => {
288        self.decoder.flush();
289        return Err(ImageDecodeError::Decode(crate::decoder::software_exit(
290          state, e,
291        )));
292      }
293    }
294    // **The other road the same fact arrives on.** A decoder is entitled
295    // to hand back a picture and mark it corrupt — that is libavcodec
296    // saying it concealed errors rather than read the image — and
297    // `ImageFrame` has no more room for that fact than it had for the
298    // packet's. Closing one road and leaving its sibling open is the
299    // shape this release has already been caught by twice.
300    //
301    // SAFETY: `scratch` owns a live `AVFrame` just filled by
302    // `receive_frame`; `flags` is a plain `c_int` field.
303    let frame_flags = unsafe { (*self.scratch.as_ptr()).flags };
304    // **Two fields, one fact.** `AV_FRAME_FLAG_CORRUPT` is the loud one;
305    // `decode_error_flags` is the quiet one, and it is the one h264
306    // actually writes — `FF_DECODE_ERROR_INVALID_BITSTREAM` /
307    // `..._MISSING_REFERENCE` / `..._CONCEALMENT_ACTIVE` /
308    // `..._DECODE_SLICES` are set on a frame the decoder *concealed its
309    // way through*, with the frame flag left clear. A gate that reads
310    // only the flag passes exactly the frames a real decoder marks.
311    //
312    // Any nonzero value counts. The field is a bit set FFmpeg extends,
313    // so enumerating the members this build names would re-open the
314    // same door on the next release; "the decoder recorded an error"
315    // is the fact, and its spelling is FFmpeg's business.
316    //
317    // SAFETY: same live `AVFrame`; `decode_error_flags` is a public
318    // `c_int` field.
319    let decode_error_flags = unsafe { (*self.scratch.as_ptr()).decode_error_flags };
320    if frame_is_corrupt(frame_flags, decode_error_flags) {
321      self.decoder.flush();
322      return Err(ImageDecodeError::Corrupt(Corrupt::new(
323        CorruptSource::DecodedFrame,
324      )));
325    }
326    // **This road needs no parked-frame seat, and the reason is the
327    // signature.** The timed decoders advance libavcodec and hand the
328    // caller nothing to retry with, which is why they park a frame
329    // whose conversion could not commit. Here the caller still holds
330    // the attachment packet — it was *borrowed* — so a failure leaves
331    // them everything needed to call again, and the decoder is flushed
332    // on the way out so the next attempt starts from the same place
333    // this one did.
334    //
335    // SAFETY: `scratch` was just filled by `receive_frame`; the
336    // conversion copies every byte it takes, so the scratch frame can
337    // be reused on the next call and the produced `ImageFrame` outlives
338    // this decoder.
339    let image = unsafe {
340      convert::av_frame_to_image_frame_as::<C>(self.scratch.as_ptr(), self.limits.frame())
341    }
342    .map_err(|e| {
343      self.decoder.flush();
344      ImageDecodeError::Convert(e)
345    })?;
346    // Leave the decoder ready for the next attachment rather than
347    // drained-and-latched at EOF.
348    self.decoder.flush();
349    Ok(image)
350  }
351}
352
353macro_rules! image_lane_face {
354  ($($lane:ty),+ $(,)?) => { $(
355    impl CarrierImageDecoder<$lane> {
356      /// Opens a still-image decoder for `parameters`.
357      pub fn open(
358        parameters: Parameters,
359        limits: DecoderLimits,
360      ) -> Result<Self, ImageDecodeError> {
361        Self::open_impl(parameters, limits)
362      }
363
364      /// The budgets this decoder was opened with.
365      pub const fn limits(&self) -> DecoderLimits {
366        self.limits_impl()
367      }
368
369      /// The wrapped decoder context.
370      pub const fn inner(&self) -> &ffmpeg_next::decoder::Video {
371        self.inner_impl()
372      }
373    }
374
375    impl ImageDecoder for CarrierImageDecoder<$lane> {
376      type Adapter = Ffmpeg;
377      type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
378      type Error = ImageDecodeError;
379
380      fn decode(
381        &mut self,
382        packet: &AttachmentPacket<AttachmentPacketExtra, Self::Buffer>,
383      ) -> Result<ImageFrame<PixelFormat, ImageFrameExtra, Self::Buffer>, Self::Error> {
384        self.decode_impl(packet)
385      }
386    }
387  )+ };
388}
389
390image_lane_face!(crate::View, crate::Owned);
391
392/// Payload for [`ImageDecodeError::InputTooLarge`].
393///
394/// The compressed bytes handed to one decode exceed
395/// [`DecoderLimits::max_image_input_bytes`].
396#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
397#[error("a {bytes}-byte image payload exceeds the {limit}-byte input ceiling")]
398pub struct InputTooLarge {
399  bytes: usize,
400  limit: usize,
401}
402
403impl InputTooLarge {
404  /// Constructs an `InputTooLarge` payload.
405  #[cfg_attr(not(tarpaulin), inline(always))]
406  pub const fn new(bytes: usize, limit: usize) -> Self {
407    Self { bytes, limit }
408  }
409  /// The payload length the caller handed over.
410  #[cfg_attr(not(tarpaulin), inline(always))]
411  pub const fn bytes(&self) -> usize {
412    self.bytes
413  }
414  /// The ceiling in force.
415  #[cfg_attr(not(tarpaulin), inline(always))]
416  pub const fn limit(&self) -> usize {
417    self.limit
418  }
419}
420
421/// Errors from [`FfmpegImageDecoder`].
422#[derive(thiserror::Error, Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
423#[unwrap(ref, ref_mut)]
424#[try_unwrap(ref, ref_mut)]
425pub enum ImageDecodeError {
426  /// The compressed payload is larger than the ceiling allows. Refused
427  /// **before** it is copied into an `AVPacket`.
428  #[error(transparent)]
429  InputTooLarge(#[from] InputTooLarge),
430
431  /// The wrapped `ffmpeg::decoder::Video` reported an error, or the
432  /// codec context could not be built.
433  #[error(transparent)]
434  Decode(#[from] Error),
435  /// Conversion from FFmpeg's `AVFrame` to mediadecode's `ImageFrame`
436  /// failed — an undeliverable pixel format, or a plane layout the
437  /// safe accessors refuse.
438  #[error(transparent)]
439  Convert(#[from] ConvertError),
440  /// The attachment carried no bytes, so there is nothing to decode.
441  ///
442  /// Distinct from [`Self::NoImage`]: this is an empty payload, which
443  /// the demuxer synthesizes for an attachment track whose picture the
444  /// container never stored.
445  #[error("the attachment carries no bytes; there is no image to decode")]
446  EmptyPayload,
447  /// The payload was accepted and produced no picture — it is not an
448  /// image this codec reads.
449  #[error("the attachment's bytes decoded to no image")]
450  NoImage,
451  /// Something on this road is marked corrupt, and an [`ImageFrame`]
452  /// has nowhere to say so.
453  #[error(transparent)]
454  Corrupt(#[from] Corrupt),
455  /// The attachment is marked `AV_PKT_FLAG_TRUSTED`, so its body may
456  /// hold pointers rather than bytes. See
457  /// [`crate::buffer::TrustedPayload`].
458  #[error(transparent)]
459  TrustedPayload(#[from] crate::buffer::TrustedPayload),
460}
461
462/// Whether libavcodec is telling us the picture it returned is damaged.
463///
464/// A named predicate rather than an inline condition, because it is the
465/// part of the corruption gate that can be exercised without a decoder
466/// that produces the input — see the note on `decode_error_flags`
467/// below.
468///
469/// Two fields, one fact:
470///
471/// * `AV_FRAME_FLAG_CORRUPT` is the loud one, and the only one the
472///   original gate read.
473/// * `decode_error_flags` is the quiet one, and it is the one that is
474///   actually written in practice: h264 records
475///   `FF_DECODE_ERROR_INVALID_BITSTREAM`, `..._MISSING_REFERENCE`,
476///   `..._CONCEALMENT_ACTIVE` and `..._DECODE_SLICES` there for a frame
477///   it *concealed its way through*, leaving the frame flag clear. A
478///   gate reading only the flag passes exactly the frames a real
479///   decoder marks.
480///
481/// Any nonzero value counts. The field is a bit set FFmpeg extends, so
482/// enumerating the members this build names would re-open the same door
483/// on the next release: "the decoder recorded an error" is the fact,
484/// and its spelling is FFmpeg's business.
485///
486/// **Coverage, stated honestly.** No codec reachable from this crate's
487/// test corpus produces a frame with `decode_error_flags` set — mjpeg,
488/// which is what cover art overwhelmingly is, either refuses a damaged
489/// payload outright at `send_packet` or decodes it cleanly, and the
490/// corpus has no h264 attachment to exercise concealment with. This
491/// predicate is unit-tested against both fields directly; the road from
492/// a real damaged h264 still to a nonzero `decode_error_flags` is
493/// FFmpeg's, not this crate's, and is not pinned here.
494#[inline]
495const fn frame_is_corrupt(frame_flags: i32, decode_error_flags: i32) -> bool {
496  frame_flags & ffmpeg_next::ffi::AV_FRAME_FLAG_CORRUPT != 0 || decode_error_flags != 0
497}
498
499/// Which side of the decode declared the corruption.
500///
501/// Two roads, one fact. A closed vocabulary owned by this crate, so it
502/// is an enum rather than a string.
503#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant)]
504pub enum CorruptSource {
505  /// The caller's packet arrived with `CORRUPT` set — the demuxer that
506  /// produced it found the payload damaged.
507  Packet,
508  /// libavcodec returned a picture and marked the frame itself corrupt
509  /// (`AV_FRAME_FLAG_CORRUPT`), which is a decoder saying it concealed
510  /// errors rather than read the image.
511  DecodedFrame,
512}
513
514impl core::fmt::Display for CorruptSource {
515  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
516    match self {
517      Self::Packet => f.write_str("the attachment packet"),
518      Self::DecodedFrame => f.write_str("the decoded frame"),
519    }
520  }
521}
522
523/// Payload for [`ImageDecodeError::Corrupt`].
524///
525/// # Why this is a refusal and not a forwarded flag
526///
527/// Measured, on this build, with a real cover-art payload:
528///
529/// * `AV_PKT_FLAG_CORRUPT` reaches libavcodec and libavcodec **does
530///   nothing with it** — the mjpeg decoder returns a full picture and
531///   leaves `AV_FRAME_FLAG_CORRUPT` clear. So forwarding the flag does
532///   not preserve the fact; it only moves where it is dropped.
533/// * `AV_PKT_FLAG_DISCARD`, by contrast, **is** honoured: the decoder
534///   produces no frame at all. That flag is therefore forwarded and
535///   nothing is decided here.
536///
537/// `ImageFrame` has no flag seat — deliberately, it is a picture and
538/// not a packet — so a corruption signal that survives the decode has
539/// nowhere left to go. Returning the picture anyway would hand a caller
540/// a possibly-garbage image with the one warning about it deleted en
541/// route. Named refusal, both roads. A caller who wants the bytes
542/// decoded regardless can clear the flag it set.
543// The field is `declared_by`, not `source`: `thiserror` reads a field
544// of that name as the error's `Error::source()` chain, and an inherent
545// accessor spelled `source` would shadow the trait method besides.
546#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
547#[error("{declared_by} is marked corrupt; this decoder will not return a picture from it")]
548pub struct Corrupt {
549  declared_by: CorruptSource,
550}
551
552impl Corrupt {
553  /// Constructs a `Corrupt` payload.
554  #[inline]
555  pub const fn new(declared_by: CorruptSource) -> Self {
556    Self { declared_by }
557  }
558  /// Which side declared the corruption.
559  #[inline]
560  pub const fn declared_by(&self) -> CorruptSource {
561    self.declared_by
562  }
563}
564
565#[cfg(test)]
566mod tests {
567  use super::*;
568
569  #[test]
570  fn the_corruption_gate_reads_both_fields() {
571    const CORRUPT: i32 = ffmpeg_next::ffi::AV_FRAME_FLAG_CORRUPT;
572    const KEY: i32 = 1 << 1;
573
574    // Clean is clean.
575    assert!(!frame_is_corrupt(0, 0));
576    assert!(!frame_is_corrupt(KEY, 0));
577
578    // The loud field.
579    assert!(frame_is_corrupt(CORRUPT, 0));
580    assert!(frame_is_corrupt(KEY | CORRUPT, 0));
581
582    // The quiet one, which is the one h264 actually writes — this is
583    // the case the gate used to pass.
584    assert!(frame_is_corrupt(
585      KEY,
586      ffmpeg_next::ffi::FF_DECODE_ERROR_CONCEALMENT_ACTIVE
587    ));
588    assert!(frame_is_corrupt(
589      KEY,
590      ffmpeg_next::ffi::FF_DECODE_ERROR_INVALID_BITSTREAM
591    ));
592    assert!(frame_is_corrupt(
593      KEY,
594      ffmpeg_next::ffi::FF_DECODE_ERROR_MISSING_REFERENCE
595    ));
596    assert!(frame_is_corrupt(
597      KEY,
598      ffmpeg_next::ffi::FF_DECODE_ERROR_DECODE_SLICES
599    ));
600
601    // And any bit a future FFmpeg adds, without this crate learning its
602    // name — which is the whole reason the test is `!= 0`.
603    assert!(frame_is_corrupt(0, 1 << 30));
604  }
605}