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, `receive_frame` drains one decoded frame at a time, and
11//! `NoFrameReady` is signalled via [`SubtitleDecodeError::NoFrameReady`].
12
13use std::option::Option;
14
15use derive_more::{IsVariant, TryUnwrap, Unwrap};
16use ffmpeg_next::{codec::Parameters, ffi::avsubtitle_free};
17use mediadecode::{
18  Timebase, decoder::SubtitleDecoder, frame::SubtitleFrame, packet::SubtitlePacket,
19};
20
21use crate::{
22  Error, Ffmpeg, FfmpegBuffer, boundary,
23  convert::{self, ConvertError},
24  decoder::build_codec_context,
25  extras::{SubtitleFrameExtra, SubtitlePacketExtra},
26};
27
28/// RAII wrapper that owns an `ffmpeg_next::Subtitle` scratch slot and
29/// frees the FFmpeg-side rect allocations on drop / explicit `clear`.
30///
31/// `ffmpeg::Subtitle::new()` zero-initializes; `decoder.decode()` may
32/// allocate per-rect storage (`AVSubtitleRect.text` / `.ass` /
33/// `.data[0]` / `.data[1]`) which only `avsubtitle_free` releases.
34/// Without this wrapper, every successful decode leaks until the
35/// decoder drops.
36struct ScratchSubtitle {
37  inner: ffmpeg_next::Subtitle,
38}
39
40impl ScratchSubtitle {
41  fn new() -> Self {
42    Self {
43      inner: ffmpeg_next::Subtitle::new(),
44    }
45  }
46
47  fn clear(&mut self) {
48    // SAFETY: `inner` holds a valid AVSubtitle (zero-initialized or
49    // populated by `decode`). `avsubtitle_free` frees the rect array
50    // and per-rect allocations, then leaves the struct in a state
51    // suitable for reuse by the next decode call.
52    unsafe { avsubtitle_free(self.inner.as_mut_ptr()) };
53  }
54}
55
56impl Drop for ScratchSubtitle {
57  fn drop(&mut self) {
58    self.clear();
59  }
60}
61
62/// `mediadecode::SubtitleDecoder` impl wrapping `ffmpeg::decoder::Subtitle`.
63///
64/// Subtitle decoders are stateless from FFmpeg's perspective — each
65/// `decode()` call consumes one packet and produces zero-or-one
66/// `AVSubtitle`. The pending-frame buffer here is a one-slot queue
67/// so the trait's `send_packet` / `receive_frame` split works.
68pub struct FfmpegSubtitleStreamDecoder {
69  decoder: ffmpeg_next::decoder::Subtitle,
70  scratch: ScratchSubtitle,
71  pending: Option<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>>,
72  time_base: Timebase,
73}
74
75impl FfmpegSubtitleStreamDecoder {
76  /// Opens a subtitle decoder for the given codec parameters.
77  pub fn open(parameters: Parameters, time_base: Timebase) -> Result<Self, SubtitleDecodeError> {
78    // Use the checked codec-context builder — `Context::from_parameters`
79    // is OOM-UB-prone (see `crate::decoder::build_codec_context`).
80    let ctx = build_codec_context(&parameters).map_err(SubtitleDecodeError::Decode)?;
81    let decoder = ctx
82      .decoder()
83      .subtitle()
84      .map_err(|e| SubtitleDecodeError::Decode(Error::Ffmpeg(e)))?;
85    Ok(Self {
86      decoder,
87      scratch: ScratchSubtitle::new(),
88      pending: None,
89      time_base,
90    })
91  }
92
93  /// Returns the time base associated with the source stream.
94  #[cfg_attr(not(tarpaulin), inline(always))]
95  pub const fn time_base(&self) -> Timebase {
96    self.time_base
97  }
98
99  /// Borrow the wrapped `ffmpeg::decoder::Subtitle`.
100  #[cfg_attr(not(tarpaulin), inline(always))]
101  pub const fn inner(&self) -> &ffmpeg_next::decoder::Subtitle {
102    &self.decoder
103  }
104}
105
106impl SubtitleDecoder for FfmpegSubtitleStreamDecoder {
107  type Adapter = Ffmpeg;
108  type Buffer = FfmpegBuffer;
109  type Error = SubtitleDecodeError;
110
111  fn send_packet(
112    &mut self,
113    packet: &SubtitlePacket<SubtitlePacketExtra, Self::Buffer>,
114  ) -> Result<(), Self::Error> {
115    // Disallow sending while a previously-decoded frame hasn't been
116    // drained yet. The legacy `decode()` API produces a frame inline,
117    // so a second send would silently drop the first — surface that
118    // as an error so callers notice the drain ordering.
119    if self.pending.is_some() {
120      return Err(SubtitleDecodeError::FramePending);
121    }
122    let av_pkt = boundary::ffmpeg_packet_from_subtitle_packet(packet)
123      .map_err(|e| SubtitleDecodeError::Decode(Error::PacketBuild(e)))?;
124    // Free any allocations from a previous decode before reusing the
125    // scratch — avoids leaking when the previous packet produced no
126    // frame (got == false, which still mutates the struct).
127    self.scratch.clear();
128    let got = self
129      .decoder
130      .decode(&av_pkt, &mut self.scratch.inner)
131      .map_err(|e| SubtitleDecodeError::Decode(Error::Ffmpeg(e)))?;
132    if got {
133      // SAFETY: scratch.inner is a live AVSubtitle just filled by
134      // decode. Conversion deep-copies all rect contents into owned
135      // FfmpegBuffers; the FFmpeg-side allocations are released
136      // unconditionally below (success and error paths both reach
137      // the next `clear()` on the next decode or on drop).
138      let result = unsafe {
139        convert::av_subtitle_to_subtitle_frame(self.scratch.inner.as_ptr(), self.time_base)
140      };
141      match result {
142        Ok(frame) => self.pending = Some(frame),
143        Err(e) => {
144          // Free immediately on conversion failure — without this, a
145          // caller that ignores the error and calls `flush` would
146          // bypass the scratch's deferred cleanup.
147          self.scratch.clear();
148          return Err(SubtitleDecodeError::Convert(e));
149        }
150      }
151    }
152    Ok(())
153  }
154
155  fn receive_frame(
156    &mut self,
157    dst: &mut SubtitleFrame<SubtitleFrameExtra, Self::Buffer>,
158  ) -> Result<(), Self::Error> {
159    match self.pending.take() {
160      Some(frame) => {
161        *dst = frame;
162        Ok(())
163      }
164      None => Err(SubtitleDecodeError::NoFrameReady),
165    }
166  }
167
168  fn send_eof(&mut self) -> Result<(), Self::Error> {
169    // Subtitle decoders have no draining — the legacy decode() API
170    // produces a frame inline with each packet. EOF is a no-op.
171    Ok(())
172  }
173
174  fn flush(&mut self) -> Result<(), Self::Error> {
175    self.decoder.flush();
176    self.pending = None;
177    self.scratch.clear();
178    Ok(())
179  }
180}
181
182/// Errors from [`FfmpegSubtitleStreamDecoder`].
183#[derive(thiserror::Error, Debug, IsVariant, Unwrap, TryUnwrap)]
184#[unwrap(ref, ref_mut)]
185#[try_unwrap(ref, ref_mut)]
186pub enum SubtitleDecodeError {
187  /// The wrapped `ffmpeg::decoder::Subtitle` reported an error.
188  #[error(transparent)]
189  Decode(#[from] Error),
190  /// Conversion from FFmpeg's `AVSubtitle` to mediadecode's
191  /// `SubtitleFrame` failed.
192  #[error(transparent)]
193  Convert(#[from] ConvertError),
194  /// `receive_frame` was called with no buffered frame ready — caller
195  /// should send another packet.
196  #[error("no subtitle frame ready; send another packet first")]
197  NoFrameReady,
198  /// `send_packet` was called while a decoded frame from a previous
199  /// packet hasn't been drained — the legacy `decode()` API can't
200  /// queue, so the caller must drain via `receive_frame` first.
201  #[error("subtitle frame already pending; drain via receive_frame first")]
202  FramePending,
203}