Skip to main content

mediadecode_ffmpeg/
subtitle.rs

1//! `mediadecode::SubtitleDecoder` impl backed by
2//! `ffmpeg::decoder::Subtitle`.
3//!
4//! Subtitles use FFmpeg's legacy synchronous `decode()` API rather
5//! than `send_packet`/`receive_frame`. We bridge the difference by
6//! converting the produced `AVSubtitle` into a
7//! [`mediadecode::SubtitleFrame`] inside [`SubtitleDecoder::send_packet`]
8//! and stashing it in `pending` for the next [`SubtitleDecoder::receive_frame`]
9//! call. This matches the trait's contract: `send_packet` enqueues
10//! work — answering [`Sent::MustDrain`] while the seat is still full,
11//! since the inline API cannot queue a second cue — and `receive_frame`
12//! drains one decoded frame at a time, answering
13//! [`Received::NeedsInput`] when the seat is empty and
14//! [`Received::Ended`] once [`SubtitleDecoder::send_eof`] has been
15//! signalled.
16//!
17//! **A decoder with no tail still has an end.** `avcodec_decode_subtitle2`
18//! produces its cue inline, so nothing is buffered and `send_eof` has
19//! nothing to flush — but a session the caller has declared over is a
20//! different state from one still waiting for packets, and only one of
21//! them lets a drain loop stop. The latch that tells them apart is the
22//! whole of this backend's end-of-stream machinery.
23
24use derive_more::{IsVariant, TryUnwrap, Unwrap};
25use ffmpeg_next::{codec::Parameters, ffi::avsubtitle_free};
26use mediadecode::{
27  Received, Sent, Timebase, decoder::SubtitleDecoder, frame::SubtitleFrame, packet::SubtitlePacket,
28};
29
30use crate::{
31  DecoderLimits, Error, Ffmpeg, boundary,
32  convert::{self, ConvertError},
33  decoder::build_codec_context,
34  extras::{SubtitleFrameExtra, SubtitlePacketExtra},
35};
36
37/// RAII wrapper that owns an `ffmpeg_next::Subtitle` scratch slot and
38/// frees the FFmpeg-side rect allocations on drop / explicit `clear`.
39///
40/// `ffmpeg::Subtitle::new()` zero-initializes; `decoder.decode()` may
41/// allocate per-rect storage (`AVSubtitleRect.text` / `.ass` /
42/// `.data[0]` / `.data[1]`) which only `avsubtitle_free` releases.
43/// Without this wrapper, every successful decode leaks until the
44/// decoder drops.
45struct ScratchSubtitle {
46  inner: ffmpeg_next::Subtitle,
47}
48
49impl ScratchSubtitle {
50  fn new() -> Self {
51    Self {
52      inner: ffmpeg_next::Subtitle::new(),
53    }
54  }
55
56  fn clear(&mut self) {
57    // SAFETY: `inner` holds a valid AVSubtitle (zero-initialized or
58    // populated by `decode`). `avsubtitle_free` frees the rect array
59    // and per-rect allocations, then leaves the struct in a state
60    // suitable for reuse by the next decode call.
61    unsafe { avsubtitle_free(self.inner.as_mut_ptr()) };
62  }
63}
64
65impl Drop for ScratchSubtitle {
66  fn drop(&mut self) {
67    self.clear();
68  }
69}
70
71/// `mediadecode::SubtitleDecoder` impl wrapping `ffmpeg::decoder::Subtitle`.
72///
73/// Subtitle decoders are stateless from FFmpeg's perspective — each
74/// `decode()` call consumes one packet and produces zero-or-one
75/// `AVSubtitle`. The pending-frame buffer here is a one-slot queue
76/// so the trait's `send_packet` / `receive_frame` split works.
77pub struct CarrierSubtitleStreamDecoder<C: crate::FfmpegCarrier> {
78  decoder: ffmpeg_next::decoder::Subtitle,
79  scratch: ScratchSubtitle,
80  /// `true` when [`Self::scratch`] holds a decoded `AVSubtitle` that
81  /// has not been converted and delivered.
82  ///
83  /// **The conversion happens on the receive side, and that is the
84  /// point.** `avcodec_decode_subtitle2` consumes the packet: once it
85  /// has answered, the cue exists only in this scratch, and nothing
86  /// re-offers it. Converting inside `send_packet` meant an allocation
87  /// that failed took the cue with it — the scratch was freed, the
88  /// error returned, and the caller's next packet decoded the *next*
89  /// cue. Deferring the conversion to `receive_frame` gives it a seat
90  /// to fail into: the scratch is cleared when a carrier exists for it,
91  /// not before.
92  ///
93  /// The same shape the audio and video decoders keep for a decoded
94  /// `AVFrame`, and the same discipline `flush` clears.
95  scratch_pending: bool,
96  /// `true` once [`Self::send_eof_impl`] has been called and no
97  /// [`Self::flush_impl`] has reset the session.
98  ///
99  /// The legacy `decode()` API buffers nothing, so this latch is not a
100  /// drain cursor — it is the only thing that distinguishes "no cue
101  /// yet, send another packet" from "there will be no more cues". Both
102  /// used to answer with the same error arm, which meant a caller
103  /// draining to the end of a subtitle track had no terminating
104  /// condition to look for at all.
105  eof: bool,
106  time_base: Timebase,
107  /// Retained, not discarded at open: the send path judges
108  /// [`DecoderLimits::max_packet_bytes`] against every packet it
109  /// rebuilds into an `AVPacket`.
110  limits: DecoderLimits,
111  /// Keeps the [`CallbackState`](crate::ffi::CallbackState) alive for as
112  /// long as the codec context that points at it.
113  ///
114  /// Declared **after** the decoder on purpose: struct fields drop in
115  /// declaration order, so the `AVCodecContext` is freed first and the
116  /// state it references outlives it.
117  _callback_state: Box<crate::ffi::CallbackState>,
118  /// The lane this decoder captures into. A marker: the carrier
119  /// appears in the frames it produces, not in its own state.
120  _carrier: core::marker::PhantomData<C>,
121}
122
123impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierSubtitleStreamDecoder<C> {
124  /// Opens a subtitle decoder for the given codec parameters.
125  ///
126  /// `limits` reaches the `AVCodecContext` this call opens. A subtitle
127  /// decoder produces no pixels, so the pixel half never fires here —
128  /// it is passed for one reason: every decoder this crate opens gets
129  /// the same ceiling written into it, and a seam that skipped one
130  /// would be a seam somebody has to remember.
131  pub(crate) fn open_impl(
132    parameters: Parameters,
133    time_base: Timebase,
134    limits: DecoderLimits,
135  ) -> Result<Self, SubtitleDecodeError> {
136    // Use the checked codec-context builder — `Context::from_parameters`
137    // is OOM-UB-prone (see `crate::decoder::build_codec_context`).
138    let (ctx, callback_state) = build_codec_context(&parameters, limits, Some(time_base))
139      .map_err(SubtitleDecodeError::Decode)?;
140    // Opened without forming a bindgen enum from FFmpeg memory: the codec
141    // is resolved off a raw `codec_id`, and the medium is proved off a raw
142    // `codec_type`. See `crate::decoder::ensure_codec_type`.
143    let codec = crate::decoder::find_decoder(&parameters).map_err(SubtitleDecodeError::Decode)?;
144    let opened = ctx
145      .decoder()
146      .open_as(codec)
147      .map_err(|e| SubtitleDecodeError::Decode(Error::Ffmpeg(e)))?;
148    crate::decoder::ensure_codec_type(
149      &opened,
150      ffmpeg_next::ffi::AVMediaType::AVMEDIA_TYPE_SUBTITLE,
151    )
152    .map_err(SubtitleDecodeError::Decode)?;
153    let decoder = ffmpeg_next::decoder::Subtitle(opened);
154    Ok(Self {
155      decoder,
156      scratch: ScratchSubtitle::new(),
157      scratch_pending: false,
158      eof: false,
159      time_base,
160      limits,
161      _callback_state: callback_state,
162      _carrier: core::marker::PhantomData,
163    })
164  }
165
166  /// Returns the time base associated with the source stream.
167  #[cfg_attr(not(tarpaulin), inline(always))]
168  pub(crate) const fn time_base_impl(&self) -> Timebase {
169    self.time_base
170  }
171
172  /// The ceilings this decoder was opened with.
173  #[cfg_attr(not(tarpaulin), inline(always))]
174  pub(crate) const fn limits_impl(&self) -> DecoderLimits {
175    self.limits
176  }
177
178  /// Borrow the wrapped `ffmpeg::decoder::Subtitle`.
179  #[cfg_attr(not(tarpaulin), inline(always))]
180  pub(crate) const fn inner_impl(&self) -> &ffmpeg_next::decoder::Subtitle {
181    &self.decoder
182  }
183}
184
185impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierSubtitleStreamDecoder<C> {
186  pub(crate) fn send_packet_impl(
187    &mut self,
188    packet: &SubtitlePacket<SubtitlePacketExtra, C::Buffer>,
189  ) -> Result<Sent, SubtitleDecodeError> {
190    // **Nothing is sent after the stream has been declared over, and
191    // this gate is FIRST for a reason.**
192    //
193    // Every other decoder in the family gets this refusal from its
194    // substrate: `avcodec_send_packet` answers `AVERROR_EOF` to a packet
195    // that follows a flush packet, and the WebCodecs decoder tracks its
196    // own resolved flush. `avcodec_decode_subtitle2` has no send/receive
197    // state machine at all — it is a synchronous call that decodes
198    // whatever it is handed — so this session's `eof` latch is the only
199    // thing that knows, and a latch that gates only the receive side is
200    // not a latch. Without this, a valid packet after `send_eof` decoded
201    // normally, set the seat, and made the *next* `receive_frame` answer
202    // `Frame`: a terminal `Ended` reversed with no `flush` anywhere.
203    //
204    // **Before the held-cue check, not after.** A cue in the seat would
205    // otherwise turn a usage fault into `Sent::MustDrain` — an
206    // instruction to drain and re-offer, which is precisely the one
207    // thing that must not happen: the drained retry would then be
208    // accepted and the reversal would happen one call later.
209    if self.eof {
210      return Err(SubtitleDecodeError::AfterEof);
211    }
212    // **Nothing is sent while a cue is held.** The legacy `decode()`
213    // API produces a frame inline, so a second send would silently drop
214    // the first. The discipline is unchanged; only its spelling moved.
215    // It was `FramePending`, a fault-shaped value that made a caller
216    // choose between giving up and guessing — and the guess that
217    // survived was to offer the packet twice. It is back pressure, and
218    // it says so: nothing was consumed, drain and offer again.
219    if self.scratch_pending {
220      return Ok(Sent::MustDrain);
221    }
222    // Free any allocations from a previous decode before reusing the
223    // scratch — avoids leaking when the previous packet produced no
224    // frame (got == false, which still mutates the struct).
225    self.scratch.clear();
226    // Scoped submission — see `boundary::with_ffmpeg_subtitle_packet`.
227    let state: *const crate::ffi::CallbackState = &*self._callback_state;
228    let decoder = &mut self.decoder;
229    let scratch = &mut self.scratch.inner;
230    let got = boundary::with_ffmpeg_subtitle_packet::<C, _>(
231      packet,
232      self.limits.packet_limits(),
233      // Nothing on this road records what it is sent, so the packet
234      // really does die inside the call and its body may be shared.
235      crate::carrier::BodyRoute::Submission,
236      |av_pkt| {
237        decoder.decode(av_pkt, scratch).map_err(|e| {
238          // SAFETY: the callback state outlives this decoder.
239          SubtitleDecodeError::Decode(crate::decoder::software_exit(unsafe { &*state }, e))
240        })
241      },
242    )
243    .map_err(|e| SubtitleDecodeError::Decode(Error::PacketBuild(e)))??;
244    // The cue stays in the scratch until a carrier exists for it — see
245    // [`Self::scratch_pending`]. Nothing is converted here.
246    self.scratch_pending = got;
247    Ok(Sent::Accepted)
248  }
249
250  pub(crate) fn receive_frame_impl(
251    &mut self,
252    dst: &mut SubtitleFrame<SubtitleFrameExtra, C::Buffer>,
253  ) -> Result<Received, SubtitleDecodeError> {
254    if !self.scratch_pending {
255      // A held cue is delivered even after EOF — the latch ends the
256      // session, it does not discard what the session already made.
257      return Ok(if self.eof {
258        Received::Ended
259      } else {
260        Received::NeedsInput
261      });
262    }
263    // SAFETY: `scratch.inner` is a live `AVSubtitle` filled by the
264    // decode this seat is holding. Conversion copies every rect it
265    // takes — `AVSubtitleRect` has no refcounted buffer, so both lanes
266    // copy — and the FFmpeg-side allocations are released below, once
267    // there is something to release them in favour of.
268    let converted =
269      unsafe { convert::av_subtitle_to_subtitle_frame_as::<C>(self.scratch.inner.as_ptr()) };
270    match converted {
271      Ok(frame) => {
272        self.scratch.clear();
273        self.scratch_pending = false;
274        *dst = frame;
275        Ok(Received::Frame)
276      }
277      Err(e) if e.parks_in_decode() => {
278        // Kept: another attempt could carry this cue, and there is no
279        // other copy of it anywhere.
280        Err(SubtitleDecodeError::Convert(e))
281      }
282      Err(e) => {
283        // A cue nothing can carry is let go — freed immediately, so a
284        // caller that ignores the error cannot leave the scratch
285        // holding FFmpeg allocations, and re-offering it forever would
286        // stall the session.
287        self.scratch.clear();
288        self.scratch_pending = false;
289        Err(SubtitleDecodeError::Convert(e))
290      }
291    }
292  }
293
294  pub(crate) fn send_eof_impl(&mut self) -> Result<Sent, SubtitleDecodeError> {
295    // Subtitle decoders have no tail to drain — the legacy decode() API
296    // produces a cue inline with each packet — so nothing is forwarded
297    // to libavcodec here. What EOF does mean is that no further packet
298    // is coming, which is what `receive_frame` needs in order to answer
299    // `Ended` instead of asking for input that will never arrive.
300    //
301    // **Always `Accepted`, including under a held cue and including a
302    // second time.** The family's line is *sending data after the end
303    // is a fault; re-declaring the end is not* — a packet after EOF is
304    // input the caller believes will be decoded and will not be, while
305    // a second `send_eof` restates a fact that is already true and
306    // costs nothing. The held cue is still delivered by the next
307    // `receive_frame`, and only then does the seat answer `Ended`;
308    // refusing here would be back pressure with nothing behind it.
309    //
310    // The `swresample` seam one tier along is idempotent for the same
311    // reason. The raw FFmpeg decoders are the documented exception:
312    // libavcodec refuses a second flush packet with `AVERROR_EOF`, and
313    // that is the substrate's word, reported rather than papered over.
314    self.eof = true;
315    Ok(Sent::Accepted)
316  }
317
318  pub(crate) fn flush_impl(&mut self) -> Result<(), SubtitleDecodeError> {
319    self.decoder.flush();
320    // A held cue belongs to the position being abandoned.
321    self.scratch_pending = false;
322    self.scratch.clear();
323    // And the session is open again: flush is how a caller reuses this
324    // decoder for another stream, so the end it declared is retracted
325    // with the rest of the position.
326    self.eof = false;
327    Ok(())
328  }
329}
330
331macro_rules! subtitle_lane_face {
332  ($($lane:ty),+ $(,)?) => { $(
333    impl CarrierSubtitleStreamDecoder<$lane> {
334      /// Opens a subtitle decoder for `parameters`.
335      pub fn open(
336        parameters: Parameters,
337        time_base: Timebase,
338        limits: DecoderLimits,
339      ) -> Result<Self, SubtitleDecodeError> {
340        Self::open_impl(parameters, time_base, limits)
341      }
342
343      /// The time base associated with the source stream.
344      pub const fn time_base(&self) -> Timebase {
345        self.time_base_impl()
346      }
347
348      /// The budgets this decoder was opened with.
349      pub const fn limits(&self) -> DecoderLimits {
350        self.limits_impl()
351      }
352
353      /// The wrapped decoder context.
354      pub const fn inner(&self) -> &ffmpeg_next::decoder::Subtitle {
355        self.inner_impl()
356      }
357    }
358
359    impl SubtitleDecoder for CarrierSubtitleStreamDecoder<$lane> {
360      type Adapter = Ffmpeg;
361      type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
362      type Error = SubtitleDecodeError;
363
364      fn send_packet(
365        &mut self,
366        packet: &SubtitlePacket<SubtitlePacketExtra, Self::Buffer>,
367      ) -> Result<Sent, Self::Error> {
368        self.send_packet_impl(packet)
369      }
370
371      fn receive_frame(
372        &mut self,
373        dst: &mut SubtitleFrame<SubtitleFrameExtra, Self::Buffer>,
374      ) -> Result<Received, Self::Error> {
375        self.receive_frame_impl(dst)
376      }
377
378      fn send_eof(&mut self) -> Result<Sent, Self::Error> {
379        self.send_eof_impl()
380      }
381
382      fn flush(&mut self) -> Result<(), Self::Error> {
383        self.flush_impl()
384      }
385    }
386  )+ };
387}
388
389subtitle_lane_face!(crate::View, crate::Owned);
390
391/// Errors from [`FfmpegSubtitleStreamDecoder`] — **faults and the
392/// send-side refusal**.
393///
394/// `NoFrameReady` used to be here, and it was the crate's worst
395/// conflation: `send_eof` on this backend is a no-op, so "no cue yet"
396/// and "there will be no more cues" were the same value, and a caller
397/// draining to the end of a subtitle track had nothing to stop on. Both
398/// are [`Received`] states now — [`Received::NeedsInput`] and
399/// [`Received::Ended`] — told apart by the session's own EOF latch.
400///
401/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
402/// fail are discovered — a backend, a ceiling, a corruption a codec
403/// learns to report — and a consumer that meets one it has never heard
404/// of should take its generic-fault path. That is exactly what the
405/// wildcard arm this attribute forces is for. The two status
406/// vocabularies opposite it,
407/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
408/// are exhaustive for the mirror-image reason: their arms are the
409/// substrate's fixed state set, and there the wildcard would be dead
410/// weight hiding a state a consumer forgot.
411#[derive(thiserror::Error, Debug, IsVariant, Unwrap, TryUnwrap)]
412#[unwrap(ref, ref_mut)]
413#[try_unwrap(ref, ref_mut)]
414#[non_exhaustive]
415pub enum SubtitleDecodeError {
416  /// The wrapped `ffmpeg::decoder::Subtitle` reported an error.
417  #[error(transparent)]
418  Decode(#[from] Error),
419  /// Conversion from FFmpeg's `AVSubtitle` to mediadecode's
420  /// `SubtitleFrame` failed.
421  #[error(transparent)]
422  Convert(#[from] ConvertError),
423
424  /// [`send_packet`](SubtitleDecoder::send_packet) was called after
425  /// [`send_eof`](SubtitleDecoder::send_eof). Call
426  /// [`flush`](SubtitleDecoder::flush) first to reuse the decoder for
427  /// another stream.
428  ///
429  /// **A caller usage fault, not back pressure**, which is the line
430  /// that keeps it here while the held-cue refusal became
431  /// [`Sent::MustDrain`]. Draining changes nothing about it: this
432  /// session will refuse every packet until `flush`, so answering
433  /// `MustDrain` would send the caller into a loop with no exit — and,
434  /// worse, the loop's next offer would be *accepted*, reversing a
435  /// terminal [`Received::Ended`].
436  ///
437  /// Named `AfterEof` rather than `AtEof` because that is what this
438  /// crate already calls the condition one seam over
439  /// ([`ResampleError::AfterEof`](crate::ResampleError::AfterEof)), and
440  /// one condition deserves one word.
441  #[error("send_packet after send_eof; flush() first to start another stream")]
442  AfterEof,
443}