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(¶meters, 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(¶meters).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}