use std::collections::{HashMap, VecDeque};
use oxideav_core::Decoder;
use oxideav_core::{
CodecId, CodecParameters, Error, Frame, Packet, Result, VideoFrame, VideoPlane,
};
use crate::DVBSUB_CODEC_ID;
pub const SEG_PAGE_COMPOSITION: u8 = 0x10;
pub const SEG_REGION_COMPOSITION: u8 = 0x11;
pub const SEG_CLUT_DEFINITION: u8 = 0x12;
pub const SEG_OBJECT_DATA: u8 = 0x13;
pub const SEG_DISPLAY_DEFINITION: u8 = 0x14;
pub const SEG_END_OF_DISPLAY_SET: u8 = 0x80;
#[derive(Clone, Debug)]
pub struct RawSegment {
pub seg_type: u8,
pub page_id: u16,
pub body: Vec<u8>,
}
pub fn read_segment(buf: &[u8], pos: usize) -> Result<(RawSegment, usize)> {
if pos + 6 > buf.len() {
return Err(Error::NeedMore);
}
if buf[pos] != 0x0F {
return Err(Error::invalid(format!(
"DVB sub: segment sync byte 0x0F expected, got 0x{:02X}",
buf[pos]
)));
}
let seg_type = buf[pos + 1];
let page_id = u16::from_be_bytes([buf[pos + 2], buf[pos + 3]]);
let len = u16::from_be_bytes([buf[pos + 4], buf[pos + 5]]) as usize;
let end = pos + 6 + len;
if end > buf.len() {
return Err(Error::NeedMore);
}
Ok((
RawSegment {
seg_type,
page_id,
body: buf[pos + 6..end].to_vec(),
},
end,
))
}
#[derive(Clone, Debug)]
struct DisplayDefinition {
width: u16,
height: u16,
}
impl Default for DisplayDefinition {
fn default() -> Self {
Self {
width: 720,
height: 576,
}
}
}
fn parse_display_definition(body: &[u8]) -> Result<DisplayDefinition> {
if body.len() < 5 {
return Err(Error::invalid("DVB DDS: body too short"));
}
let width = u16::from_be_bytes([body[1], body[2]]).wrapping_add(1);
let height = u16::from_be_bytes([body[3], body[4]]).wrapping_add(1);
Ok(DisplayDefinition { width, height })
}
#[derive(Clone, Debug)]
struct PageRegion {
region_id: u8,
x: u16,
y: u16,
}
fn parse_page_composition(body: &[u8]) -> Result<Vec<PageRegion>> {
if body.len() < 2 {
return Err(Error::invalid("DVB page_composition: body too short"));
}
let mut cur = 2;
let mut regions = Vec::new();
while cur + 6 <= body.len() {
let region_id = body[cur];
let x = u16::from_be_bytes([body[cur + 2], body[cur + 3]]);
let y = u16::from_be_bytes([body[cur + 4], body[cur + 5]]);
cur += 6;
regions.push(PageRegion { region_id, x, y });
}
Ok(regions)
}
#[derive(Clone, Debug)]
struct Region {
#[allow(dead_code)]
width: u16,
#[allow(dead_code)]
height: u16,
#[allow(dead_code)]
depth_bits: u8,
clut_id: u8,
objects: Vec<RegionObject>,
}
#[derive(Clone, Debug)]
struct RegionObject {
object_id: u16,
x: u16,
y: u16,
}
fn parse_region_composition(body: &[u8]) -> Result<(u8, Region)> {
if body.len() < 10 {
return Err(Error::invalid("DVB region_composition: body too short"));
}
let region_id = body[0];
let width = u16::from_be_bytes([body[2], body[3]]);
let height = u16::from_be_bytes([body[4], body[5]]);
let region_depth = (body[6] >> 2) & 0x07;
let depth_bits = match region_depth {
1 => 2,
2 => 4,
3 => 8,
_ => 4, };
let clut_id = body[7];
let mut cur = 10;
let mut objects = Vec::new();
while cur + 6 <= body.len() {
let obj_hi = body[cur];
let obj_lo = body[cur + 1];
let object_id = u16::from_be_bytes([obj_hi, obj_lo]);
let obj_type = (body[cur + 2] >> 6) & 0x03;
let x = u16::from_be_bytes([body[cur + 2] & 0x3F, body[cur + 3]]);
let y = u16::from_be_bytes([body[cur + 4] & 0x0F, body[cur + 5]]);
cur += 6;
if obj_type == 0x01 || obj_type == 0x02 {
if cur + 2 <= body.len() {
cur += 2;
}
}
objects.push(RegionObject { object_id, x, y });
}
Ok((
region_id,
Region {
width,
height,
depth_bits,
clut_id,
objects,
},
))
}
#[derive(Clone, Debug)]
struct Clut {
entries: [[u8; 4]; 256],
}
impl Default for Clut {
fn default() -> Self {
Self {
entries: [[0u8; 4]; 256],
}
}
}
fn ycbcr_to_rgba(y: u8, cr: u8, cb: u8, t8: u8) -> [u8; 4] {
let y = y as i32;
let cb = cb as i32 - 128;
let cr = cr as i32 - 128;
let r = y + ((91881 * cr) >> 16);
let g = y - ((22554 * cb + 46802 * cr) >> 16);
let b = y + ((116130 * cb) >> 16);
let alpha = 255u8.saturating_sub(t8);
[
r.clamp(0, 255) as u8,
g.clamp(0, 255) as u8,
b.clamp(0, 255) as u8,
alpha,
]
}
fn parse_clut_into(body: &[u8], cluts: &mut HashMap<u8, Clut>) -> Result<()> {
if body.len() < 2 {
return Err(Error::invalid("DVB CLUT: body too short"));
}
let clut_id = body[0];
let entry = cluts.entry(clut_id).or_default();
let mut cur = 2;
while cur < body.len() {
if cur + 2 > body.len() {
break;
}
let entry_id = body[cur];
let flags = body[cur + 1];
cur += 2;
let full = (flags & 0x01) != 0;
if full {
if cur + 4 > body.len() {
return Err(Error::invalid("DVB CLUT: truncated full entry"));
}
let y = body[cur];
let cr = body[cur + 1];
let cb = body[cur + 2];
let t = body[cur + 3];
cur += 4;
entry.entries[entry_id as usize] = ycbcr_to_rgba(y, cr, cb, t);
} else {
if cur + 2 > body.len() {
return Err(Error::invalid("DVB CLUT: truncated short entry"));
}
let b0 = body[cur];
let b1 = body[cur + 1];
cur += 2;
let y = b0 & 0xFC;
let cr = (((b0 & 0x03) << 2) | (b1 >> 6)) << 4;
let cb = ((b1 >> 2) & 0x0F) << 4;
let t = (b1 & 0x03) << 6;
entry.entries[entry_id as usize] = ycbcr_to_rgba(y, cr, cb, t);
}
}
Ok(())
}
#[derive(Clone, Debug)]
struct Object {
rows: Vec<Vec<u8>>,
#[allow(dead_code)]
coding_method: u8,
}
fn parse_object_data(body: &[u8]) -> Result<(u16, Object)> {
if body.len() < 3 {
return Err(Error::invalid("DVB object_data: body too short"));
}
let object_id = u16::from_be_bytes([body[0], body[1]]);
let coding_method = (body[2] >> 2) & 0x03;
if coding_method != 0 {
return Err(Error::unsupported(format!(
"DVB sub: coding_method {} (character/text objects)",
coding_method
)));
}
if body.len() < 7 {
return Err(Error::invalid(
"DVB object_data: pixel-coded header truncated",
));
}
let top_len = u16::from_be_bytes([body[3], body[4]]) as usize;
let bot_len = u16::from_be_bytes([body[5], body[6]]) as usize;
let top_start = 7;
let top_end = top_start + top_len;
let bot_end = top_end + bot_len;
if bot_end > body.len() {
return Err(Error::invalid(
"DVB object_data: pixel-coded line blocks truncated",
));
}
let top_rows = parse_pixel_lines(&body[top_start..top_end])?;
let bot_rows = if bot_len > 0 {
parse_pixel_lines(&body[top_end..bot_end])?
} else {
top_rows.clone()
};
let mut rows = Vec::with_capacity(top_rows.len() + bot_rows.len());
let n = top_rows.len().max(bot_rows.len());
for i in 0..n {
if i < top_rows.len() {
rows.push(top_rows[i].clone());
}
if i < bot_rows.len() {
rows.push(bot_rows[i].clone());
}
}
Ok((
object_id,
Object {
rows,
coding_method,
},
))
}
fn parse_pixel_lines(buf: &[u8]) -> Result<Vec<Vec<u8>>> {
let mut rows: Vec<Vec<u8>> = Vec::new();
let mut row: Vec<u8> = Vec::new();
let mut i = 0;
while i < buf.len() {
let code = buf[i];
i += 1;
match code {
0x10 => {
let (consumed, pixels) = decode_2bit_string(&buf[i..])?;
i += consumed;
row.extend_from_slice(&pixels);
}
0x11 => {
let (consumed, pixels) = decode_4bit_string(&buf[i..])?;
i += consumed;
row.extend_from_slice(&pixels);
}
0x12 => {
let (consumed, pixels) = decode_8bit_string(&buf[i..])?;
i += consumed;
row.extend_from_slice(&pixels);
}
0x20..=0x22 => {
if i + 2 > buf.len() {
return Err(Error::invalid("DVB: map table truncated"));
}
i += 2;
}
0xF0 => {
rows.push(std::mem::take(&mut row));
}
_ => {
return Err(Error::invalid(format!(
"DVB: unknown pixel-line data_type 0x{:02X}",
code
)));
}
}
}
if !row.is_empty() {
rows.push(row);
}
Ok(rows)
}
fn decode_2bit_string(buf: &[u8]) -> Result<(usize, Vec<u8>)> {
let mut bits = BitReader::new(buf);
let mut pixels = Vec::new();
loop {
let code = bits.read(2)?;
if code != 0 {
pixels.push(code as u8);
continue;
}
let b1 = bits.read(1)?;
if b1 == 1 {
let run = bits.read(3)? as usize + 3;
let col = bits.read(2)? as u8;
for _ in 0..run {
pixels.push(col);
}
continue;
}
let b2 = bits.read(1)?;
if b2 == 1 {
let run = bits.read(4)? as usize + 12;
pixels.extend(std::iter::repeat(0_u8).take(run));
continue;
}
let b3 = bits.read(2)?;
match b3 {
0x00 => {
bits.align();
break;
}
0x01 => pixels.push(0), 0x02 => {
let run = bits.read(3)? as usize + 3;
let col = bits.read(2)? as u8;
for _ in 0..run {
pixels.push(col);
}
}
0x03 => {
let run = bits.read(8)? as usize + 29;
let col = bits.read(2)? as u8;
for _ in 0..run {
pixels.push(col);
}
}
_ => unreachable!(),
}
}
Ok((bits.consumed_bytes(), pixels))
}
fn decode_4bit_string(buf: &[u8]) -> Result<(usize, Vec<u8>)> {
let mut bits = BitReader::new(buf);
let mut pixels = Vec::new();
loop {
let code = bits.read(4)?;
if code != 0 {
pixels.push(code as u8);
continue;
}
let b1 = bits.read(1)?;
if b1 == 0 {
let run_hdr = bits.read(3)?;
if run_hdr == 0 {
bits.align();
break;
}
let run = run_hdr as usize + 2;
pixels.extend(std::iter::repeat(0_u8).take(run));
continue;
}
let b2 = bits.read(1)?;
if b2 == 0 {
let run = (bits.read(2)? as usize) + 4;
let col = bits.read(4)? as u8;
for _ in 0..run {
pixels.push(col);
}
continue;
}
let b3 = bits.read(2)?;
match b3 {
0x00 => pixels.push(0),
0x01 => {
pixels.push(0);
pixels.push(0);
}
0x02 => {
let run = (bits.read(4)? as usize) + 9;
let col = bits.read(4)? as u8;
for _ in 0..run {
pixels.push(col);
}
}
0x03 => {
let run = (bits.read(8)? as usize) + 25;
let col = bits.read(4)? as u8;
for _ in 0..run {
pixels.push(col);
}
}
_ => unreachable!(),
}
}
Ok((bits.consumed_bytes(), pixels))
}
fn decode_8bit_string(buf: &[u8]) -> Result<(usize, Vec<u8>)> {
let mut i = 0usize;
let mut pixels = Vec::new();
while i < buf.len() {
let b = buf[i];
i += 1;
if b != 0 {
pixels.push(b);
continue;
}
if i >= buf.len() {
return Err(Error::invalid("DVB 8-bit: escape truncated"));
}
let b1 = buf[i];
i += 1;
if b1 == 0 {
break;
}
let run_flag = (b1 & 0x80) != 0;
let count = (b1 & 0x7F) as usize;
if !run_flag {
if count == 0 {
continue;
}
pixels.extend(std::iter::repeat(0_u8).take(count));
} else {
if i >= buf.len() {
return Err(Error::invalid("DVB 8-bit: run-byte truncated"));
}
let col = buf[i];
i += 1;
for _ in 0..count {
pixels.push(col);
}
}
}
Ok((i, pixels))
}
struct BitReader<'a> {
buf: &'a [u8],
bit_pos: usize,
}
impl<'a> BitReader<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf, bit_pos: 0 }
}
fn read(&mut self, n: u32) -> Result<u32> {
let mut out = 0u32;
for _ in 0..n {
if self.bit_pos >= self.buf.len() * 8 {
return Err(Error::invalid("DVB bit reader: ran out of data"));
}
let byte = self.buf[self.bit_pos / 8];
let bit = (byte >> (7 - (self.bit_pos % 8))) & 1;
out = (out << 1) | (bit as u32);
self.bit_pos += 1;
}
Ok(out)
}
fn align(&mut self) {
self.bit_pos = self.bit_pos.div_ceil(8) * 8;
}
fn consumed_bytes(&self) -> usize {
self.bit_pos.div_ceil(8)
}
}
pub fn make_decoder(_params: &CodecParameters) -> Result<Box<dyn Decoder>> {
Ok(Box::new(DvbSubDecoder {
codec_id: CodecId::new(DVBSUB_CODEC_ID),
pending: VecDeque::new(),
eof: false,
}))
}
struct DvbSubDecoder {
codec_id: CodecId,
pending: VecDeque<Frame>,
eof: bool,
}
impl Decoder for DvbSubDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
let payload = strip_pes_prefix(&packet.data);
let mut dds = DisplayDefinition::default();
let mut regions: HashMap<u8, Region> = HashMap::new();
let mut objects: HashMap<u16, Object> = HashMap::new();
let mut cluts: HashMap<u8, Clut> = HashMap::new();
let mut page: Vec<PageRegion> = Vec::new();
let mut cur = 0;
while cur < payload.len() {
let (seg, next) = match read_segment(payload, cur) {
Ok(x) => x,
Err(Error::NeedMore) => break,
Err(e) => return Err(e),
};
match seg.seg_type {
SEG_DISPLAY_DEFINITION => {
dds = parse_display_definition(&seg.body)?;
}
SEG_PAGE_COMPOSITION => {
page = parse_page_composition(&seg.body)?;
}
SEG_REGION_COMPOSITION => {
let (id, region) = parse_region_composition(&seg.body)?;
regions.insert(id, region);
}
SEG_CLUT_DEFINITION => {
parse_clut_into(&seg.body, &mut cluts)?;
}
SEG_OBJECT_DATA => {
let (id, obj) = parse_object_data(&seg.body)?;
objects.insert(id, obj);
}
SEG_END_OF_DISPLAY_SET => {
break;
}
_ => {}
}
cur = next;
}
let width = dds.width as usize;
let height = dds.height as usize;
if width == 0 || height == 0 {
return Err(Error::invalid("DVB sub: zero canvas"));
}
let mut canvas = vec![0u8; width * height * 4];
for pr in &page {
let Some(region) = regions.get(&pr.region_id) else {
continue;
};
let clut = cluts.get(®ion.clut_id).cloned().unwrap_or_default();
for ro in ®ion.objects {
let Some(obj) = objects.get(&ro.object_id) else {
continue;
};
let base_x = pr.x as usize + ro.x as usize;
let base_y = pr.y as usize + ro.y as usize;
crate::composite::blit_indexed(
&mut canvas,
width,
height,
&obj.rows,
base_x,
base_y,
|px| {
if px == 0 {
[0, 0, 0, 0]
} else {
clut.entries[px as usize]
}
},
);
}
}
let frame = VideoFrame {
pts: packet.pts,
planes: vec![VideoPlane {
stride: width * 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(())
}
}
fn strip_pes_prefix(buf: &[u8]) -> &[u8] {
if buf.len() >= 2 && buf[0] == 0x20 && buf[1] == 0x00 {
&buf[2..]
} else {
buf
}
}
#[doc(hidden)]
pub fn build_demo_pes(canvas: (u16, u16), pixels: &[u8], width: usize, height: usize) -> Vec<u8> {
assert_eq!(pixels.len(), width * height);
fn segment(out: &mut Vec<u8>, seg_type: u8, body: &[u8]) {
out.push(0x0F);
out.push(seg_type);
out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&(body.len() as u16).to_be_bytes());
out.extend_from_slice(body);
}
let mut out = vec![0x20, 0x00];
let mut dds = Vec::new();
dds.push(0); dds.extend_from_slice(&(canvas.0.saturating_sub(1)).to_be_bytes());
dds.extend_from_slice(&(canvas.1.saturating_sub(1)).to_be_bytes());
segment(&mut out, SEG_DISPLAY_DEFINITION, &dds);
let mut page = vec![
0, 0, 0, 0xFF, ];
page.extend_from_slice(&0u16.to_be_bytes()); page.extend_from_slice(&0u16.to_be_bytes()); segment(&mut out, SEG_PAGE_COMPOSITION, &page);
let mut region = Vec::new();
region.push(0); region.push(0); region.extend_from_slice(&(width as u16).to_be_bytes());
region.extend_from_slice(&(height as u16).to_be_bytes());
region.push(3 << 2); region.push(0); region.push(0); region.push(0); region.extend_from_slice(&0u16.to_be_bytes()); region.extend_from_slice(&0u16.to_be_bytes()); region.extend_from_slice(&0u16.to_be_bytes()); segment(&mut out, SEG_REGION_COMPOSITION, ®ion);
let mut clut = vec![
0, 0, 1, 0xFF, 255, 128, 128, 0, ];
clut.push(2);
clut.push(0xFF);
clut.push(81); clut.push(240); clut.push(90); clut.push(0);
segment(&mut out, SEG_CLUT_DEFINITION, &clut);
let mut obj = Vec::new();
obj.extend_from_slice(&0u16.to_be_bytes()); obj.push(0);
fn encode_rows_8bit(rows: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
for row in rows {
out.push(0x12); for &p in row {
if p == 0 {
out.push(0x00);
out.push(0x01); continue;
}
out.push(p);
}
out.push(0x00);
out.push(0x00); out.push(0xF0); }
out
}
let mut top: Vec<Vec<u8>> = Vec::new();
let mut bot: Vec<Vec<u8>> = Vec::new();
for (i, row) in pixels.chunks(width).enumerate() {
if i % 2 == 0 {
top.push(row.to_vec());
} else {
bot.push(row.to_vec());
}
}
let top_bytes = encode_rows_8bit(&top);
let bot_bytes = encode_rows_8bit(&bot);
obj.extend_from_slice(&(top_bytes.len() as u16).to_be_bytes());
obj.extend_from_slice(&(bot_bytes.len() as u16).to_be_bytes());
obj.extend_from_slice(&top_bytes);
obj.extend_from_slice(&bot_bytes);
segment(&mut out, SEG_OBJECT_DATA, &obj);
segment(&mut out, SEG_END_OF_DISPLAY_SET, &[]);
out
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::TimeBase;
#[test]
fn decodes_pixel_coded_bitmap() {
let pixels = [1u8, 2, 2, 1];
let pes = build_demo_pes((2, 2), &pixels, 2, 2);
let params = CodecParameters::video(CodecId::new(DVBSUB_CODEC_ID));
let mut dec = make_decoder(¶ms).unwrap();
let pkt = Packet::new(0, TimeBase::new(1, 90_000), pes).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 r0c0 = &v.planes[0].data[0..4];
let r0c1 = &v.planes[0].data[4..8];
assert!(
r0c0[0] > 200 && r0c0[1] > 200 && r0c0[2] > 200,
"expected white, got {:?}",
r0c0
);
assert!(
r0c1[0] > r0c1[1] && r0c1[0] > r0c1[2],
"expected red-dominant, got {:?}",
r0c1
);
}
#[test]
fn rejects_character_coded_objects() {
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes()); body.push(0b0000_0100); body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
let err = parse_object_data(&body).unwrap_err();
match err {
Error::Unsupported(_) => {}
other => panic!("expected Unsupported, got {other:?}"),
}
}
}