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