oxideav-sub-image 0.0.7

Pure-Rust bitmap-subtitle decoders: PGS (.sup Blu-ray), DVB subtitles, VobSub (.idx/.sub)
Documentation
//! Bitmap-native subtitle formats for oxideav.
//!
//! These subtitle formats don't carry text — each cue is a picture that
//! overlays the video. The decoders in this crate therefore produce
//! [`oxideav_core::Frame::Video`] values holding an RGBA canvas sized to
//! the subtitle's display context (either the declared video frame size
//! or the bitmap's own size, depending on format), not
//! [`oxideav_core::Frame::Subtitle`].
//!
//! | Format   | Codec id  | Container name | Extensions    |
//! |----------|-----------|----------------|---------------|
//! | PGS      | `pgs`     | `pgs`          | `.sup`        |
//! | DVB sub  | `dvbsub`  | *(none)*       | rides MPEG-TS |
//! | VobSub   | `vobsub`  | `vobsub`       | `.idx`+`.sub` |
//!
//! ## Scope
//!
//! * PGS supports both decode and encode.
//! * DVB subtitles and VobSub are decode-only for now. DVB subs need a
//!   TS-aware muxer upstream; VobSub needs the `.idx`+`.sub` pair written
//!   in lock-step which this crate doesn't yet expose.
//! * One RGBA [`oxideav_core::VideoFrame`] is emitted per display-set
//!   (cue change) — either the full video-canvas-sized frame (PGS/DVB)
//!   or the bitmap's own rectangle (VobSub).
//! * Output pixel format is always [`oxideav_core::PixelFormat::Rgba`].
//! * `pts` on the emitted frame matches the cue start time (in the
//!   packet's [`oxideav_core::TimeBase`]). Duration is carried on the
//!   [`oxideav_core::Packet`] the container emits.
//!
//! See per-module docs for format-specific limitations.

pub mod composite;
pub mod dvbsub;
pub mod pgs;
pub mod vobsub;

use oxideav_core::ContainerRegistry;
use oxideav_core::RuntimeContext;
use oxideav_core::{CodecCapabilities, CodecId, MediaType};
use oxideav_core::{CodecInfo, CodecRegistry};

/// Codec id for PGS / HDMV / Blu-ray `.sup` streams.
pub const PGS_CODEC_ID: &str = "pgs";
/// Codec id for DVB subtitle streams (ETSI EN 300 743).
pub const DVBSUB_CODEC_ID: &str = "dvbsub";
/// Codec id for VobSub / DVD SPU streams.
pub const VOBSUB_CODEC_ID: &str = "vobsub";

/// Register decoders for PGS, DVB subtitles, and VobSub.
///
/// Media type is `Subtitle` even though the emitted frames are
/// `Frame::Video(Rgba)` — bitmap-subtitle codecs are subtitle
/// streams at the container level but produce pre-rendered RGBA
/// pictures (PGS, DVB subtitles, and VobSub all decode to bitmap
/// frames per their respective specs). The `Subtitle` media-kind
/// tag is the conventional way to surface this dual nature to
/// downstream consumers (player, mixer, file writer) so they route
/// the stream to subtitle handling but receive video frames.
pub fn register_codecs(reg: &mut CodecRegistry) {
    for (id, impl_name) in [
        (PGS_CODEC_ID, "pgs_sw"),
        (DVBSUB_CODEC_ID, "dvbsub_sw"),
        (VOBSUB_CODEC_ID, "vobsub_sw"),
    ] {
        let caps = CodecCapabilities {
            decode: true,
            encode: false,
            media_type: MediaType::Subtitle,
            intra_only: true,
            lossy: false,
            lossless: true,
            hardware_accelerated: false,
            implementation: impl_name.into(),
            max_width: None,
            max_height: None,
            max_bitrate: None,
            max_sample_rate: None,
            max_channels: None,
            priority: 100,
            accepted_pixel_formats: Vec::new(),
        };
        let factory = match id {
            PGS_CODEC_ID => pgs::make_decoder,
            DVBSUB_CODEC_ID => dvbsub::make_decoder,
            VOBSUB_CODEC_ID => vobsub::make_decoder,
            _ => unreachable!(),
        };
        reg.register(
            CodecInfo::new(CodecId::new(id))
                .capabilities(caps)
                .decoder(factory),
        );
    }

    // PGS encoder. The other two formats are decode-only for now: DVB
    // subs need a TS-aware muxer to produce valid streams, and VobSub
    // needs the `.idx`+`.sub` pair written in lock-step.
    let pgs_enc_caps = CodecCapabilities {
        decode: false,
        encode: true,
        media_type: MediaType::Subtitle,
        intra_only: true,
        lossy: true,
        lossless: false,
        hardware_accelerated: false,
        implementation: "pgs_sw".into(),
        max_width: None,
        max_height: None,
        max_bitrate: None,
        max_sample_rate: None,
        max_channels: None,
        priority: 100,
        accepted_pixel_formats: vec![oxideav_core::PixelFormat::Rgba],
    };
    reg.register(
        CodecInfo::new(CodecId::new(PGS_CODEC_ID))
            .capabilities(pgs_enc_caps)
            .encoder(pgs::make_encoder),
    );
}

/// Register the PGS (`.sup`) and VobSub (`.idx`+`.sub`) containers.
///
/// DVB subtitles aren't a standalone file container — they ride inside
/// MPEG-TS — so no demuxer is registered for them here. The codec is
/// what the MPEG-TS demuxer would dispatch to.
pub fn register_containers(reg: &mut ContainerRegistry) {
    pgs::register_container(reg);
    vobsub::register_container(reg);
}

/// Unified registration entry point — installs PGS / DVB / VobSub
/// codecs into the codec sub-registry and the PGS + VobSub containers
/// into the container sub-registry of the supplied [`RuntimeContext`].
///
/// Also wired into [`oxideav_meta::register_all`] via the
/// [`oxideav_core::register!`] macro below.
pub fn register(ctx: &mut RuntimeContext) {
    register_codecs(&mut ctx.codecs);
    register_containers(&mut ctx.containers);
}

oxideav_core::register!("sub_image", register);

#[cfg(test)]
mod register_tests {
    use super::*;

    #[test]
    fn register_via_runtime_context_installs_both_sides() {
        let mut ctx = RuntimeContext::new();
        register(&mut ctx);
        let id = CodecId::new(PGS_CODEC_ID);
        assert!(
            ctx.codecs.has_decoder(&id),
            "PGS decoder factory not installed via RuntimeContext"
        );
        assert!(
            ctx.codecs.has_encoder(&id),
            "PGS encoder factory not installed via RuntimeContext"
        );
        assert_eq!(
            ctx.containers.container_for_extension("sup"),
            Some("pgs"),
            "PGS container extension not installed via RuntimeContext"
        );
    }
}