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}