Skip to main content

oxideav_sub_image/
lib.rs

1//! Bitmap-native subtitle formats for oxideav.
2//!
3//! These subtitle formats don't carry text — each cue is a picture that
4//! overlays the video. The decoders in this crate therefore produce
5//! [`oxideav_core::Frame::Video`] values holding an RGBA canvas sized to
6//! the subtitle's display context (either the declared video frame size
7//! or the bitmap's own size, depending on format), not
8//! [`oxideav_core::Frame::Subtitle`].
9//!
10//! | Format   | Codec id  | Container name | Extensions    |
11//! |----------|-----------|----------------|---------------|
12//! | PGS      | `pgs`     | `pgs`          | `.sup`        |
13//! | DVB sub  | `dvbsub`  | *(none)*       | rides MPEG-TS |
14//! | VobSub   | `vobsub`  | `vobsub`       | `.idx`+`.sub` |
15//!
16//! ## Scope
17//!
18//! * PGS supports both decode and encode.
19//! * DVB subtitles and VobSub are decode-only for now. DVB subs need a
20//!   TS-aware muxer upstream; VobSub needs the `.idx`+`.sub` pair written
21//!   in lock-step which this crate doesn't yet expose.
22//! * One RGBA [`oxideav_core::VideoFrame`] is emitted per display-set
23//!   (cue change) — either the full video-canvas-sized frame (PGS/DVB)
24//!   or the bitmap's own rectangle (VobSub).
25//! * Output pixel format is always [`oxideav_core::PixelFormat::Rgba`].
26//! * `pts` on the emitted frame matches the cue start time (in the
27//!   packet's [`oxideav_core::TimeBase`]). Duration is carried on the
28//!   [`oxideav_core::Packet`] the container emits.
29//!
30//! See per-module docs for format-specific limitations.
31
32pub mod composite;
33pub mod dvbsub;
34pub mod pgs;
35pub mod vobsub;
36
37use oxideav_core::ContainerRegistry;
38use oxideav_core::RuntimeContext;
39use oxideav_core::{CodecCapabilities, CodecId, MediaType};
40use oxideav_core::{CodecInfo, CodecRegistry};
41
42/// Codec id for PGS / HDMV / Blu-ray `.sup` streams.
43pub const PGS_CODEC_ID: &str = "pgs";
44/// Codec id for DVB subtitle streams (ETSI EN 300 743).
45pub const DVBSUB_CODEC_ID: &str = "dvbsub";
46/// Codec id for VobSub / DVD SPU streams.
47pub const VOBSUB_CODEC_ID: &str = "vobsub";
48
49/// Register decoders for PGS, DVB subtitles, and VobSub.
50///
51/// Media type is `Subtitle` even though the emitted frames are
52/// `Frame::Video(Rgba)` — bitmap-subtitle codecs are subtitle
53/// streams at the container level but produce pre-rendered RGBA
54/// pictures (PGS, DVB subtitles, and VobSub all decode to bitmap
55/// frames per their respective specs). The `Subtitle` media-kind
56/// tag is the conventional way to surface this dual nature to
57/// downstream consumers (player, mixer, file writer) so they route
58/// the stream to subtitle handling but receive video frames.
59pub fn register_codecs(reg: &mut CodecRegistry) {
60    for (id, impl_name) in [
61        (PGS_CODEC_ID, "pgs_sw"),
62        (DVBSUB_CODEC_ID, "dvbsub_sw"),
63        (VOBSUB_CODEC_ID, "vobsub_sw"),
64    ] {
65        let caps = CodecCapabilities {
66            decode: true,
67            encode: false,
68            media_type: MediaType::Subtitle,
69            intra_only: true,
70            lossy: false,
71            lossless: true,
72            hardware_accelerated: false,
73            implementation: impl_name.into(),
74            max_width: None,
75            max_height: None,
76            max_bitrate: None,
77            max_sample_rate: None,
78            max_channels: None,
79            priority: 100,
80            accepted_pixel_formats: Vec::new(),
81        };
82        let factory = match id {
83            PGS_CODEC_ID => pgs::make_decoder,
84            DVBSUB_CODEC_ID => dvbsub::make_decoder,
85            VOBSUB_CODEC_ID => vobsub::make_decoder,
86            _ => unreachable!(),
87        };
88        reg.register(
89            CodecInfo::new(CodecId::new(id))
90                .capabilities(caps)
91                .decoder(factory),
92        );
93    }
94
95    // PGS encoder. The other two formats are decode-only for now: DVB
96    // subs need a TS-aware muxer to produce valid streams, and VobSub
97    // needs the `.idx`+`.sub` pair written in lock-step.
98    let pgs_enc_caps = CodecCapabilities {
99        decode: false,
100        encode: true,
101        media_type: MediaType::Subtitle,
102        intra_only: true,
103        lossy: true,
104        lossless: false,
105        hardware_accelerated: false,
106        implementation: "pgs_sw".into(),
107        max_width: None,
108        max_height: None,
109        max_bitrate: None,
110        max_sample_rate: None,
111        max_channels: None,
112        priority: 100,
113        accepted_pixel_formats: vec![oxideav_core::PixelFormat::Rgba],
114    };
115    reg.register(
116        CodecInfo::new(CodecId::new(PGS_CODEC_ID))
117            .capabilities(pgs_enc_caps)
118            .encoder(pgs::make_encoder),
119    );
120}
121
122/// Register the PGS (`.sup`) and VobSub (`.idx`+`.sub`) containers.
123///
124/// DVB subtitles aren't a standalone file container — they ride inside
125/// MPEG-TS — so no demuxer is registered for them here. The codec is
126/// what the MPEG-TS demuxer would dispatch to.
127pub fn register_containers(reg: &mut ContainerRegistry) {
128    pgs::register_container(reg);
129    vobsub::register_container(reg);
130}
131
132/// Unified registration entry point — installs PGS / DVB / VobSub
133/// codecs into the codec sub-registry and the PGS + VobSub containers
134/// into the container sub-registry of the supplied [`RuntimeContext`].
135///
136/// Also wired into [`oxideav_meta::register_all`] via the
137/// [`oxideav_core::register!`] macro below.
138pub fn register(ctx: &mut RuntimeContext) {
139    register_codecs(&mut ctx.codecs);
140    register_containers(&mut ctx.containers);
141}
142
143oxideav_core::register!("sub_image", register);
144
145#[cfg(test)]
146mod register_tests {
147    use super::*;
148
149    #[test]
150    fn register_via_runtime_context_installs_both_sides() {
151        let mut ctx = RuntimeContext::new();
152        register(&mut ctx);
153        let id = CodecId::new(PGS_CODEC_ID);
154        assert!(
155            ctx.codecs.has_decoder(&id),
156            "PGS decoder factory not installed via RuntimeContext"
157        );
158        assert!(
159            ctx.codecs.has_encoder(&id),
160            "PGS encoder factory not installed via RuntimeContext"
161        );
162        assert_eq!(
163            ctx.containers.container_for_extension("sup"),
164            Some("pgs"),
165            "PGS container extension not installed via RuntimeContext"
166        );
167    }
168}