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 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}