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) =
139 build_codec_context(¶meters, limits).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(¶meters).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 = unsafe {
269 convert::av_subtitle_to_subtitle_frame_as::<C>(self.scratch.inner.as_ptr(), self.time_base)
270 };
271 match converted {
272 Ok(frame) => {
273 self.scratch.clear();
274 self.scratch_pending = false;
275 *dst = frame;
276 Ok(Received::Frame)
277 }
278 Err(e) if e.parks_in_decode() => {
279 // Kept: another attempt could carry this cue, and there is no
280 // other copy of it anywhere.
281 Err(SubtitleDecodeError::Convert(e))
282 }
283 Err(e) => {
284 // A cue nothing can carry is let go — freed immediately, so a
285 // caller that ignores the error cannot leave the scratch
286 // holding FFmpeg allocations, and re-offering it forever would
287 // stall the session.
288 self.scratch.clear();
289 self.scratch_pending = false;
290 Err(SubtitleDecodeError::Convert(e))
291 }
292 }
293 }
294
295 pub(crate) fn send_eof_impl(&mut self) -> Result<Sent, SubtitleDecodeError> {
296 // Subtitle decoders have no tail to drain — the legacy decode() API
297 // produces a cue inline with each packet — so nothing is forwarded
298 // to libavcodec here. What EOF does mean is that no further packet
299 // is coming, which is what `receive_frame` needs in order to answer
300 // `Ended` instead of asking for input that will never arrive.
301 //
302 // **Always `Accepted`, including under a held cue and including a
303 // second time.** The family's line is *sending data after the end
304 // is a fault; re-declaring the end is not* — a packet after EOF is
305 // input the caller believes will be decoded and will not be, while
306 // a second `send_eof` restates a fact that is already true and
307 // costs nothing. The held cue is still delivered by the next
308 // `receive_frame`, and only then does the seat answer `Ended`;
309 // refusing here would be back pressure with nothing behind it.
310 //
311 // The `swresample` seam one tier along is idempotent for the same
312 // reason. The raw FFmpeg decoders are the documented exception:
313 // libavcodec refuses a second flush packet with `AVERROR_EOF`, and
314 // that is the substrate's word, reported rather than papered over.
315 self.eof = true;
316 Ok(Sent::Accepted)
317 }
318
319 pub(crate) fn flush_impl(&mut self) -> Result<(), SubtitleDecodeError> {
320 self.decoder.flush();
321 // A held cue belongs to the position being abandoned.
322 self.scratch_pending = false;
323 self.scratch.clear();
324 // And the session is open again: flush is how a caller reuses this
325 // decoder for another stream, so the end it declared is retracted
326 // with the rest of the position.
327 self.eof = false;
328 Ok(())
329 }
330}
331
332macro_rules! subtitle_lane_face {
333 ($($lane:ty),+ $(,)?) => { $(
334 impl CarrierSubtitleStreamDecoder<$lane> {
335 /// Opens a subtitle decoder for `parameters`.
336 pub fn open(
337 parameters: Parameters,
338 time_base: Timebase,
339 limits: DecoderLimits,
340 ) -> Result<Self, SubtitleDecodeError> {
341 Self::open_impl(parameters, time_base, limits)
342 }
343
344 /// The time base associated with the source stream.
345 pub const fn time_base(&self) -> Timebase {
346 self.time_base_impl()
347 }
348
349 /// The budgets this decoder was opened with.
350 pub const fn limits(&self) -> DecoderLimits {
351 self.limits_impl()
352 }
353
354 /// The wrapped decoder context.
355 pub const fn inner(&self) -> &ffmpeg_next::decoder::Subtitle {
356 self.inner_impl()
357 }
358 }
359
360 impl SubtitleDecoder for CarrierSubtitleStreamDecoder<$lane> {
361 type Adapter = Ffmpeg;
362 type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
363 type Error = SubtitleDecodeError;
364
365 fn send_packet(
366 &mut self,
367 packet: &SubtitlePacket<SubtitlePacketExtra, Self::Buffer>,
368 ) -> Result<Sent, Self::Error> {
369 self.send_packet_impl(packet)
370 }
371
372 fn receive_frame(
373 &mut self,
374 dst: &mut SubtitleFrame<SubtitleFrameExtra, Self::Buffer>,
375 ) -> Result<Received, Self::Error> {
376 self.receive_frame_impl(dst)
377 }
378
379 fn send_eof(&mut self) -> Result<Sent, Self::Error> {
380 self.send_eof_impl()
381 }
382
383 fn flush(&mut self) -> Result<(), Self::Error> {
384 self.flush_impl()
385 }
386 }
387 )+ };
388}
389
390subtitle_lane_face!(crate::View, crate::Owned);
391
392/// Errors from [`FfmpegSubtitleStreamDecoder`] — **faults and the
393/// send-side refusal**.
394///
395/// `NoFrameReady` used to be here, and it was the crate's worst
396/// conflation: `send_eof` on this backend is a no-op, so "no cue yet"
397/// and "there will be no more cues" were the same value, and a caller
398/// draining to the end of a subtitle track had nothing to stop on. Both
399/// are [`Received`] states now — [`Received::NeedsInput`] and
400/// [`Received::Ended`] — told apart by the session's own EOF latch.
401///
402/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
403/// fail are discovered — a backend, a ceiling, a corruption a codec
404/// learns to report — and a consumer that meets one it has never heard
405/// of should take its generic-fault path. That is exactly what the
406/// wildcard arm this attribute forces is for. The two status
407/// vocabularies opposite it,
408/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
409/// are exhaustive for the mirror-image reason: their arms are the
410/// substrate's fixed state set, and there the wildcard would be dead
411/// weight hiding a state a consumer forgot.
412#[derive(thiserror::Error, Debug, IsVariant, Unwrap, TryUnwrap)]
413#[unwrap(ref, ref_mut)]
414#[try_unwrap(ref, ref_mut)]
415#[non_exhaustive]
416pub enum SubtitleDecodeError {
417 /// The wrapped `ffmpeg::decoder::Subtitle` reported an error.
418 #[error(transparent)]
419 Decode(#[from] Error),
420 /// Conversion from FFmpeg's `AVSubtitle` to mediadecode's
421 /// `SubtitleFrame` failed.
422 #[error(transparent)]
423 Convert(#[from] ConvertError),
424
425 /// [`send_packet`](SubtitleDecoder::send_packet) was called after
426 /// [`send_eof`](SubtitleDecoder::send_eof). Call
427 /// [`flush`](SubtitleDecoder::flush) first to reuse the decoder for
428 /// another stream.
429 ///
430 /// **A caller usage fault, not back pressure**, which is the line
431 /// that keeps it here while the held-cue refusal became
432 /// [`Sent::MustDrain`]. Draining changes nothing about it: this
433 /// session will refuse every packet until `flush`, so answering
434 /// `MustDrain` would send the caller into a loop with no exit — and,
435 /// worse, the loop's next offer would be *accepted*, reversing a
436 /// terminal [`Received::Ended`].
437 ///
438 /// Named `AfterEof` rather than `AtEof` because that is what this
439 /// crate already calls the condition one seam over
440 /// ([`ResampleError::AfterEof`](crate::ResampleError::AfterEof)), and
441 /// one condition deserves one word.
442 #[error("send_packet after send_eof; flush() first to start another stream")]
443 AfterEof,
444}