use thiserror::Error;
use tokio::io::AsyncWriteExt;
use crate::framebus::FrameFormat;
pub const TAP_CONTRACT_VERSION: u32 = 2;
pub const TAP_MAGIC: u32 = 0x4144_5654;
pub const TAP_HEADER_LEN: usize = 24;
pub const TAP_MAX_FRAME: usize = 64 * 1024 * 1024;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum TapHeaderError {
#[error("bad tap frame magic {0:#x}")]
BadMagic(u32),
#[error("bad tap format tag {0}")]
BadFormatTag(u8),
#[error("tap frame of {0} bytes exceeds the {TAP_MAX_FRAME}-byte cap")]
FrameTooLarge(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TapHeader {
pub format: FrameFormat,
pub width: u32,
pub height: u32,
pub byte_len: usize,
pub ts_mono_ns: u64,
}
pub fn format_tag(f: FrameFormat) -> u8 {
match f {
FrameFormat::Rgb24 => 0,
FrameFormat::Nv12 => 1,
FrameFormat::Yuv420p => 2,
FrameFormat::Gray8 => 3,
FrameFormat::Gray16 => 4,
FrameFormat::BayerRggb8 => 5,
FrameFormat::BayerRggb16 => 6,
}
}
pub fn format_from_tag(t: u8) -> Option<FrameFormat> {
match t {
0 => Some(FrameFormat::Rgb24),
1 => Some(FrameFormat::Nv12),
2 => Some(FrameFormat::Yuv420p),
3 => Some(FrameFormat::Gray8),
4 => Some(FrameFormat::Gray16),
5 => Some(FrameFormat::BayerRggb8),
6 => Some(FrameFormat::BayerRggb16),
_ => None,
}
}
pub fn encode_tap_header(
format: FrameFormat,
width: u32,
height: u32,
byte_len: u32,
ts_mono_ns: u64,
) -> [u8; TAP_HEADER_LEN] {
let mut h = [0u8; TAP_HEADER_LEN];
h[0..4].copy_from_slice(&TAP_MAGIC.to_le_bytes());
h[4] = format_tag(format);
h[6..8].copy_from_slice(&(width as u16).to_le_bytes());
h[8..10].copy_from_slice(&(height as u16).to_le_bytes());
h[12..16].copy_from_slice(&byte_len.to_le_bytes());
h[16..24].copy_from_slice(&ts_mono_ns.to_le_bytes());
h
}
pub fn decode_tap_header(h: &[u8; TAP_HEADER_LEN]) -> Result<TapHeader, TapHeaderError> {
let magic = u32::from_le_bytes(h[0..4].try_into().unwrap());
if magic != TAP_MAGIC {
return Err(TapHeaderError::BadMagic(magic));
}
let format = format_from_tag(h[4]).ok_or(TapHeaderError::BadFormatTag(h[4]))?;
let width = u16::from_le_bytes(h[6..8].try_into().unwrap()) as u32;
let height = u16::from_le_bytes(h[8..10].try_into().unwrap()) as u32;
let byte_len = u32::from_le_bytes(h[12..16].try_into().unwrap()) as usize;
if byte_len > TAP_MAX_FRAME {
return Err(TapHeaderError::FrameTooLarge(byte_len));
}
let ts_mono_ns = u64::from_le_bytes(h[16..24].try_into().unwrap());
Ok(TapHeader {
format,
width,
height,
byte_len,
ts_mono_ns,
})
}
pub async fn write_tap_frame<W>(
w: &mut W,
format: FrameFormat,
width: u32,
height: u32,
pixels: &[u8],
ts_mono_ns: u64,
) -> std::io::Result<()>
where
W: AsyncWriteExt + Unpin,
{
let header = encode_tap_header(format, width, height, pixels.len() as u32, ts_mono_ns);
w.write_all(&header).await?;
if !pixels.is_empty() {
w.write_all(pixels).await?;
}
w.flush().await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_matches_registry() {
assert_eq!(
TAP_CONTRACT_VERSION as u16,
crate::contracts::contract_version("tap.frame").unwrap()
);
}
#[test]
fn header_round_trips() {
for (fmt, w, h, ts) in [
(FrameFormat::Rgb24, 640u32, 480u32, 0u64),
(FrameFormat::Nv12, 1280, 720, 1),
(FrameFormat::Yuv420p, 1920, 1080, u64::MAX),
(FrameFormat::Gray8, 640, 480, 123_456_789_012),
(FrameFormat::Gray16, 640, 480, 7),
(FrameFormat::BayerRggb8, 640, 480, 8),
(FrameFormat::BayerRggb16, 640, 480, 9),
] {
let len = fmt.frame_bytes(w, h);
let hdr = encode_tap_header(fmt, w, h, len as u32, ts);
let d = decode_tap_header(&hdr).unwrap();
assert_eq!(
d,
TapHeader {
format: fmt,
width: w,
height: h,
byte_len: len,
ts_mono_ns: ts,
}
);
}
}
#[test]
fn header_layout_is_pinned() {
let hdr = encode_tap_header(FrameFormat::Rgb24, 8, 4, 96, 0x0102_0304_0506_0708);
assert_eq!(&hdr[0..4], b"TVDA"); assert_eq!(hdr[4], 0); assert_eq!(u16::from_le_bytes([hdr[6], hdr[7]]), 8);
assert_eq!(u16::from_le_bytes([hdr[8], hdr[9]]), 4);
assert_eq!(u32::from_le_bytes([hdr[12], hdr[13], hdr[14], hdr[15]]), 96);
assert_eq!(
u64::from_le_bytes(hdr[16..24].try_into().unwrap()),
0x0102_0304_0506_0708
);
}
#[test]
fn decode_rejects_bad_magic_tag_and_oversize() {
let mut hdr = encode_tap_header(FrameFormat::Rgb24, 8, 4, 96, 0);
let good = hdr;
hdr[0] ^= 0xFF;
assert!(matches!(
decode_tap_header(&hdr),
Err(TapHeaderError::BadMagic(_))
));
let mut bad_tag = good;
bad_tag[4] = 9;
assert_eq!(
decode_tap_header(&bad_tag),
Err(TapHeaderError::BadFormatTag(9))
);
let mut big = good;
big[12..16].copy_from_slice(&((TAP_MAX_FRAME as u32) + 1).to_le_bytes());
assert!(matches!(
decode_tap_header(&big),
Err(TapHeaderError::FrameTooLarge(_))
));
}
}