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