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