use crate::error::NetError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RtmpExtendedCodec {
Av1,
Vp9,
Hevc,
}
impl RtmpExtendedCodec {
#[must_use]
pub const fn fourcc(&self) -> [u8; 4] {
match self {
Self::Av1 => *b"av01",
Self::Vp9 => *b"vp09",
Self::Hevc => *b"hvc1",
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Av1 => "AV1",
Self::Vp9 => "VP9",
Self::Hevc => "HEVC",
}
}
#[must_use]
pub const fn is_patent_free(&self) -> bool {
matches!(self, Self::Av1 | Self::Vp9)
}
#[must_use]
pub fn from_fourcc(bytes: &[u8; 4]) -> Option<Self> {
match bytes {
b"av01" => Some(Self::Av1),
b"vp09" => Some(Self::Vp9),
b"hvc1" => Some(Self::Hevc),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum ExVideoPacketType {
SequenceStart = 0,
CodedFrames = 1,
SequenceEnd = 2,
CodedFramesX = 3,
Metadata = 4,
Mpeg2SequenceStart = 5,
}
impl ExVideoPacketType {
#[must_use]
pub const fn from_bits(bits: u8) -> Option<Self> {
match bits & 0x07 {
0 => Some(Self::SequenceStart),
1 => Some(Self::CodedFrames),
2 => Some(Self::SequenceEnd),
3 => Some(Self::CodedFramesX),
4 => Some(Self::Metadata),
5 => Some(Self::Mpeg2SequenceStart),
_ => None,
}
}
#[must_use]
pub const fn as_bits(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExVideoHeader {
pub is_ex_header: bool,
pub frame_type: u8,
pub codec: RtmpExtendedCodec,
pub packet_type: ExVideoPacketType,
}
impl ExVideoHeader {
#[must_use]
pub fn new(codec: RtmpExtendedCodec, frame_type: u8, packet_type: ExVideoPacketType) -> Self {
Self {
is_ex_header: true,
frame_type,
codec,
packet_type,
}
}
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(5);
let ft = (self.frame_type & 0x0F) << 4;
let ex = if self.is_ex_header { 0x08 } else { 0x00 };
let pt = self.packet_type.as_bits() & 0x07;
out.push(ft | ex | pt);
out.extend_from_slice(&self.codec.fourcc());
out
}
pub fn parse(data: &[u8]) -> Result<Self, NetError> {
if data.len() < 5 {
return Err(NetError::parse(
0,
format!(
"ExVideoHeader requires at least 5 bytes, got {}",
data.len()
),
));
}
let first = data[0];
let is_ex_header = (first & 0x08) != 0;
if !is_ex_header {
return Err(NetError::parse(
0,
"ExVideoHeader: is_ex_header bit not set — this is a legacy RTMP packet",
));
}
let frame_type = (first >> 4) & 0x0F;
let pkt_bits = first & 0x07;
let packet_type = ExVideoPacketType::from_bits(pkt_bits).ok_or_else(|| {
NetError::parse(
0,
format!("ExVideoHeader: unknown packet type bits {pkt_bits}"),
)
})?;
let fourcc: [u8; 4] = [data[1], data[2], data[3], data[4]];
let codec = RtmpExtendedCodec::from_fourcc(&fourcc).ok_or_else(|| {
NetError::parse(
1,
format!(
"ExVideoHeader: unrecognised FourCC {:?}",
std::str::from_utf8(&fourcc).unwrap_or("????")
),
)
})?;
Ok(Self {
is_ex_header,
frame_type,
codec,
packet_type,
})
}
#[must_use]
pub const fn is_keyframe(&self) -> bool {
self.frame_type == 1
}
}
#[derive(Debug, Clone)]
pub struct RtmpExtendedPacket {
pub header: ExVideoHeader,
pub payload: Vec<u8>,
}
impl RtmpExtendedPacket {
#[must_use]
pub fn new_av1_sequence(config_obu: Vec<u8>) -> Self {
Self {
header: ExVideoHeader::new(
RtmpExtendedCodec::Av1,
1, ExVideoPacketType::SequenceStart,
),
payload: config_obu,
}
}
#[must_use]
pub fn new_av1_frame(data: Vec<u8>, is_keyframe: bool) -> Self {
let frame_type = if is_keyframe { 1 } else { 2 };
Self {
header: ExVideoHeader::new(
RtmpExtendedCodec::Av1,
frame_type,
ExVideoPacketType::CodedFramesX, ),
payload: data,
}
}
#[must_use]
pub fn new_vp9_sequence(vp9_config: Vec<u8>) -> Self {
Self {
header: ExVideoHeader::new(RtmpExtendedCodec::Vp9, 1, ExVideoPacketType::SequenceStart),
payload: vp9_config,
}
}
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
let mut out = self.header.serialize();
if self.header.packet_type == ExVideoPacketType::CodedFrames {
out.push(0x00);
out.push(0x00);
out.push(0x00);
}
out.extend_from_slice(&self.payload);
out
}
pub fn parse(data: &[u8]) -> Result<Self, NetError> {
let header = ExVideoHeader::parse(data)?;
let payload_offset = if header.packet_type == ExVideoPacketType::CodedFrames {
if data.len() < 8 {
return Err(NetError::parse(
5,
format!(
"RtmpExtendedPacket CodedFrames requires 8+ bytes, got {}",
data.len()
),
));
}
8 } else {
5 };
let payload = data[payload_offset..].to_vec();
Ok(Self { header, payload })
}
#[must_use]
pub fn is_keyframe(&self) -> bool {
self.header.is_keyframe()
}
#[must_use]
pub fn is_sequence_start(&self) -> bool {
self.header.packet_type == ExVideoPacketType::SequenceStart
}
#[must_use]
pub fn codec(&self) -> RtmpExtendedCodec {
self.header.codec
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codec_fourcc() {
assert_eq!(&RtmpExtendedCodec::Av1.fourcc(), b"av01");
assert_eq!(&RtmpExtendedCodec::Vp9.fourcc(), b"vp09");
assert_eq!(&RtmpExtendedCodec::Hevc.fourcc(), b"hvc1");
}
#[test]
fn test_codec_names() {
assert_eq!(RtmpExtendedCodec::Av1.name(), "AV1");
assert_eq!(RtmpExtendedCodec::Vp9.name(), "VP9");
assert_eq!(RtmpExtendedCodec::Hevc.name(), "HEVC");
}
#[test]
fn test_codec_patent_free() {
assert!(RtmpExtendedCodec::Av1.is_patent_free());
assert!(RtmpExtendedCodec::Vp9.is_patent_free());
assert!(!RtmpExtendedCodec::Hevc.is_patent_free());
}
#[test]
fn test_codec_from_fourcc() {
assert_eq!(
RtmpExtendedCodec::from_fourcc(b"av01"),
Some(RtmpExtendedCodec::Av1)
);
assert_eq!(
RtmpExtendedCodec::from_fourcc(b"vp09"),
Some(RtmpExtendedCodec::Vp9)
);
assert_eq!(RtmpExtendedCodec::from_fourcc(b"avc1"), None);
}
#[test]
fn test_packet_type_bits_roundtrip() {
for (bits, expected) in [
(0u8, ExVideoPacketType::SequenceStart),
(1, ExVideoPacketType::CodedFrames),
(2, ExVideoPacketType::SequenceEnd),
(3, ExVideoPacketType::CodedFramesX),
(4, ExVideoPacketType::Metadata),
(5, ExVideoPacketType::Mpeg2SequenceStart),
] {
let decoded = ExVideoPacketType::from_bits(bits).expect("should decode");
assert_eq!(decoded, expected);
assert_eq!(decoded.as_bits(), bits);
}
}
#[test]
fn test_packet_type_unknown() {
assert!(ExVideoPacketType::from_bits(6).is_none());
assert!(ExVideoPacketType::from_bits(7).is_none());
}
#[test]
fn test_header_new_sets_ex_bit() {
let h = ExVideoHeader::new(RtmpExtendedCodec::Av1, 1, ExVideoPacketType::SequenceStart);
assert!(h.is_ex_header);
assert!(h.is_keyframe());
}
#[test]
fn test_header_serialize_av1_sequence() {
let h = ExVideoHeader::new(
RtmpExtendedCodec::Av1,
1, ExVideoPacketType::SequenceStart,
);
let bytes = h.serialize();
assert_eq!(bytes.len(), 5);
assert_eq!(bytes[0], 0x18, "first byte should be 0x18");
assert_eq!(&bytes[1..5], b"av01");
}
#[test]
fn test_header_roundtrip_vp9_inter() {
let original = ExVideoHeader::new(
RtmpExtendedCodec::Vp9,
2, ExVideoPacketType::CodedFramesX,
);
let bytes = original.serialize();
let decoded = ExVideoHeader::parse(&bytes).expect("should parse");
assert_eq!(decoded.codec, RtmpExtendedCodec::Vp9);
assert_eq!(decoded.frame_type, 2);
assert_eq!(decoded.packet_type, ExVideoPacketType::CodedFramesX);
assert!(decoded.is_ex_header);
}
#[test]
fn test_header_parse_rejects_legacy() {
let data = [0x10u8, b'a', b'v', b'0', b'1'];
let result = ExVideoHeader::parse(&data);
assert!(result.is_err());
}
#[test]
fn test_header_parse_too_short() {
let data = [0x18u8, b'a', b'v']; let result = ExVideoHeader::parse(&data);
assert!(result.is_err());
}
#[test]
fn test_header_parse_unknown_fourcc() {
let data = [0x18u8, b'x', b'y', b'z', b'w']; let result = ExVideoHeader::parse(&data);
assert!(result.is_err());
}
#[test]
fn test_packet_av1_sequence() {
let obu = vec![0x81, 0x00, 0x0C, 0x00];
let pkt = RtmpExtendedPacket::new_av1_sequence(obu.clone());
assert!(pkt.is_sequence_start());
assert!(pkt.is_keyframe());
assert_eq!(pkt.codec(), RtmpExtendedCodec::Av1);
assert_eq!(pkt.payload, obu);
}
#[test]
fn test_packet_av1_keyframe() {
let data = vec![0x12, 0x34, 0x56];
let pkt = RtmpExtendedPacket::new_av1_frame(data.clone(), true);
assert!(pkt.is_keyframe());
assert!(!pkt.is_sequence_start());
assert_eq!(pkt.header.packet_type, ExVideoPacketType::CodedFramesX);
}
#[test]
fn test_packet_av1_interframe() {
let pkt = RtmpExtendedPacket::new_av1_frame(vec![0xAB], false);
assert!(!pkt.is_keyframe());
assert_eq!(pkt.header.frame_type, 2);
}
#[test]
fn test_packet_vp9_sequence() {
let cfg = vec![0x00, 0x63, 0x00, 0x01];
let pkt = RtmpExtendedPacket::new_vp9_sequence(cfg.clone());
assert!(pkt.is_sequence_start());
assert_eq!(pkt.codec(), RtmpExtendedCodec::Vp9);
assert_eq!(pkt.payload, cfg);
}
#[test]
fn test_packet_roundtrip_av1_sequence() {
let obu = vec![0x81, 0x00, 0x0C];
let original = RtmpExtendedPacket::new_av1_sequence(obu.clone());
let bytes = original.serialize();
let decoded = RtmpExtendedPacket::parse(&bytes).expect("should parse");
assert_eq!(decoded.header.codec, RtmpExtendedCodec::Av1);
assert_eq!(decoded.header.packet_type, ExVideoPacketType::SequenceStart);
assert_eq!(decoded.payload, obu);
}
#[test]
fn test_packet_roundtrip_av1_coded_frames_x() {
let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
let original = RtmpExtendedPacket::new_av1_frame(data.clone(), true);
let bytes = original.serialize();
let decoded = RtmpExtendedPacket::parse(&bytes).expect("should parse");
assert_eq!(decoded.header.packet_type, ExVideoPacketType::CodedFramesX);
assert_eq!(decoded.payload, data);
}
#[test]
fn test_packet_coded_frames_composition_time_bytes() {
let pkt = RtmpExtendedPacket {
header: ExVideoHeader::new(RtmpExtendedCodec::Vp9, 1, ExVideoPacketType::CodedFrames),
payload: vec![0xAA, 0xBB],
};
let bytes = pkt.serialize();
assert_eq!(bytes.len(), 10);
assert_eq!(bytes[5], 0x00);
assert_eq!(bytes[6], 0x00);
assert_eq!(bytes[7], 0x00);
}
#[test]
fn test_packet_coded_frames_parse_skips_comp_time() {
let pkt = RtmpExtendedPacket {
header: ExVideoHeader::new(RtmpExtendedCodec::Av1, 2, ExVideoPacketType::CodedFrames),
payload: vec![0x11, 0x22, 0x33],
};
let bytes = pkt.serialize();
let decoded = RtmpExtendedPacket::parse(&bytes).expect("should parse");
assert_eq!(decoded.payload, vec![0x11, 0x22, 0x33]);
}
#[test]
fn test_packet_parse_coded_frames_too_short() {
let data = [0x19u8, b'a', b'v', b'0', b'1', 0x00, 0x01]; let result = RtmpExtendedPacket::parse(&data);
assert!(result.is_err());
}
#[test]
fn test_packet_hevc_sequence_roundtrip() {
let cfg = vec![0x01, 0x64, 0x00, 0x1F];
let pkt = RtmpExtendedPacket {
header: ExVideoHeader::new(
RtmpExtendedCodec::Hevc,
1,
ExVideoPacketType::SequenceStart,
),
payload: cfg.clone(),
};
let bytes = pkt.serialize();
let decoded = RtmpExtendedPacket::parse(&bytes).expect("should parse");
assert_eq!(decoded.codec(), RtmpExtendedCodec::Hevc);
assert_eq!(decoded.payload, cfg);
}
}