oxideav-mkv 0.0.9

Pure-Rust Matroska (MKV/WebM) container for oxideav
Documentation
//! Map between Matroska codec ID strings and oxideav [`CodecId`].
//!
//! Reference: <https://www.matroska.org/technical/codec_specs.html>

use oxideav_core::CodecId;

/// Matroska CodecID strings that WebM permits.
///
/// WebM is a restricted subset of Matroska: the WebM specification
/// (<https://www.webmproject.org/docs/container/>) only allows VP8, VP9 and
/// AV1 for video, and Vorbis and Opus for audio. Anything else must be
/// rejected by a WebM muxer — writing e.g. H.264 into a DocType="webm"
/// file produces an invalid WebM even though the container bytes are valid
/// Matroska.
pub const ALLOWED_WEBM_CODECS: &[&str] = &[
    // Video.
    "V_VP8", "V_VP9", "V_AV1", // Audio.
    "A_VORBIS", "A_OPUS",
];

/// Return true if `matroska_codec_id` (e.g. `"A_OPUS"`) is permitted inside
/// a WebM container.
pub fn is_webm_matroska_codec(matroska_codec_id: &str) -> bool {
    ALLOWED_WEBM_CODECS.contains(&matroska_codec_id)
}

/// Return true if the oxideav-internal [`CodecId`] corresponds to a codec
/// that WebM permits. Unknown / unmapped codec ids return false.
pub fn is_webm_codec(id: &CodecId) -> bool {
    matches!(id.as_str(), "vp8" | "vp9" | "av1" | "vorbis" | "opus")
}

/// Best-effort mapping from a Matroska codec id string (e.g. `"A_FLAC"`) to
/// the oxideav codec id we use internally.
///
/// `codec_private` is consulted for `V_MS/VFW/FOURCC` tracks because the
/// BITMAPINFOHEADER's `biCompression` field carries the actual codec. For
/// other codec ids it is ignored.
pub fn from_matroska(s: &str, codec_private: &[u8]) -> CodecId {
    let id = match s {
        "A_FLAC" => "flac",
        "A_OPUS" => "opus",
        "A_VORBIS" => "vorbis",
        "A_PCM/INT/LIT" => "pcm_s16le",
        "A_PCM/INT/BIG" => "pcm_s16be",
        "A_PCM/FLOAT/IEEE" => "pcm_f32le",
        "A_AAC" | "A_AAC/MPEG4/LC" | "A_AAC/MPEG2/LC" => "aac",
        "A_MPEG/L3" => "mp3",
        "A_AC3" => "ac3",
        "A_EAC3" => "eac3",
        // DTS family — Matroska maps every DTS variant (Core, DTS-HD HRA,
        // DTS-HD MA, DTS:X) onto a single `A_DTS` CodecID. The bitstream
        // syntex itself distinguishes them; the container only carries the
        // wire bytes. See <https://www.matroska.org/technical/codec_specs.html>.
        // Blu-ray ships DTS-HD MA / HRA tracks via this CodecID with no
        // extradata required for passthrough.
        "A_DTS" => "dts",
        // Dolby TrueHD (BD primary lossless audio). Same passthrough model
        // as DTS: container carries the raw TrueHD frames, decoder picks
        // up the substream metadata from the bitstream.
        "A_TRUEHD" => "truehd",
        "V_VP8" => "vp8",
        "V_VP9" => "vp9",
        "V_AV1" => "av1",
        "V_MPEG1" => "mpeg1video",
        "V_MPEG2" => "mpeg2video",
        "V_MPEG4/ISO/AVC" => "h264",
        "V_MPEGH/ISO/HEVC" => "h265",
        "V_FFV1" => "ffv1",
        "V_THEORA" => "theora",
        "V_MS/VFW/FOURCC" => return from_bitmapinfoheader(codec_private),
        // Subtitles. Matroska's "S_TEXT/*" family carries plain UTF-8 with
        // per-format markup; "S_HDMV/PGS" carries Blu-ray bitmap subs;
        // "S_VOBSUB" carries DVD bitmap subs. We map them to short oxideav
        // codec ids so downstream decoders / muxers can recognise the
        // format without parsing the Matroska string themselves.
        "S_TEXT/UTF8" => "subrip",
        "S_TEXT/SSA" => "ssa",
        "S_TEXT/ASS" => "ass",
        "S_TEXT/WEBVTT" => "webvtt",
        "S_TEXT/USF" => "usf",
        "S_VOBSUB" => "dvd_subtitle",
        "S_HDMV/PGS" => "hdmv_pgs_subtitle",
        "S_HDMV/TEXTST" => "hdmv_text_subtitle",
        "S_DVBSUB" => "dvb_subtitle",
        "S_KATE" => "kate",
        other => return CodecId::new(format!("mkv:{other}")),
    };
    CodecId::new(id)
}

/// Extract the codec id from a BITMAPINFOHEADER `CodecPrivate` blob. The
/// fourcc lives at bytes 16..20 (biCompression). Unrecognised fourcc falls
/// back to `mkv:BI/<fourcc>`.
fn from_bitmapinfoheader(cp: &[u8]) -> CodecId {
    if cp.len() < 20 {
        return CodecId::new("mkv:BI/<truncated>");
    }
    let fourcc = &cp[16..20];
    let fourcc_str = std::str::from_utf8(fourcc).unwrap_or("????");
    match fourcc_str {
        "FFV1" => CodecId::new("ffv1"),
        other => CodecId::new(format!("mkv:BI/{other}")),
    }
}

/// If `codec_private` is a BITMAPINFOHEADER, return the inner codec-specific
/// extradata (everything after the 40-byte header). Otherwise returns the
/// slice unchanged.
pub fn strip_bitmapinfoheader(codec_id: &str, codec_private: &[u8]) -> Vec<u8> {
    if codec_id == "V_MS/VFW/FOURCC" && codec_private.len() >= 40 {
        codec_private[40..].to_vec()
    } else {
        codec_private.to_vec()
    }
}

/// Inverse of `from_matroska` for codecs we support writing. Returns `None`
/// for codecs without a Matroska mapping we know.
pub fn to_matroska(id: &CodecId) -> Option<&'static str> {
    Some(match id.as_str() {
        "flac" => "A_FLAC",
        "opus" => "A_OPUS",
        "vorbis" => "A_VORBIS",
        "pcm_s16le" => "A_PCM/INT/LIT",
        "pcm_s16be" => "A_PCM/INT/BIG",
        "pcm_f32le" => "A_PCM/FLOAT/IEEE",
        "aac" => "A_AAC",
        "mp3" => "A_MPEG/L3",
        "ac3" => "A_AC3",
        "eac3" => "A_EAC3",
        // DTS family — see notes on `A_DTS` in `from_matroska`. The Matroska
        // CodecID does not distinguish DTS-HD MA / HRA / Core: callers carry
        // those distinctions in the bitstream itself.
        "dts" => "A_DTS",
        // Dolby TrueHD — BD's primary lossless audio. Passthrough only;
        // muxer never re-encodes.
        "truehd" => "A_TRUEHD",
        "vp8" => "V_VP8",
        "vp9" => "V_VP9",
        "av1" => "V_AV1",
        "mpeg1video" => "V_MPEG1",
        "mpeg2video" => "V_MPEG2",
        "h264" => "V_MPEG4/ISO/AVC",
        "h265" => "V_MPEGH/ISO/HEVC",
        "ffv1" => "V_FFV1",
        "subrip" => "S_TEXT/UTF8",
        "ssa" => "S_TEXT/SSA",
        "ass" => "S_TEXT/ASS",
        "webvtt" => "S_TEXT/WEBVTT",
        "usf" => "S_TEXT/USF",
        "dvd_subtitle" => "S_VOBSUB",
        "hdmv_pgs_subtitle" => "S_HDMV/PGS",
        "hdmv_text_subtitle" => "S_HDMV/TEXTST",
        "dvb_subtitle" => "S_DVBSUB",
        "kate" => "S_KATE",
        _ => return None,
    })
}

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

    /// Helper: round-trip a Matroska CodecID through
    /// [`from_matroska`] → [`to_matroska`] and assert the result is
    /// byte-identical to the original. Codec_private is empty (only
    /// `V_MS/VFW/FOURCC` consults it).
    fn assert_round_trip(matroska_id: &str) {
        let cid = from_matroska(matroska_id, &[]);
        let back = to_matroska(&cid).unwrap_or_else(|| {
            panic!(
                "no inverse mapping for {matroska_id} → oxideav id {}",
                cid.as_str()
            )
        });
        assert_eq!(
            back,
            matroska_id,
            "round-trip mismatch: {matroska_id} → {} → {back}",
            cid.as_str()
        );
    }

    /// Every Blu-ray-relevant Matroska CodecID must round-trip cleanly
    /// through the bi-directional table. Adding a new BD CodecID
    /// requires an entry in [`from_matroska`], a matching entry in
    /// [`to_matroska`], and a line here.
    #[test]
    fn bd_codec_ids_round_trip() {
        // Video.
        assert_round_trip("V_MPEG4/ISO/AVC");
        assert_round_trip("V_MPEGH/ISO/HEVC");
        // Audio.
        assert_round_trip("A_AC3");
        assert_round_trip("A_EAC3");
        assert_round_trip("A_DTS");
        assert_round_trip("A_TRUEHD");
        assert_round_trip("A_PCM/INT/BIG");
        // Subtitle.
        assert_round_trip("S_HDMV/PGS");
        assert_round_trip("S_HDMV/TEXTST");
    }

    /// `A_DTS` and `A_TRUEHD` were added for BD passthrough. Locked
    /// down so the oxideav-internal codec ids match the rest of the
    /// project's naming convention (lowercase, short canonical
    /// names).
    #[test]
    fn dts_and_truehd_map_to_expected_oxideav_ids() {
        assert_eq!(from_matroska("A_DTS", &[]).as_str(), "dts");
        assert_eq!(from_matroska("A_TRUEHD", &[]).as_str(), "truehd");
        assert_eq!(to_matroska(&CodecId::new("dts")), Some("A_DTS"));
        assert_eq!(to_matroska(&CodecId::new("truehd")), Some("A_TRUEHD"));
    }

    /// BD audio is big-endian LPCM (BD-ROM Part 3 §5.4). Make sure
    /// the muxer maps the existing `pcm_s16be` id through to
    /// `A_PCM/INT/BIG` and not the more common little-endian flavour.
    #[test]
    fn bd_lpcm_maps_to_big_endian_pcm() {
        assert_eq!(
            to_matroska(&CodecId::new("pcm_s16be")),
            Some("A_PCM/INT/BIG")
        );
        assert_eq!(from_matroska("A_PCM/INT/BIG", &[]).as_str(), "pcm_s16be");
    }
}