use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use broadcast_common::{Package, Unpackage};
use crate::flv::{FlvDemux, FlvError, FlvMux};
use crate::media::Media;
pub const RTMP_VERSION: u8 = 3;
pub const HANDSHAKE_PACKET_LEN: usize = 1536;
const HANDSHAKE_TIME_LEN: usize = 4;
pub const HANDSHAKE_RANDOM_LEN: usize = HANDSHAKE_PACKET_LEN - 2 * HANDSHAKE_TIME_LEN;
pub const DEFAULT_CHUNK_SIZE: usize = 128;
pub const CSID_CONTROL: u32 = 2;
const CSID_AUDIO: u32 = 4;
const CSID_VIDEO: u32 = 5;
const CSID_DATA: u32 = 6;
const CSID_MARKER_2BYTE: u8 = 0;
const CSID_MARKER_3BYTE: u8 = 1;
const CSID_1BYTE_MAX: u32 = 63;
const CSID_2BYTE_MAX: u32 = 319;
const CSID_EXT_OFFSET: u32 = 64;
const EXT_TIMESTAMP_SENTINEL: u32 = 0x00FF_FFFF;
const EXT_TIMESTAMP_LEN: usize = 4;
const MSG_HEADER_LEN_FMT0: usize = 11;
const MSG_HEADER_LEN_FMT1: usize = 7;
const MSG_HEADER_LEN_FMT2: usize = 3;
const MSG_HEADER_LEN_FMT3: usize = 0;
pub mod msg_type {
pub const SET_CHUNK_SIZE: u8 = 1;
pub const ABORT: u8 = 2;
pub const ACKNOWLEDGEMENT: u8 = 3;
pub const USER_CONTROL: u8 = 4;
pub const WINDOW_ACK_SIZE: u8 = 5;
pub const SET_PEER_BANDWIDTH: u8 = 6;
pub const AUDIO: u8 = 8;
pub const VIDEO: u8 = 9;
pub const DATA_AMF3: u8 = 15;
pub const DATA_AMF0: u8 = 18;
pub const COMMAND_AMF3: u8 = 17;
pub const COMMAND_AMF0: u8 = 20;
}
pub mod bandwidth_limit {
pub const HARD: u8 = 0;
pub const SOFT: u8 = 1;
pub const DYNAMIC: u8 = 2;
}
pub mod amf0 {
pub const NUMBER: u8 = 0x00;
pub const BOOLEAN: u8 = 0x01;
pub const STRING: u8 = 0x02;
pub const OBJECT: u8 = 0x03;
pub const NULL: u8 = 0x05;
pub const ECMA_ARRAY: u8 = 0x08;
pub const OBJECT_END: u8 = 0x09;
}
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub enum RtmpError {
Truncated {
what: &'static str,
need: usize,
have: usize,
},
BadVersion(u8),
NoChunkContext(u32),
BadControlLength {
msg_type: u8,
need: usize,
have: usize,
},
UnknownControlMsgType(u8),
UnsupportedAmf0Marker(u8),
AmfStringTooLong {
len: usize,
},
IncompleteMessage {
csid: u32,
declared: usize,
collected: usize,
},
Flv(FlvError),
Amf0TooDeep {
depth: usize,
},
FieldOverflow(broadcast_common::len::FieldOverflow),
}
pub const MAX_AMF0_DEPTH: usize = 32;
impl fmt::Display for RtmpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RtmpError::Truncated { what, need, have } => {
write!(
f,
"RTMP truncated while parsing {what}: need {need}, have {have}"
)
}
RtmpError::BadVersion(v) => write!(f, "RTMP bad version {v} (expected {RTMP_VERSION})"),
RtmpError::NoChunkContext(csid) => {
write!(
f,
"RTMP chunk on csid {csid} has no Type-0 context to inherit"
)
}
RtmpError::BadControlLength {
msg_type,
need,
have,
} => write!(
f,
"RTMP control message type {msg_type} bad length: need {need}, have {have}"
),
RtmpError::UnknownControlMsgType(t) => {
write!(f, "RTMP unknown protocol-control message type {t}")
}
RtmpError::UnsupportedAmf0Marker(m) => {
write!(f, "RTMP unsupported AMF0 marker 0x{m:02X}")
}
RtmpError::Amf0TooDeep { depth } => write!(
f,
"RTMP AMF0 nesting depth {depth} exceeds the {MAX_AMF0_DEPTH}-level cap"
),
RtmpError::AmfStringTooLong { len } => {
write!(
f,
"RTMP AMF0 string of {len} bytes exceeds the u16 length prefix (long string type not implemented)"
)
}
RtmpError::IncompleteMessage {
csid,
declared,
collected,
} => write!(
f,
"RTMP incomplete message on csid {csid}: declared {declared}, collected {collected}"
),
RtmpError::Flv(e) => write!(f, "RTMP FLV routing: {e}"),
RtmpError::FieldOverflow(e) => write!(f, "RTMP {e}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for RtmpError {}
impl From<FlvError> for RtmpError {
fn from(e: FlvError) -> Self {
RtmpError::Flv(e)
}
}
impl From<broadcast_common::len::FieldOverflow> for RtmpError {
fn from(e: broadcast_common::len::FieldOverflow) -> Self {
RtmpError::FieldOverflow(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Handshake0 {
pub version: u8,
}
impl Handshake0 {
pub fn parse(input: &[u8]) -> Result<Self, RtmpError> {
let v = *input.first().ok_or(RtmpError::Truncated {
what: "C0/S0 version",
need: 1,
have: 0,
})?;
if v != RTMP_VERSION {
return Err(RtmpError::BadVersion(v));
}
Ok(Self { version: v })
}
pub fn to_bytes(&self) -> Vec<u8> {
vec![self.version]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handshake1 {
pub time: u32,
pub random: Vec<u8>,
}
impl Handshake1 {
pub fn parse(input: &[u8]) -> Result<Self, RtmpError> {
if input.len() < HANDSHAKE_PACKET_LEN {
return Err(RtmpError::Truncated {
what: "C1/S1",
need: HANDSHAKE_PACKET_LEN,
have: input.len(),
});
}
let time = u32::from_be_bytes([input[0], input[1], input[2], input[3]]);
let random = input[2 * HANDSHAKE_TIME_LEN..HANDSHAKE_PACKET_LEN].to_vec();
Ok(Self { time, random })
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(HANDSHAKE_PACKET_LEN);
out.extend_from_slice(&self.time.to_be_bytes());
out.extend_from_slice(&[0u8; HANDSHAKE_TIME_LEN]); let mut rnd = self.random.clone();
rnd.resize(HANDSHAKE_RANDOM_LEN, 0);
out.extend_from_slice(&rnd);
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handshake2 {
pub time: u32,
pub time2: u32,
pub random_echo: Vec<u8>,
}
impl Handshake2 {
pub fn parse(input: &[u8]) -> Result<Self, RtmpError> {
if input.len() < HANDSHAKE_PACKET_LEN {
return Err(RtmpError::Truncated {
what: "C2/S2",
need: HANDSHAKE_PACKET_LEN,
have: input.len(),
});
}
let time = u32::from_be_bytes([input[0], input[1], input[2], input[3]]);
let time2 = u32::from_be_bytes([input[4], input[5], input[6], input[7]]);
let random_echo = input[2 * HANDSHAKE_TIME_LEN..HANDSHAKE_PACKET_LEN].to_vec();
Ok(Self {
time,
time2,
random_echo,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(HANDSHAKE_PACKET_LEN);
out.extend_from_slice(&self.time.to_be_bytes());
out.extend_from_slice(&self.time2.to_be_bytes());
let mut echo = self.random_echo.clone();
echo.resize(HANDSHAKE_RANDOM_LEN, 0);
out.extend_from_slice(&echo);
out
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BasicHeader {
pub fmt: u8,
pub csid: u32,
}
impl BasicHeader {
pub fn serialized_len(&self) -> usize {
if self.csid <= CSID_1BYTE_MAX {
1
} else if self.csid <= CSID_2BYTE_MAX {
2
} else {
3
}
}
pub fn write_into(&self, out: &mut Vec<u8>) {
let fmt_bits = (self.fmt & 0x03) << 6;
if self.csid <= CSID_1BYTE_MAX {
out.push(fmt_bits | (self.csid as u8 & 0x3F));
} else if self.csid <= CSID_2BYTE_MAX {
out.push(fmt_bits | CSID_MARKER_2BYTE);
out.push((self.csid - CSID_EXT_OFFSET) as u8);
} else {
out.push(fmt_bits | CSID_MARKER_3BYTE);
let ext = self.csid - CSID_EXT_OFFSET;
out.push((ext & 0xFF) as u8);
out.push(((ext >> 8) & 0xFF) as u8);
}
}
pub fn parse(input: &[u8]) -> Result<(Self, usize), RtmpError> {
let b0 = *input.first().ok_or(RtmpError::Truncated {
what: "basic header",
need: 1,
have: 0,
})?;
let fmt = b0 >> 6;
let marker = b0 & 0x3F;
if marker == CSID_MARKER_2BYTE {
let b1 = *input.get(1).ok_or(RtmpError::Truncated {
what: "basic header (2-byte)",
need: 2,
have: input.len(),
})?;
Ok((
Self {
fmt,
csid: b1 as u32 + CSID_EXT_OFFSET,
},
2,
))
} else if marker == CSID_MARKER_3BYTE {
if input.len() < 3 {
return Err(RtmpError::Truncated {
what: "basic header (3-byte)",
need: 3,
have: input.len(),
});
}
let ext = input[1] as u32 + ((input[2] as u32) << 8);
Ok((
Self {
fmt,
csid: ext + CSID_EXT_OFFSET,
},
3,
))
} else {
Ok((
Self {
fmt,
csid: marker as u32,
},
1,
))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct MessageHeader {
pub timestamp: u32,
pub message_length: u32,
pub message_type_id: u8,
pub message_stream_id: u32,
}
impl MessageHeader {
pub fn serialized_len(fmt: u8) -> usize {
match fmt {
0 => MSG_HEADER_LEN_FMT0,
1 => MSG_HEADER_LEN_FMT1,
2 => MSG_HEADER_LEN_FMT2,
_ => MSG_HEADER_LEN_FMT3,
}
}
pub fn needs_extended(&self, fmt: u8) -> bool {
fmt != 3 && self.timestamp >= EXT_TIMESTAMP_SENTINEL
}
pub fn write_into(&self, fmt: u8, out: &mut Vec<u8>) {
let ext = self.needs_extended(fmt);
let ts24 = if ext {
EXT_TIMESTAMP_SENTINEL
} else {
self.timestamp
};
if fmt <= 2 {
write_u24(out, ts24);
}
if fmt <= 1 {
write_u24(out, self.message_length);
out.push(self.message_type_id);
}
if fmt == 0 {
out.extend_from_slice(&self.message_stream_id.to_le_bytes());
}
if ext {
out.extend_from_slice(&self.timestamp.to_be_bytes());
}
}
fn parse_fields(fmt: u8, input: &[u8]) -> Result<(Self, u32, usize), RtmpError> {
let need = Self::serialized_len(fmt);
if input.len() < need {
return Err(RtmpError::Truncated {
what: "message header",
need,
have: input.len(),
});
}
let mut off = 0;
let mut h = MessageHeader::default();
let mut ts24 = 0;
if fmt <= 2 {
ts24 = read_u24(&input[off..]);
h.timestamp = ts24;
off += 3;
}
if fmt <= 1 {
h.message_length = read_u24(&input[off..]);
off += 3;
h.message_type_id = input[off];
off += 1;
}
if fmt == 0 {
h.message_stream_id =
u32::from_le_bytes([input[off], input[off + 1], input[off + 2], input[off + 3]]);
off += 4;
}
Ok((h, ts24, off))
}
}
fn write_u24(out: &mut Vec<u8>, v: u32) {
out.push((v >> 16) as u8);
out.push((v >> 8) as u8);
out.push(v as u8);
}
fn read_u24(b: &[u8]) -> u32 {
((b[0] as u32) << 16) | ((b[1] as u32) << 8) | (b[2] as u32)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProtocolControl {
SetChunkSize(u32),
Abort(u32),
Acknowledgement(u32),
WindowAckSize(u32),
SetPeerBandwidth {
window_size: u32,
limit_type: u8,
},
}
impl ProtocolControl {
pub fn message_type_id(&self) -> u8 {
match self {
ProtocolControl::SetChunkSize(_) => msg_type::SET_CHUNK_SIZE,
ProtocolControl::Abort(_) => msg_type::ABORT,
ProtocolControl::Acknowledgement(_) => msg_type::ACKNOWLEDGEMENT,
ProtocolControl::WindowAckSize(_) => msg_type::WINDOW_ACK_SIZE,
ProtocolControl::SetPeerBandwidth { .. } => msg_type::SET_PEER_BANDWIDTH,
}
}
pub fn to_body(&self) -> Vec<u8> {
match *self {
ProtocolControl::SetChunkSize(v) => (v & 0x7FFF_FFFF).to_be_bytes().to_vec(),
ProtocolControl::Abort(v) => v.to_be_bytes().to_vec(),
ProtocolControl::Acknowledgement(v) => v.to_be_bytes().to_vec(),
ProtocolControl::WindowAckSize(v) => v.to_be_bytes().to_vec(),
ProtocolControl::SetPeerBandwidth {
window_size,
limit_type,
} => {
let mut out = Vec::with_capacity(5);
out.extend_from_slice(&window_size.to_be_bytes());
out.push(limit_type);
out
}
}
}
pub fn is_control_type(msg_type_id: u8) -> bool {
matches!(
msg_type_id,
msg_type::SET_CHUNK_SIZE
| msg_type::ABORT
| msg_type::ACKNOWLEDGEMENT
| msg_type::WINDOW_ACK_SIZE
| msg_type::SET_PEER_BANDWIDTH
)
}
pub fn parse(msg_type_id: u8, body: &[u8]) -> Result<Self, RtmpError> {
let want_u32 = |what_len: usize| -> Result<u32, RtmpError> {
if body.len() < what_len {
Err(RtmpError::BadControlLength {
msg_type: msg_type_id,
need: what_len,
have: body.len(),
})
} else {
Ok(u32::from_be_bytes([body[0], body[1], body[2], body[3]]))
}
};
match msg_type_id {
msg_type::SET_CHUNK_SIZE => {
Ok(ProtocolControl::SetChunkSize(want_u32(4)? & 0x7FFF_FFFF))
}
msg_type::ABORT => Ok(ProtocolControl::Abort(want_u32(4)?)),
msg_type::ACKNOWLEDGEMENT => Ok(ProtocolControl::Acknowledgement(want_u32(4)?)),
msg_type::WINDOW_ACK_SIZE => Ok(ProtocolControl::WindowAckSize(want_u32(4)?)),
msg_type::SET_PEER_BANDWIDTH => {
if body.len() < 5 {
return Err(RtmpError::BadControlLength {
msg_type: msg_type_id,
need: 5,
have: body.len(),
});
}
Ok(ProtocolControl::SetPeerBandwidth {
window_size: u32::from_be_bytes([body[0], body[1], body[2], body[3]]),
limit_type: body[4],
})
}
other => Err(RtmpError::UnknownControlMsgType(other)),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum AmfValue {
Number(f64),
Boolean(bool),
String(String),
Object(Vec<(String, AmfValue)>),
Null,
EcmaArray(Vec<(String, AmfValue)>),
}
impl AmfValue {
pub fn write_into(&self, out: &mut Vec<u8>) -> Result<(), RtmpError> {
match self {
AmfValue::Number(n) => {
out.push(amf0::NUMBER);
out.extend_from_slice(&n.to_be_bytes());
}
AmfValue::Boolean(b) => {
out.push(amf0::BOOLEAN);
out.push(u8::from(*b));
}
AmfValue::String(s) => {
out.push(amf0::STRING);
write_amf0_string(out, s)?;
}
AmfValue::Null => out.push(amf0::NULL),
AmfValue::Object(members) => {
out.push(amf0::OBJECT);
write_amf0_members(out, members)?;
}
AmfValue::EcmaArray(members) => {
out.push(amf0::ECMA_ARRAY);
out.extend_from_slice(&(members.len() as u32).to_be_bytes());
write_amf0_members(out, members)?;
}
}
Ok(())
}
pub fn parse(input: &[u8]) -> Result<(Self, usize), RtmpError> {
Self::parse_at_depth(input, 0)
}
fn parse_at_depth(input: &[u8], depth: usize) -> Result<(Self, usize), RtmpError> {
if depth > MAX_AMF0_DEPTH {
return Err(RtmpError::Amf0TooDeep { depth });
}
let marker = *input.first().ok_or(RtmpError::Truncated {
what: "AMF0 marker",
need: 1,
have: 0,
})?;
let rest = &input[1..];
match marker {
amf0::NUMBER => {
if rest.len() < 8 {
return Err(RtmpError::Truncated {
what: "AMF0 number",
need: 8,
have: rest.len(),
});
}
let mut b = [0u8; 8];
b.copy_from_slice(&rest[..8]);
Ok((AmfValue::Number(f64::from_be_bytes(b)), 9))
}
amf0::BOOLEAN => {
let v = *rest.first().ok_or(RtmpError::Truncated {
what: "AMF0 boolean",
need: 1,
have: 0,
})?;
Ok((AmfValue::Boolean(v != 0), 2))
}
amf0::STRING => {
let (s, n) = read_amf0_string(rest)?;
Ok((AmfValue::String(s), 1 + n))
}
amf0::NULL => Ok((AmfValue::Null, 1)),
amf0::OBJECT => {
let (members, n) = read_amf0_members(rest, depth + 1)?;
Ok((AmfValue::Object(members), 1 + n))
}
amf0::ECMA_ARRAY => {
if rest.len() < 4 {
return Err(RtmpError::Truncated {
what: "AMF0 ECMA array count",
need: 4,
have: rest.len(),
});
}
let (members, n) = read_amf0_members(&rest[4..], depth + 1)?;
Ok((AmfValue::EcmaArray(members), 1 + 4 + n))
}
other => Err(RtmpError::UnsupportedAmf0Marker(other)),
}
}
}
fn write_amf0_string(out: &mut Vec<u8>, s: &str) -> Result<(), RtmpError> {
if s.len() > usize::from(u16::MAX) {
return Err(RtmpError::AmfStringTooLong { len: s.len() });
}
out.extend_from_slice(&(s.len() as u16).to_be_bytes());
out.extend_from_slice(s.as_bytes());
Ok(())
}
fn write_amf0_members(out: &mut Vec<u8>, members: &[(String, AmfValue)]) -> Result<(), RtmpError> {
for (k, v) in members {
write_amf0_string(out, k)?;
v.write_into(out)?;
}
out.extend_from_slice(&0u16.to_be_bytes());
out.push(amf0::OBJECT_END);
Ok(())
}
fn read_amf0_string(input: &[u8]) -> Result<(String, usize), RtmpError> {
if input.len() < 2 {
return Err(RtmpError::Truncated {
what: "AMF0 string length",
need: 2,
have: input.len(),
});
}
let len = u16::from_be_bytes([input[0], input[1]]) as usize;
if input.len() < 2 + len {
return Err(RtmpError::Truncated {
what: "AMF0 string body",
need: 2 + len,
have: input.len(),
});
}
let s = String::from_utf8_lossy(&input[2..2 + len]).into_owned();
Ok((s, 2 + len))
}
fn read_amf0_members(
input: &[u8],
depth: usize,
) -> Result<(Vec<(String, AmfValue)>, usize), RtmpError> {
let mut members = Vec::new();
let mut off = 0;
loop {
let (key, kn) = read_amf0_string(&input[off..])?;
if key.is_empty() {
let end = *input.get(off + kn).ok_or(RtmpError::Truncated {
what: "AMF0 object-end marker",
need: off + kn + 1,
have: input.len(),
})?;
if end == amf0::OBJECT_END {
off += kn + 1;
return Ok((members, off));
}
}
off += kn;
let (val, vn) = AmfValue::parse_at_depth(&input[off..], depth)?;
off += vn;
members.push((key, val));
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Command {
pub name: String,
pub transaction_id: f64,
pub arguments: Vec<AmfValue>,
}
impl Command {
pub fn to_body(&self) -> Result<Vec<u8>, RtmpError> {
let mut out = Vec::new();
AmfValue::String(self.name.clone()).write_into(&mut out)?;
AmfValue::Number(self.transaction_id).write_into(&mut out)?;
for a in &self.arguments {
a.write_into(&mut out)?;
}
Ok(out)
}
pub fn parse(body: &[u8]) -> Result<Self, RtmpError> {
let (name_v, mut off) = AmfValue::parse(body)?;
let AmfValue::String(name) = name_v else {
return Err(RtmpError::UnsupportedAmf0Marker(
body.first().copied().unwrap_or(0),
));
};
let (txn_v, n) = AmfValue::parse(&body[off..])?;
off += n;
let transaction_id = match txn_v {
AmfValue::Number(n) => n,
_ => 0.0,
};
let mut arguments = Vec::new();
while off < body.len() {
let (v, n) = AmfValue::parse(&body[off..])?;
off += n;
arguments.push(v);
}
Ok(Command {
name,
transaction_id,
arguments,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub csid: u32,
pub message_type_id: u8,
pub message_stream_id: u32,
pub timestamp: u32,
pub body: Vec<u8>,
}
pub fn write_chunks(messages: &[Message], chunk_size: usize) -> Result<Vec<u8>, RtmpError> {
let chunk_size = chunk_size.max(1);
let mut out = Vec::new();
for m in messages {
let message_length = broadcast_common::len::fit_u24(m.body.len(), "message_length")?;
let mut body_off = 0;
let mut first = true;
loop {
let fmt = if first { 0u8 } else { 3u8 };
BasicHeader { fmt, csid: m.csid }.write_into(&mut out);
if first {
let mh = MessageHeader {
timestamp: m.timestamp,
message_length,
message_type_id: m.message_type_id,
message_stream_id: m.message_stream_id,
};
mh.write_into(0, &mut out);
} else if m.timestamp >= EXT_TIMESTAMP_SENTINEL {
out.extend_from_slice(&m.timestamp.to_be_bytes());
}
let take = core::cmp::min(chunk_size, m.body.len() - body_off);
out.extend_from_slice(&m.body[body_off..body_off + take]);
body_off += take;
first = false;
if body_off >= m.body.len() {
break;
}
}
}
Ok(out)
}
#[derive(Clone)]
struct ChunkContext {
message_type_id: u8,
message_length: usize,
message_stream_id: u32,
timestamp: u32,
timestamp_delta: u32,
extended: bool,
partial: Vec<u8>,
}
pub fn read_chunks(mut input: &[u8]) -> Result<Vec<Message>, RtmpError> {
let mut chunk_size = DEFAULT_CHUNK_SIZE;
let mut ctx: Vec<(u32, ChunkContext)> = Vec::new();
let mut out = Vec::new();
while !input.is_empty() {
let (bh, bn) = BasicHeader::parse(input)?;
let mut off = bn;
let (mh, ts24, mn) = MessageHeader::parse_fields(bh.fmt, &input[off..])?;
off += mn;
let idx = ctx.iter().position(|(c, _)| *c == bh.csid);
let prev = idx.map(|i| {
let slot = &mut ctx[i].1;
let partial = core::mem::take(&mut slot.partial);
ChunkContext {
partial,
..slot.clone()
}
});
let ext_present = match bh.fmt {
0..=2 => ts24 >= EXT_TIMESTAMP_SENTINEL,
_ => prev.as_ref().map(|p| p.extended).unwrap_or(false),
};
let ext_ts = if ext_present {
if input.len() < off + EXT_TIMESTAMP_LEN {
return Err(RtmpError::Truncated {
what: "extended timestamp",
need: off + EXT_TIMESTAMP_LEN,
have: input.len(),
});
}
let t =
u32::from_be_bytes([input[off], input[off + 1], input[off + 2], input[off + 3]]);
off += EXT_TIMESTAMP_LEN;
Some(t)
} else {
None
};
let delta = ext_ts.unwrap_or(mh.timestamp);
let mut cx = match bh.fmt {
0 => ChunkContext {
message_type_id: mh.message_type_id,
message_length: mh.message_length as usize,
message_stream_id: mh.message_stream_id,
timestamp: ext_ts.unwrap_or(mh.timestamp),
timestamp_delta: 0,
extended: ext_present,
partial: Vec::new(),
},
1 => {
let p = prev.ok_or(RtmpError::NoChunkContext(bh.csid))?;
ChunkContext {
message_type_id: mh.message_type_id,
message_length: mh.message_length as usize,
message_stream_id: p.message_stream_id,
timestamp: p.timestamp.wrapping_add(delta),
timestamp_delta: delta,
extended: ext_present,
partial: Vec::new(),
}
}
2 => {
let p = prev.ok_or(RtmpError::NoChunkContext(bh.csid))?;
ChunkContext {
message_type_id: p.message_type_id,
message_length: p.message_length,
message_stream_id: p.message_stream_id,
timestamp: p.timestamp.wrapping_add(delta),
timestamp_delta: delta,
extended: ext_present,
partial: Vec::new(),
}
}
_ => {
let mut p = prev.ok_or(RtmpError::NoChunkContext(bh.csid))?;
let starting_new_message = p.partial.is_empty();
if starting_new_message {
let advance = ext_ts.unwrap_or(p.timestamp_delta);
p.timestamp = p.timestamp.wrapping_add(advance);
}
p
}
};
let remaining = cx.message_length.checked_sub(cx.partial.len()).ok_or(
RtmpError::IncompleteMessage {
csid: bh.csid,
declared: cx.message_length,
collected: cx.partial.len(),
},
)?;
let take = core::cmp::min(chunk_size, remaining);
if input.len() < off + take {
return Err(RtmpError::Truncated {
what: "chunk data",
need: off + take,
have: input.len(),
});
}
cx.partial.extend_from_slice(&input[off..off + take]);
off += take;
input = &input[off..];
if cx.partial.len() >= cx.message_length {
let body = core::mem::take(&mut cx.partial);
match ProtocolControl::is_control_type(cx.message_type_id) {
false => {}
true => match ProtocolControl::parse(cx.message_type_id, &body) {
Ok(ProtocolControl::SetChunkSize(sz)) => {
chunk_size = (sz as usize).max(1);
}
Ok(ProtocolControl::Abort(target)) => {
if let Some(i) = ctx.iter().position(|(c, _)| *c == target) {
ctx[i].1.partial.clear();
ctx[i].1.timestamp_delta = 0;
}
}
_ => {}
},
}
out.push(Message {
csid: bh.csid,
message_type_id: cx.message_type_id,
message_stream_id: cx.message_stream_id,
timestamp: cx.timestamp,
body,
});
}
match idx {
Some(i) => ctx[i].1 = cx,
None => ctx.push((bh.csid, cx)),
}
}
Ok(out)
}
const FLV_SIGNATURE: [u8; 3] = *b"FLV";
const FLV_VERSION: u8 = 1;
const FLV_HEADER_LEN: u32 = 9;
const FLV_FLAG_AUDIO: u8 = 0x04;
const FLV_FLAG_VIDEO: u8 = 0x01;
const FLV_TAG_HEADER_LEN: usize = 11;
const FLV_PREV_TAG_SIZE_LEN: usize = 4;
const FLV_TAG_AUDIO: u8 = msg_type::AUDIO;
const FLV_TAG_VIDEO: u8 = msg_type::VIDEO;
const FLV_TAG_SCRIPT: u8 = msg_type::DATA_AMF0;
fn build_flv(
tags: &[(u8, u32, Vec<u8>)],
has_video: bool,
has_audio: bool,
) -> Result<Vec<u8>, RtmpError> {
let mut out = Vec::new();
out.extend_from_slice(&FLV_SIGNATURE);
out.push(FLV_VERSION);
let mut flags = 0u8;
if has_video {
flags |= FLV_FLAG_VIDEO;
}
if has_audio {
flags |= FLV_FLAG_AUDIO;
}
out.push(flags);
out.extend_from_slice(&FLV_HEADER_LEN.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes()); for (tag_type, ts, body) in tags {
let start = out.len();
out.push(*tag_type);
let data_size = broadcast_common::len::fit_u24(body.len(), "DataSize")?;
write_u24(&mut out, data_size);
out.push((*ts >> 16) as u8);
out.push((*ts >> 8) as u8);
out.push(*ts as u8);
out.push((*ts >> 24) as u8);
out.extend_from_slice(&[0, 0, 0]); out.extend_from_slice(body);
let tag_size = (out.len() - start) as u32;
out.extend_from_slice(&tag_size.to_be_bytes());
}
Ok(out)
}
fn split_flv(flv: &[u8]) -> Result<Vec<(u8, u32, Vec<u8>)>, RtmpError> {
if flv.len() < FLV_HEADER_LEN as usize + FLV_PREV_TAG_SIZE_LEN {
return Err(RtmpError::Truncated {
what: "FLV header",
need: FLV_HEADER_LEN as usize + FLV_PREV_TAG_SIZE_LEN,
have: flv.len(),
});
}
let data_offset = u32::from_be_bytes([flv[5], flv[6], flv[7], flv[8]]) as usize;
let mut off = data_offset.max(FLV_HEADER_LEN as usize) + FLV_PREV_TAG_SIZE_LEN;
let mut tags = Vec::new();
while off + FLV_TAG_HEADER_LEN <= flv.len() {
let tag_type = flv[off];
let data_size = read_u24(&flv[off + 1..]) as usize;
let ts_lo = read_u24(&flv[off + 4..]);
let ts_ext = flv[off + 7] as u32;
let timestamp = (ts_ext << 24) | ts_lo;
let body_start = off + FLV_TAG_HEADER_LEN;
let body_end = body_start + data_size;
if body_end + FLV_PREV_TAG_SIZE_LEN > flv.len() {
return Err(RtmpError::Truncated {
what: "FLV tag body",
need: body_end + FLV_PREV_TAG_SIZE_LEN,
have: flv.len(),
});
}
tags.push((tag_type, timestamp, flv[body_start..body_end].to_vec()));
off = body_end + FLV_PREV_TAG_SIZE_LEN;
}
Ok(tags)
}
#[derive(Debug, Default, Clone)]
pub struct RtmpDemux<'a> {
_marker: core::marker::PhantomData<&'a [u8]>,
}
impl RtmpDemux<'_> {
pub fn new() -> Self {
Self {
_marker: core::marker::PhantomData,
}
}
}
impl<'a> Unpackage for RtmpDemux<'a> {
type Input = &'a [u8];
type Media = Media;
type Error = RtmpError;
fn unpackage(&mut self, input: &'a [u8]) -> Result<Media, RtmpError> {
let messages = read_chunks(input)?;
let mut tags: Vec<(u8, u32, Vec<u8>)> = Vec::new();
let mut has_video = false;
let mut has_audio = false;
for m in messages {
match m.message_type_id {
msg_type::AUDIO => {
has_audio = true;
tags.push((FLV_TAG_AUDIO, m.timestamp, m.body));
}
msg_type::VIDEO => {
has_video = true;
tags.push((FLV_TAG_VIDEO, m.timestamp, m.body));
}
msg_type::DATA_AMF0 => {
tags.push((FLV_TAG_SCRIPT, m.timestamp, m.body));
}
_ => { }
}
}
let flv = build_flv(&tags, has_video, has_audio)?;
let mut demux = FlvDemux::new();
demux.unpackage(&flv).map_err(RtmpError::Flv)
}
}
#[derive(Debug, Clone)]
pub struct RtmpMux {
pub chunk_size: usize,
pub message_stream_id: u32,
}
impl Default for RtmpMux {
fn default() -> Self {
Self {
chunk_size: DEFAULT_CHUNK_SIZE,
message_stream_id: 1,
}
}
}
impl RtmpMux {
pub fn new() -> Self {
Self::default()
}
pub fn with_chunk_size(chunk_size: usize) -> Self {
Self {
chunk_size: chunk_size.max(1),
message_stream_id: 1,
}
}
}
impl Package for RtmpMux {
type Media = Media;
type Output = Vec<u8>;
type Error = RtmpError;
fn package(&mut self, media: &Media) -> Result<Vec<u8>, RtmpError> {
let mut flv_mux = FlvMux::new();
let flv = flv_mux.package(media).map_err(RtmpError::Flv)?;
let tags = split_flv(&flv)?;
let mut messages = Vec::with_capacity(tags.len());
for (tag_type, ts, body) in tags {
let (csid, msg_type_id) = match tag_type {
FLV_TAG_AUDIO => (CSID_AUDIO, msg_type::AUDIO),
FLV_TAG_VIDEO => (CSID_VIDEO, msg_type::VIDEO),
FLV_TAG_SCRIPT => (CSID_DATA, msg_type::DATA_AMF0),
_ => (CSID_DATA, msg_type::DATA_AMF0),
};
messages.push(Message {
csid,
message_type_id: msg_type_id,
message_stream_id: self.message_stream_id,
timestamp: ts,
body,
});
}
write_chunks(&messages, self.chunk_size)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn u24_round_trip() {
let mut out = Vec::new();
write_u24(&mut out, 0x123456);
assert_eq!(out, [0x12, 0x34, 0x56]);
assert_eq!(read_u24(&out), 0x123456);
}
#[test]
fn basic_header_forms() {
for csid in [3u32, 63, 64, 319, 320, 65599] {
let bh = BasicHeader { fmt: 0, csid };
let mut out = Vec::new();
bh.write_into(&mut out);
assert_eq!(out.len(), bh.serialized_len());
let (parsed, n) = BasicHeader::parse(&out).unwrap();
assert_eq!(parsed, bh);
assert_eq!(n, out.len());
}
}
#[test]
fn amf0_object_round_trip() {
let obj = AmfValue::Object(vec![
("app".into(), AmfValue::String("live".into())),
("audioOnly".into(), AmfValue::Boolean(false)),
("fps".into(), AmfValue::Number(30.0)),
]);
let mut out = Vec::new();
obj.write_into(&mut out).unwrap();
let (parsed, n) = AmfValue::parse(&out).unwrap();
assert_eq!(parsed, obj);
assert_eq!(n, out.len());
}
#[test]
fn fmt1_header_starts_a_fresh_message_on_the_same_csid() {
let mut input = Vec::new();
BasicHeader { fmt: 0, csid: 3 }.write_into(&mut input);
MessageHeader {
timestamp: 0,
message_length: 200,
message_type_id: msg_type::VIDEO,
message_stream_id: 1,
}
.write_into(0, &mut input);
let first_chunk: Vec<u8> = (0..DEFAULT_CHUNK_SIZE).map(|i| i as u8).collect();
input.extend_from_slice(&first_chunk);
let second_body: Vec<u8> = (0xF0u8..0xF0 + 10).collect();
BasicHeader { fmt: 1, csid: 3 }.write_into(&mut input);
MessageHeader {
timestamp: 7,
message_length: second_body.len() as u32,
message_type_id: msg_type::VIDEO,
message_stream_id: 0,
}
.write_into(1, &mut input);
input.extend_from_slice(&second_body);
let msgs = read_chunks(&input).expect("fmt-1 must start a new message");
assert_eq!(
msgs.len(),
1,
"only the complete 10-byte message may be emitted"
);
assert_eq!(msgs[0].body, second_body);
assert!(
msgs.iter().all(|m| m.body.len() != 256),
"the stale fmt-0 partial must not be merged into the new message"
);
}
#[test]
fn fmt3_continuation_still_reassembles_a_split_message() {
let body: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
let mut input = Vec::new();
BasicHeader { fmt: 0, csid: 3 }.write_into(&mut input);
MessageHeader {
timestamp: 0,
message_length: body.len() as u32,
message_type_id: msg_type::VIDEO,
message_stream_id: 1,
}
.write_into(0, &mut input);
input.extend_from_slice(&body[..DEFAULT_CHUNK_SIZE]);
BasicHeader { fmt: 3, csid: 3 }.write_into(&mut input);
input.extend_from_slice(&body[DEFAULT_CHUNK_SIZE..]);
let msgs = read_chunks(&input).expect("fmt-3 must continue the message");
assert_eq!(msgs.len(), 1);
assert_eq!(msgs[0].body, body);
}
#[test]
fn control_type_guard_matches_the_control_message_set() {
for t in [
msg_type::SET_CHUNK_SIZE,
msg_type::ABORT,
msg_type::ACKNOWLEDGEMENT,
msg_type::WINDOW_ACK_SIZE,
msg_type::SET_PEER_BANDWIDTH,
] {
assert!(
ProtocolControl::is_control_type(t),
"{t} is protocol control"
);
}
for t in [
msg_type::AUDIO,
msg_type::VIDEO,
msg_type::DATA_AMF0,
msg_type::DATA_AMF3,
0,
0xFF,
] {
assert!(
!ProtocolControl::is_control_type(t),
"{t} is not protocol control"
);
assert!(ProtocolControl::parse(t, &[]).is_err());
}
}
#[test]
fn write_chunks_oversized_body_errors() {
let messages = alloc::vec![Message {
csid: 4,
message_type_id: msg_type::VIDEO,
message_stream_id: 1,
timestamp: 0,
body: alloc::vec![0u8; 1 << 24],
}];
let err = write_chunks(&messages, DEFAULT_CHUNK_SIZE).unwrap_err();
assert!(
matches!(
err,
RtmpError::FieldOverflow(broadcast_common::len::FieldOverflow {
field: "message_length",
..
})
),
"expected FieldOverflow for message_length, got {err:?}"
);
}
#[test]
fn write_chunks_max_body_round_trips() {
let body = alloc::vec![0xAAu8; (1 << 24) - 1];
let big_chunk_size = body.len();
let messages = alloc::vec![
Message {
csid: 2,
message_type_id: msg_type::SET_CHUNK_SIZE,
message_stream_id: 0,
timestamp: 0,
body: ProtocolControl::SetChunkSize(big_chunk_size as u32).to_body(),
},
Message {
csid: 4,
message_type_id: msg_type::VIDEO,
message_stream_id: 1,
timestamp: 0,
body: body.clone(),
},
];
let wire = write_chunks(&messages, big_chunk_size).unwrap();
let msgs = read_chunks(&wire).unwrap();
assert_eq!(msgs.len(), 2);
assert_eq!(msgs[1].body, body);
}
#[test]
fn build_flv_oversized_tag_errors() {
let tags = alloc::vec![(FLV_TAG_VIDEO, 0u32, alloc::vec![0u8; 1 << 24])];
let err = build_flv(&tags, true, false).unwrap_err();
assert!(
matches!(
err,
RtmpError::FieldOverflow(broadcast_common::len::FieldOverflow {
field: "DataSize",
..
})
),
"expected FieldOverflow for DataSize, got {err:?}"
);
}
#[test]
fn fmt3_started_message_advances_by_the_inherited_delta() {
const DELTA: u32 = 23; const FRAME: usize = 8;
let mut input = Vec::new();
BasicHeader { fmt: 0, csid: 6 }.write_into(&mut input);
MessageHeader {
timestamp: 0,
message_length: FRAME as u32,
message_type_id: msg_type::AUDIO,
message_stream_id: 1,
}
.write_into(0, &mut input);
input.extend_from_slice(&[0xAF, 0x01, 0, 0x11, 0x22, 0x33, 0x44, 0x55]);
BasicHeader { fmt: 2, csid: 6 }.write_into(&mut input);
MessageHeader {
timestamp: DELTA,
message_length: FRAME as u32,
message_type_id: 0,
message_stream_id: 0,
}
.write_into(2, &mut input);
input.extend_from_slice(&[0xAF, 0x01, 0, 0x11, 0x22, 0x33, 0x44, 0x55]);
for _ in 0..2 {
BasicHeader { fmt: 3, csid: 6 }.write_into(&mut input);
input.extend_from_slice(&[0xAF, 0x01, 0, 0x11, 0x22, 0x33, 0x44, 0x55]);
}
let msgs = read_chunks(&input).expect("chunk stream");
let stamps: Vec<u32> = msgs.iter().map(|m| m.timestamp).collect();
assert_eq!(stamps, vec![0, DELTA, 2 * DELTA, 3 * DELTA]);
}
#[test]
fn fmt3_continuation_does_not_advance_the_timestamp() {
let body: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
let mut input = Vec::new();
BasicHeader { fmt: 0, csid: 3 }.write_into(&mut input);
MessageHeader {
timestamp: 100,
message_length: body.len() as u32,
message_type_id: msg_type::VIDEO,
message_stream_id: 1,
}
.write_into(0, &mut input);
input.extend_from_slice(&body[..DEFAULT_CHUNK_SIZE]);
BasicHeader { fmt: 3, csid: 3 }.write_into(&mut input);
input.extend_from_slice(&body[DEFAULT_CHUNK_SIZE..]);
let msgs = read_chunks(&input).expect("chunk stream");
assert_eq!(msgs.len(), 1);
assert_eq!(msgs[0].timestamp, 100, "continuation keeps the timestamp");
assert_eq!(msgs[0].body, body);
}
#[test]
fn abort_message_discards_the_partial_on_the_target_csid() {
let mut input = Vec::new();
BasicHeader { fmt: 0, csid: 3 }.write_into(&mut input);
MessageHeader {
timestamp: 0,
message_length: 200,
message_type_id: msg_type::VIDEO,
message_stream_id: 1,
}
.write_into(0, &mut input);
input.extend_from_slice(&[0xAA; DEFAULT_CHUNK_SIZE]);
let abort_body = ProtocolControl::Abort(3).to_body();
BasicHeader { fmt: 0, csid: 2 }.write_into(&mut input);
MessageHeader {
timestamp: 0,
message_length: abort_body.len() as u32,
message_type_id: msg_type::ABORT,
message_stream_id: 0,
}
.write_into(0, &mut input);
input.extend_from_slice(&abort_body);
let resent: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
for piece in resent.chunks(DEFAULT_CHUNK_SIZE) {
BasicHeader { fmt: 3, csid: 3 }.write_into(&mut input);
input.extend_from_slice(piece);
}
let msgs = read_chunks(&input).expect("chunk stream");
let video: Vec<&Message> = msgs
.iter()
.filter(|m| m.message_type_id == msg_type::VIDEO)
.collect();
assert_eq!(video.len(), 1, "the aborted message is never emitted");
assert_eq!(
video[0].body, resent,
"only the re-sent message may be emitted on csid 3"
);
assert!(
msgs.iter().all(|m| !m.body.starts_with(&[0xAA, 0xAA])),
"no emitted message may contain the abandoned bytes"
);
}
#[test]
fn deeply_nested_amf0_is_rejected_not_a_stack_overflow() {
fn nested(depth: usize) -> Vec<u8> {
let mut out = Vec::new();
for _ in 0..depth {
out.push(amf0::OBJECT);
out.extend_from_slice(&1u16.to_be_bytes());
out.push(b'k');
}
out.push(amf0::NUMBER);
out.extend_from_slice(&0f64.to_be_bytes());
for _ in 0..depth {
out.extend_from_slice(&0u16.to_be_bytes());
out.push(amf0::OBJECT_END);
}
out
}
assert_eq!(MAX_AMF0_DEPTH, 32, "the documented cap");
assert!(
AmfValue::parse(&nested(32)).is_ok(),
"exactly 32 levels must parse"
);
let err33 = AmfValue::parse(&nested(33)).expect_err("33 levels must be rejected");
assert!(
matches!(err33, RtmpError::Amf0TooDeep { depth: 33 }),
"expected Amf0TooDeep {{ depth: 33 }}, got {err33:?}"
);
let err = AmfValue::parse(&nested(MAX_AMF0_DEPTH + 1))
.expect_err("nesting past the cap must be rejected");
assert!(
matches!(err, RtmpError::Amf0TooDeep { .. }),
"expected Amf0TooDeep, got {err:?}"
);
assert!(AmfValue::parse(&nested(250_000)).is_err());
}
#[test]
fn fmt3_uses_the_extended_timestamp_as_a_delta() {
const BIG_DELTA: u32 = 0x0100_0000;
const FRAME: usize = 2 * DEFAULT_CHUNK_SIZE;
const PAYLOAD: [u8; DEFAULT_CHUNK_SIZE] = [0xAF; DEFAULT_CHUNK_SIZE];
fn push_split_message(out: &mut Vec<u8>, ts: u32, head: impl FnOnce(&mut Vec<u8>)) {
head(out);
out.extend_from_slice(&PAYLOAD);
BasicHeader { fmt: 3, csid: 4 }.write_into(out);
if ts >= EXT_TIMESTAMP_SENTINEL {
out.extend_from_slice(&ts.to_be_bytes());
}
out.extend_from_slice(&PAYLOAD);
}
let mut input = Vec::new();
push_split_message(&mut input, 0, |out| {
BasicHeader { fmt: 0, csid: 4 }.write_into(out);
MessageHeader {
timestamp: 0,
message_length: FRAME as u32,
message_type_id: msg_type::AUDIO,
message_stream_id: 1,
}
.write_into(0, out);
});
push_split_message(&mut input, BIG_DELTA, |out| {
BasicHeader { fmt: 2, csid: 4 }.write_into(out);
MessageHeader {
timestamp: BIG_DELTA,
message_length: FRAME as u32, message_type_id: 0,
message_stream_id: 0,
}
.write_into(2, out);
});
let msgs = read_chunks(&input).expect("chunk stream");
let stamps: Vec<u32> = msgs.iter().map(|m| m.timestamp).collect();
assert_eq!(
stamps,
vec![0, BIG_DELTA],
"the fmt-2 message takes the extended value as a delta from 0, and its fmt-3 continuation leaves the timestamp alone"
);
let mut whole = input.clone();
BasicHeader { fmt: 3, csid: 4 }.write_into(&mut whole);
whole.extend_from_slice(&BIG_DELTA.to_be_bytes());
whole.extend_from_slice(&PAYLOAD);
BasicHeader { fmt: 3, csid: 4 }.write_into(&mut whole);
whole.extend_from_slice(&BIG_DELTA.to_be_bytes());
whole.extend_from_slice(&PAYLOAD);
let msgs = read_chunks(&whole).expect("chunk stream");
let stamps: Vec<u32> = msgs.iter().map(|m| m.timestamp).collect();
assert_eq!(
stamps,
vec![0, BIG_DELTA, 2 * BIG_DELTA],
"a fmt-3-started message advances by the extended delta"
);
}
#[test]
fn fmt3_delta_is_applied_once_per_new_message_only() {
const DELTA: u32 = 5;
const FRAME: usize = 64;
let mut input = Vec::new();
for (fmt, ts, mtype) in [(0u8, 0u32, msg_type::AUDIO), (1, DELTA, 0), (3, 0, 0)] {
BasicHeader { fmt, csid: 5 }.write_into(&mut input);
if fmt <= 1 {
MessageHeader {
timestamp: ts,
message_length: FRAME as u32,
message_type_id: mtype,
message_stream_id: 1,
}
.write_into(fmt, &mut input);
}
input.extend_from_slice(&[0x11u8; FRAME]);
}
let msgs = read_chunks(&input).expect("chunk stream");
let stamps: Vec<u32> = msgs.iter().map(|m| m.timestamp).collect();
assert_eq!(stamps, vec![0, DELTA, 2 * DELTA]);
}
#[test]
fn abort_resets_the_delta_for_a_fmt3_resend() {
const DELTA: u32 = 40;
const FRAME: usize = 64;
let mut input = Vec::new();
BasicHeader { fmt: 0, csid: 3 }.write_into(&mut input);
MessageHeader {
timestamp: 1000,
message_length: FRAME as u32,
message_type_id: msg_type::AUDIO,
message_stream_id: 1,
}
.write_into(0, &mut input);
input.extend_from_slice(&[0x22u8; FRAME]);
BasicHeader { fmt: 1, csid: 3 }.write_into(&mut input);
MessageHeader {
timestamp: DELTA,
message_length: FRAME as u32,
message_type_id: msg_type::AUDIO, message_stream_id: 0, }
.write_into(1, &mut input);
input.extend_from_slice(&[0x22u8; FRAME]);
let abort_body = ProtocolControl::Abort(3).to_body();
BasicHeader { fmt: 0, csid: 2 }.write_into(&mut input);
MessageHeader {
timestamp: 0,
message_length: abort_body.len() as u32,
message_type_id: msg_type::ABORT,
message_stream_id: 0,
}
.write_into(0, &mut input);
input.extend_from_slice(&abort_body);
BasicHeader { fmt: 3, csid: 3 }.write_into(&mut input);
input.extend_from_slice(&[0x22u8; FRAME]);
let msgs = read_chunks(&input).expect("chunk stream");
let audio: Vec<u32> = msgs
.iter()
.filter(|m| m.message_type_id == msg_type::AUDIO)
.map(|m| m.timestamp)
.collect();
assert_eq!(audio, vec![1000, 1040, 1040]);
}
}