use std::collections::VecDeque;
use std::io::{Read, SeekFrom};
use std::path::{Path, PathBuf};
use oxideav_core::Decoder;
use oxideav_core::{
CodecId, CodecParameters, CodecResolver, Error, Frame, MediaType, Packet, PixelFormat, Result,
StreamInfo, TimeBase, VideoFrame, VideoPlane,
};
use oxideav_core::{ContainerRegistry, Demuxer, ProbeData, ProbeScore, ReadSeek};
use crate::VOBSUB_CODEC_ID;
#[derive(Clone, Debug, Default)]
pub struct VobSubIdx {
pub size: (u16, u16),
pub palette_rgb: [[u8; 3]; 16],
pub has_palette: bool,
pub cues: Vec<(i64, u64)>,
}
pub fn parse_idx(text: &str) -> Result<VobSubIdx> {
let mut idx = VobSubIdx::default();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(rest) = line.strip_prefix("size:") {
let rest = rest.trim();
if let Some((w, h)) = rest.split_once('x') {
let w: u16 = w
.trim()
.parse()
.map_err(|e| Error::invalid(format!("vobsub idx: bad size width: {e}")))?;
let h: u16 = h
.trim()
.parse()
.map_err(|e| Error::invalid(format!("vobsub idx: bad size height: {e}")))?;
idx.size = (w, h);
}
} else if let Some(rest) = line.strip_prefix("palette:") {
parse_palette_line(rest.trim(), &mut idx)?;
} else if let Some(rest) = line.strip_prefix("timestamp:") {
parse_timestamp_line(rest.trim(), &mut idx)?;
}
}
Ok(idx)
}
fn parse_palette_line(s: &str, idx: &mut VobSubIdx) -> Result<()> {
let mut cnt = 0usize;
for token in s
.split(|c: char| c == ',' || c.is_whitespace())
.filter(|t| !t.is_empty())
{
if cnt >= 16 {
break;
}
let hex = token.trim_start_matches("0x");
let v = u32::from_str_radix(hex, 16)
.map_err(|e| Error::invalid(format!("vobsub idx: bad palette entry '{token}': {e}")))?;
let r = ((v >> 16) & 0xFF) as u8;
let g = ((v >> 8) & 0xFF) as u8;
let b = (v & 0xFF) as u8;
idx.palette_rgb[cnt] = [r, g, b];
cnt += 1;
}
if cnt > 0 {
idx.has_palette = true;
}
Ok(())
}
fn parse_timestamp_line(s: &str, idx: &mut VobSubIdx) -> Result<()> {
let mut ts_str: Option<&str> = None;
let mut filepos_str: Option<&str> = None;
for part in s.split(',') {
let part = part.trim();
if let Some(rest) = part.strip_prefix("filepos:") {
filepos_str = Some(rest.trim());
} else if ts_str.is_none() {
ts_str = Some(part);
}
}
let ts = ts_str.ok_or_else(|| Error::invalid("vobsub idx: timestamp missing"))?;
let fp = filepos_str.ok_or_else(|| Error::invalid("vobsub idx: filepos missing"))?;
let mut parts = ts.split(':');
let h: i64 = parts
.next()
.unwrap_or("0")
.parse()
.map_err(|_| Error::invalid("vobsub idx: timestamp hours"))?;
let m: i64 = parts
.next()
.unwrap_or("0")
.parse()
.map_err(|_| Error::invalid("vobsub idx: timestamp minutes"))?;
let s_: i64 = parts
.next()
.unwrap_or("0")
.parse()
.map_err(|_| Error::invalid("vobsub idx: timestamp seconds"))?;
let ms: i64 = parts
.next()
.unwrap_or("0")
.parse()
.map_err(|_| Error::invalid("vobsub idx: timestamp millis"))?;
let us = ((((h * 60) + m) * 60) + s_) * 1_000_000 + ms * 1_000;
let filepos = u64::from_str_radix(fp.trim_start_matches("0x"), 16)
.or_else(|_| fp.parse::<u64>())
.map_err(|_| Error::invalid("vobsub idx: bad filepos"))?;
idx.cues.push((us, filepos));
Ok(())
}
#[derive(Clone, Debug, Default)]
pub struct Spu {
pub x1: u16,
pub y1: u16,
pub x2: u16,
pub y2: u16,
pub palette_sel: [u8; 4],
pub alpha: [u8; 4],
pub start_delay_raw: u16,
pub stop_delay_raw: u16,
pub top_rle_off: u16,
pub bot_rle_off: u16,
}
pub fn parse_and_decode_spu(spu: &[u8]) -> Result<(Spu, Vec<u8>, (u16, u16))> {
if spu.len() < 4 {
return Err(Error::invalid("vobsub SPU: too short"));
}
let spu_len = u16::from_be_bytes([spu[0], spu[1]]) as usize;
let ctrl_off = u16::from_be_bytes([spu[2], spu[3]]) as usize;
if spu_len > spu.len() || ctrl_off > spu_len || ctrl_off < 4 {
return Err(Error::invalid("vobsub SPU: inconsistent sizes"));
}
let mut out = Spu::default();
let mut pos = ctrl_off;
let mut first_seq = true;
loop {
if pos + 4 > spu_len {
break;
}
let delay = u16::from_be_bytes([spu[pos], spu[pos + 1]]);
let next = u16::from_be_bytes([spu[pos + 2], spu[pos + 3]]) as usize;
let mut cmd_pos = pos + 4;
while cmd_pos < spu_len {
let cmd = spu[cmd_pos];
cmd_pos += 1;
match cmd {
0x00 => {} 0x01 => {
if first_seq {
out.start_delay_raw = delay;
}
}
0x02 => {
out.stop_delay_raw = delay;
}
0x03 => {
if cmd_pos + 2 > spu_len {
return Err(Error::invalid("vobsub SPU: palette command truncated"));
}
let b0 = spu[cmd_pos];
let b1 = spu[cmd_pos + 1];
cmd_pos += 2;
out.palette_sel[0] = b0 >> 4; out.palette_sel[1] = b0 & 0x0F; out.palette_sel[2] = b1 >> 4; out.palette_sel[3] = b1 & 0x0F; }
0x04 => {
if cmd_pos + 2 > spu_len {
return Err(Error::invalid("vobsub SPU: alpha command truncated"));
}
let b0 = spu[cmd_pos];
let b1 = spu[cmd_pos + 1];
cmd_pos += 2;
out.alpha[0] = b0 >> 4;
out.alpha[1] = b0 & 0x0F;
out.alpha[2] = b1 >> 4;
out.alpha[3] = b1 & 0x0F;
}
0x05 => {
if cmd_pos + 6 > spu_len {
return Err(Error::invalid("vobsub SPU: coords command truncated"));
}
let b0 = spu[cmd_pos];
let b1 = spu[cmd_pos + 1];
let b2 = spu[cmd_pos + 2];
let b3 = spu[cmd_pos + 3];
let b4 = spu[cmd_pos + 4];
let b5 = spu[cmd_pos + 5];
cmd_pos += 6;
out.x1 = (((b0 as u16) << 4) | ((b1 as u16) >> 4)) & 0x0FFF;
out.x2 = ((((b1 as u16) & 0x0F) << 8) | (b2 as u16)) & 0x0FFF;
out.y1 = (((b3 as u16) << 4) | ((b4 as u16) >> 4)) & 0x0FFF;
out.y2 = ((((b4 as u16) & 0x0F) << 8) | (b5 as u16)) & 0x0FFF;
}
0x06 => {
if cmd_pos + 4 > spu_len {
return Err(Error::invalid("vobsub SPU: rle-offsets command truncated"));
}
out.top_rle_off = u16::from_be_bytes([spu[cmd_pos], spu[cmd_pos + 1]]);
out.bot_rle_off = u16::from_be_bytes([spu[cmd_pos + 2], spu[cmd_pos + 3]]);
cmd_pos += 4;
}
0xFF => {
break;
}
_ => {
return Err(Error::invalid(format!(
"vobsub SPU: unknown command 0x{:02X}",
cmd
)));
}
}
}
first_seq = false;
if next <= pos {
break;
}
pos = next;
}
if out.x2 < out.x1 || out.y2 < out.y1 {
return Err(Error::invalid("vobsub SPU: inverted coords"));
}
let width = (out.x2 - out.x1 + 1) as usize;
let height = (out.y2 - out.y1 + 1) as usize;
let mut pixels = vec![0u8; width * height];
if width > 0 && height > 0 {
let top_off = out.top_rle_off as usize;
let bot_off = out.bot_rle_off as usize;
if top_off >= spu_len {
return Err(Error::invalid("vobsub SPU: top offset out of range"));
}
let bot_end = if bot_off > top_off { bot_off } else { ctrl_off };
let top_bytes = &spu[top_off..bot_end.min(ctrl_off)];
let bot_bytes = if bot_off > 0 {
&spu[bot_off..ctrl_off]
} else {
&[][..]
};
decode_rle_field(top_bytes, width, height, 0, 2, &mut pixels)?;
if !bot_bytes.is_empty() {
decode_rle_field(bot_bytes, width, height, 1, 2, &mut pixels)?;
}
}
Ok((out, pixels, (width as u16, height as u16)))
}
fn decode_rle_field(
buf: &[u8],
width: usize,
height: usize,
start_row: usize,
row_step: usize,
pixels: &mut [u8],
) -> Result<()> {
let mut bits = NibbleReader::new(buf);
let mut row = start_row;
let mut col = 0usize;
while row < height {
let first = bits.read(4)?;
let (count, colour) = if first >= 4 {
(first >> 2, (first & 0x03) as u8)
} else if first > 0 {
let n1 = bits.read(4)?;
let combined = (first << 4) | n1;
(combined >> 2, (combined & 0x03) as u8)
} else {
let n1 = bits.read(4)?;
if n1 >= 4 {
let combined = (first << 8) | (n1 << 4) | bits.read(4)?;
(combined >> 2, (combined & 0x03) as u8)
} else if n1 > 0 {
let combined = (first << 12) | (n1 << 8) | (bits.read(4)? << 4) | bits.read(4)?;
(combined >> 2, (combined & 0x03) as u8)
} else {
let n2 = bits.read(4)?;
let combined = (first << 12) | (n1 << 8) | (n2 << 4) | bits.read(4)?;
(0, (combined & 0x03) as u8)
}
};
let run = if count == 0 {
width.saturating_sub(col)
} else {
count as usize
};
let end = (col + run).min(width);
if row < height {
let base = row * width + col;
for px in &mut pixels[base..base + (end - col)] {
*px = colour;
}
}
col = end;
if col >= width {
bits.align();
col = 0;
row += row_step;
if run == 0 {
}
}
}
Ok(())
}
struct NibbleReader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> NibbleReader<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn read(&mut self, n: u32) -> Result<u32> {
debug_assert!(n == 4);
if self.pos / 2 >= self.buf.len() {
return Err(Error::invalid("vobsub: RLE bitstream ran out"));
}
let b = self.buf[self.pos / 2];
let nibble = if self.pos % 2 == 0 { b >> 4 } else { b & 0x0F };
self.pos += 1;
Ok(nibble as u32)
}
fn align(&mut self) {
if self.pos % 2 != 0 {
self.pos += 1;
}
}
}
pub fn register_container(reg: &mut ContainerRegistry) {
reg.register_demuxer("vobsub", open_vobsub);
reg.register_extension("idx", "vobsub");
reg.register_extension("sub", "vobsub");
reg.register_probe("vobsub", probe_vobsub);
}
fn probe_vobsub(p: &ProbeData) -> ProbeScore {
let s = std::str::from_utf8(p.buf).ok().unwrap_or("");
let hit = s.contains("# VobSub index file")
|| s.contains("\nsize:")
|| s.starts_with("size:")
|| s.contains("\ntimestamp:");
match (hit, p.ext) {
(true, Some("idx")) => 100,
(true, _) => 75,
(false, Some("idx")) => 25,
_ => 0,
}
}
fn open_vobsub(
mut input: Box<dyn ReadSeek>,
_codecs: &dyn CodecResolver,
) -> Result<Box<dyn Demuxer>> {
input.seek(SeekFrom::Start(0))?;
let mut buf = Vec::new();
input.read_to_end(&mut buf)?;
let text = String::from_utf8_lossy(&buf).into_owned();
let idx = parse_idx(&text)?;
let sub_path = find_sub_alongside(&text);
let sub_bytes = match sub_path {
Some(p) => std::fs::read(&p).ok().unwrap_or_default(),
None => extract_inline_sub(&text).unwrap_or_default(),
};
let packets = build_packets(&idx, &sub_bytes);
let (w, h) = idx.size;
let mut params = CodecParameters::video(CodecId::new(VOBSUB_CODEC_ID));
params.media_type = MediaType::Subtitle;
params.width = Some(w as u32);
params.height = Some(h as u32);
params.pixel_format = Some(PixelFormat::Rgba);
let mut extra = Vec::with_capacity(48);
for entry in &idx.palette_rgb {
extra.extend_from_slice(entry);
}
params.extradata = extra;
let total_us = packets.back().and_then(|p| p.pts).unwrap_or(0);
let stream = StreamInfo {
index: 0,
time_base: TimeBase::new(1, 1_000_000),
duration: Some(total_us),
start_time: Some(0),
params,
};
Ok(Box::new(VobSubDemuxer {
streams: [stream],
packets,
}))
}
fn find_sub_alongside(idx_text: &str) -> Option<PathBuf> {
for line in idx_text.lines() {
if let Some(path) = line.strip_prefix("# idx-path:") {
let base = Path::new(path.trim()).with_extension("sub");
return Some(base);
}
}
None
}
fn extract_inline_sub(idx_text: &str) -> Option<Vec<u8>> {
for line in idx_text.lines() {
if let Some(rest) = line.strip_prefix("# sub-hex:") {
return decode_hex(rest.trim());
}
}
None
}
fn decode_hex(s: &str) -> Option<Vec<u8>> {
let clean: String = s.chars().filter(|c| !c.is_whitespace()).collect();
if clean.len() % 2 != 0 {
return None;
}
let mut out = Vec::with_capacity(clean.len() / 2);
for chunk in clean.as_bytes().chunks(2) {
let s = std::str::from_utf8(chunk).ok()?;
out.push(u8::from_str_radix(s, 16).ok()?);
}
Some(out)
}
fn build_packets(idx: &VobSubIdx, sub: &[u8]) -> VecDeque<Packet> {
let tb = TimeBase::new(1, 1_000_000);
let mut packets = VecDeque::new();
for (i, (start_us, filepos)) in idx.cues.iter().enumerate() {
let fp = *filepos as usize;
if fp >= sub.len() {
continue;
}
let spu = extract_spu(&sub[fp..]).unwrap_or_else(|| sub[fp..].to_vec());
let mut pkt = Packet::new(0, tb, spu);
pkt.pts = Some(*start_us);
pkt.dts = Some(*start_us);
pkt.flags.keyframe = true;
if i + 1 < idx.cues.len() {
let next = idx.cues[i + 1].0;
pkt.duration = Some((next - *start_us).max(0));
}
packets.push_back(pkt);
}
packets
}
fn extract_spu(buf: &[u8]) -> Option<Vec<u8>> {
if buf.len() >= 4 {
let spu_len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
if spu_len >= 4 && spu_len <= buf.len() {
return Some(buf[..spu_len].to_vec());
}
}
extract_spu_from_ps(buf)
}
fn extract_spu_from_ps(buf: &[u8]) -> Option<Vec<u8>> {
let mut cur = 0usize;
let mut spu: Vec<u8> = Vec::new();
let mut target: Option<usize> = None;
while cur + 4 <= buf.len() {
if buf[cur] != 0 || buf[cur + 1] != 0 || buf[cur + 2] != 1 {
return None;
}
let code = buf[cur + 3];
cur += 4;
match code {
0xBA => {
if cur >= buf.len() {
return None;
}
if (buf[cur] & 0xC0) == 0x40 {
if cur + 10 > buf.len() {
return None;
}
let stuffing = (buf[cur + 9] & 0x07) as usize;
cur += 10 + stuffing;
} else if (buf[cur] & 0xF0) == 0x20 {
cur += 8;
} else {
return None;
}
}
0xBB => {
if cur + 2 > buf.len() {
return None;
}
let len = u16::from_be_bytes([buf[cur], buf[cur + 1]]) as usize;
cur += 2 + len;
}
0xBD => {
if cur + 2 > buf.len() {
return None;
}
let pes_len = u16::from_be_bytes([buf[cur], buf[cur + 1]]) as usize;
cur += 2;
let pes_end = cur + pes_len;
if pes_end > buf.len() {
return None;
}
let body = parse_pes_body(&buf[cur..pes_end])?;
if body.is_empty() {
cur = pes_end;
continue;
}
let substream = body[0];
if !(0x20..=0x3F).contains(&substream) {
cur = pes_end;
continue;
}
spu.extend_from_slice(&body[1..]);
if target.is_none() && spu.len() >= 2 {
target = Some(u16::from_be_bytes([spu[0], spu[1]]) as usize);
}
if let Some(t) = target {
if spu.len() >= t {
spu.truncate(t);
return Some(spu);
}
}
cur = pes_end;
}
0xBE | 0xBF => {
if cur + 2 > buf.len() {
return None;
}
let len = u16::from_be_bytes([buf[cur], buf[cur + 1]]) as usize;
cur += 2 + len;
}
0xB9 => break,
_ => {
if cur + 2 > buf.len() {
return None;
}
let len = u16::from_be_bytes([buf[cur], buf[cur + 1]]) as usize;
cur += 2 + len;
}
}
}
None
}
fn parse_pes_body(pes: &[u8]) -> Option<&[u8]> {
if pes.is_empty() {
return None;
}
if (pes[0] & 0xC0) == 0x80 {
if pes.len() < 3 {
return None;
}
let hdr_len = pes[2] as usize;
let start = 3 + hdr_len;
if start > pes.len() {
return None;
}
return Some(&pes[start..]);
}
let mut i = 0usize;
while i < pes.len() && i < 16 && pes[i] == 0xFF {
i += 1;
}
if i >= pes.len() {
return None;
}
if (pes[i] & 0xC0) == 0x40 {
i += 2;
}
if i >= pes.len() {
return None;
}
let b = pes[i];
if (b & 0xF0) == 0x20 {
i += 5;
} else if (b & 0xF0) == 0x30 {
i += 10;
} else if b == 0x0F {
i += 1;
}
if i > pes.len() {
return None;
}
Some(&pes[i..])
}
struct VobSubDemuxer {
streams: [StreamInfo; 1],
packets: VecDeque<Packet>,
}
impl Demuxer for VobSubDemuxer {
fn format_name(&self) -> &str {
"vobsub"
}
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn next_packet(&mut self) -> Result<Packet> {
self.packets.pop_front().ok_or(Error::Eof)
}
fn duration_micros(&self) -> Option<i64> {
self.streams[0].duration
}
}
pub fn make_decoder(params: &CodecParameters) -> Result<Box<dyn Decoder>> {
let mut palette = [[0u8; 3]; 16];
if params.extradata.len() >= 48 {
for (i, p) in palette.iter_mut().enumerate() {
*p = [
params.extradata[i * 3],
params.extradata[i * 3 + 1],
params.extradata[i * 3 + 2],
];
}
} else {
for (i, p) in palette.iter_mut().enumerate() {
let g = (i * 17) as u8;
*p = [g, g, g];
}
}
Ok(Box::new(VobSubDecoder {
codec_id: CodecId::new(VOBSUB_CODEC_ID),
palette,
pending: VecDeque::new(),
eof: false,
}))
}
struct VobSubDecoder {
codec_id: CodecId,
palette: [[u8; 3]; 16],
pending: VecDeque<Frame>,
eof: bool,
}
impl Decoder for VobSubDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
let (spu, pixels, (w, h)) = parse_and_decode_spu(&packet.data)?;
let mut canvas = vec![0u8; (w as usize) * (h as usize) * 4];
for (i, &idx_4) in pixels.iter().enumerate() {
let which = idx_4 as usize & 0x03;
let pal_idx = spu.palette_sel[which] as usize & 0x0F;
let alpha4 = spu.alpha[which] & 0x0F;
let alpha = alpha4 * 17; if alpha == 0 {
continue;
}
let rgb = self.palette[pal_idx];
let dst = i * 4;
canvas[dst] = rgb[0];
canvas[dst + 1] = rgb[1];
canvas[dst + 2] = rgb[2];
canvas[dst + 3] = alpha;
}
let frame = VideoFrame {
pts: packet.pts,
planes: vec![VideoPlane {
stride: (w as usize) * 4,
data: canvas,
}],
};
self.pending.push_back(Frame::Video(frame));
Ok(())
}
fn receive_frame(&mut self) -> Result<Frame> {
if let Some(f) = self.pending.pop_front() {
return Ok(f);
}
if self.eof {
Err(Error::Eof)
} else {
Err(Error::NeedMore)
}
}
fn flush(&mut self) -> Result<()> {
self.eof = true;
Ok(())
}
fn reset(&mut self) -> Result<()> {
self.pending.clear();
self.eof = false;
Ok(())
}
}
#[doc(hidden)]
pub fn build_demo_spu(width: u16, height: u16, indices: &[u8]) -> Vec<u8> {
assert_eq!(indices.len(), (width as usize) * (height as usize));
fn push_rle_rows(
out: &mut Vec<u8>,
indices: &[u8],
width: u16,
_height: u16,
field_rows: impl Iterator<Item = usize>,
) {
let mut bits = NibbleWriter::new();
for row in field_rows {
let mut col = 0usize;
while col < width as usize {
let colour = indices[row * width as usize + col];
let mut run = 1usize;
while col + run < width as usize
&& indices[row * width as usize + col + run] == colour
&& run < 0x3FFF
{
run += 1;
}
let rest = col + run == width as usize;
emit_rle(&mut bits, if rest { 0 } else { run as u32 }, colour);
col += run;
}
bits.align();
}
bits.finish(out);
}
fn emit_rle(w: &mut NibbleWriter, count: u32, colour: u8) {
let c = (colour & 0x03) as u32;
if count == 0 {
w.write(4, 0);
w.write(4, 0);
w.write(4, 0);
w.write(4, c);
return;
}
if count < 4 {
let nib = ((count & 0x3) << 2) | c;
w.write(4, nib);
return;
}
if count < 16 {
let val = (count << 2) | c; w.write(4, (val >> 4) & 0xF);
w.write(4, val & 0xF);
return;
}
if count < 64 {
let val = (count << 2) | c; w.write(4, 0);
w.write(4, (val >> 4) & 0xF);
w.write(4, val & 0xF);
return;
}
let val = (count << 2) | c;
w.write(4, 0);
w.write(4, (val >> 12) & 0xF);
w.write(4, (val >> 8) & 0xF);
w.write(4, (val >> 4) & 0xF);
w.write(4, val & 0xF);
}
struct NibbleWriter {
nibbles: Vec<u8>,
}
impl NibbleWriter {
fn new() -> Self {
Self {
nibbles: Vec::new(),
}
}
fn write(&mut self, _bits: u32, value: u32) {
self.nibbles.push((value & 0x0F) as u8);
}
fn align(&mut self) {
if self.nibbles.len() % 2 != 0 {
self.nibbles.push(0);
}
}
fn finish(&self, out: &mut Vec<u8>) {
for pair in self.nibbles.chunks(2) {
let hi = pair[0];
let lo = if pair.len() == 2 { pair[1] } else { 0 };
out.push((hi << 4) | lo);
}
}
}
let mut top_bytes = Vec::new();
push_rle_rows(
&mut top_bytes,
indices,
width,
height,
(0..height as usize).step_by(2),
);
let mut bot_bytes = Vec::new();
push_rle_rows(
&mut bot_bytes,
indices,
width,
height,
(1..height as usize).step_by(2),
);
let top_off = 4usize;
let bot_off = top_off + top_bytes.len();
let ctrl_off = bot_off + bot_bytes.len();
let mut out = Vec::new();
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&(ctrl_off as u16).to_be_bytes());
out.extend_from_slice(&top_bytes);
out.extend_from_slice(&bot_bytes);
let ctrl_pos = out.len();
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&[0, 0]);
out.push(0x03); out.push(0x01); out.push(0x32); out.push(0x04); out.push(0x00); out.push(0xFF);
let last = out.len() - 2;
out[last] = 0x0F; out[last + 1] = 0xFF; out.push(0x05); out.push(0);
out.push((((width - 1) >> 8) as u8) & 0x0F);
out.push(((width - 1) & 0xFF) as u8);
out.push(0);
out.push((((height - 1) >> 8) as u8) & 0x0F);
out.push(((height - 1) & 0xFF) as u8);
out.push(0x06); out.extend_from_slice(&(top_off as u16).to_be_bytes());
out.extend_from_slice(&(bot_off as u16).to_be_bytes());
out.push(0x01); out.push(0xFF);
out[ctrl_pos + 2] = (ctrl_pos as u16 >> 8) as u8;
out[ctrl_pos + 3] = (ctrl_pos as u16 & 0xFF) as u8;
let total = out.len() as u16;
out[0] = (total >> 8) as u8;
out[1] = (total & 0xFF) as u8;
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_idx_basics() {
let text = "\
# VobSub index file
size: 720x480
palette: ff0000, 00ff00, 0000ff, ffffff, 000000, 808080, c0c0c0, 404040, 200020, 800080, a0a0a0, 010203, 040506, 070809, 0a0b0c, 0d0e0f
timestamp: 00:00:01:500, filepos: 000000000
timestamp: 00:00:03:000, filepos: 000000040
";
let idx = parse_idx(text).unwrap();
assert_eq!(idx.size, (720, 480));
assert!(idx.has_palette);
assert_eq!(idx.palette_rgb[0], [0xff, 0x00, 0x00]);
assert_eq!(idx.palette_rgb[1], [0x00, 0xff, 0x00]);
assert_eq!(idx.cues.len(), 2);
assert_eq!(idx.cues[0].0, 1_500_000);
assert_eq!(idx.cues[1].0, 3_000_000);
}
#[test]
fn decodes_small_spu() {
let indices = [1u8, 1, 1, 1];
let spu = build_demo_spu(2, 2, &indices);
let (state, pixels, (w, h)) = parse_and_decode_spu(&spu).unwrap();
assert_eq!(w, 2);
assert_eq!(h, 2);
assert_eq!(pixels, vec![1u8, 1, 1, 1]);
assert_eq!(state.palette_sel[1], 1);
let mut params = CodecParameters::video(CodecId::new(VOBSUB_CODEC_ID));
let mut extra = vec![0u8; 48];
extra[3] = 255;
params.extradata = extra;
let mut dec = make_decoder(¶ms).unwrap();
let pkt = Packet::new(0, TimeBase::new(1, 1_000_000), spu).with_pts(0);
dec.send_packet(&pkt).unwrap();
let frame = dec.receive_frame().unwrap();
let Frame::Video(v) = frame else {
panic!("expected video frame");
};
assert_eq!(v.planes[0].stride, 2 * 4);
assert_eq!(v.planes[0].data.len(), 2 * 2 * 4);
let data = &v.planes[0].data;
for px in data.chunks(4) {
assert_eq!(px, &[255, 0, 0, 255], "pixel not red: {:?}", px);
}
}
#[test]
fn extracts_spu_from_raw() {
let raw = build_demo_spu(2, 2, &[1u8, 1, 1, 1]);
let out = extract_spu(&raw).unwrap();
assert_eq!(out, raw);
}
#[test]
fn extracts_spu_from_mpeg_ps_wrap() {
let spu = build_demo_spu(2, 2, &[1u8, 1, 1, 1]);
let mut ps = Vec::new();
ps.extend_from_slice(&[0, 0, 1, 0xBA]);
ps.extend_from_slice(&[0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0xF8]);
let substream = 0x20u8;
let pes_payload_len = 3 + 1 + spu.len(); ps.extend_from_slice(&[0, 0, 1, 0xBD]);
ps.extend_from_slice(&(pes_payload_len as u16).to_be_bytes());
ps.extend_from_slice(&[0x80, 0x00, 0x00]);
ps.push(substream);
ps.extend_from_slice(&spu);
let out = extract_spu(&ps).unwrap();
assert_eq!(out, spu);
}
}