use broadcast_common::{Package, Unpackage};
use transmux::FlvDemux;
use transmux::rtmp::{
self, AmfValue, BasicHeader, Command, HANDSHAKE_PACKET_LEN, Handshake0, Handshake1, Handshake2,
Message, MessageHeader, ProtocolControl, RTMP_VERSION, RtmpDemux, RtmpMux, msg_type,
read_chunks, write_chunks,
};
const FLV: &[u8] = include_bytes!("../../fixtures/flv/av.flv");
#[test]
fn chunk_headers_round_trip_all_fmts() {
let hdr = MessageHeader {
timestamp: 1000,
message_length: 307,
message_type_id: msg_type::VIDEO,
message_stream_id: 12346,
};
for fmt in 0u8..=3 {
let bh = BasicHeader { fmt, csid: 4 };
let mut buf = Vec::new();
bh.write_into(&mut buf);
hdr.write_into(fmt, &mut buf);
let (bh2, bn) = BasicHeader::parse(&buf).unwrap();
assert_eq!(bh2, bh, "fmt {fmt}: basic header round-trips");
let mh_len = MessageHeader::serialized_len(fmt);
assert_eq!(
buf.len() - bn,
mh_len,
"fmt {fmt}: message-header length matches spec table"
);
}
let mut a = Vec::new();
BasicHeader { fmt: 0, csid: 4 }.write_into(&mut a);
hdr.write_into(0, &mut a);
let mut b = Vec::new();
BasicHeader { fmt: 0, csid: 4 }.write_into(&mut b);
let mut hdr2 = hdr;
hdr2.message_length = 308;
hdr2.write_into(0, &mut b);
assert_ne!(a, b, "mutating message_length changes the serialized bytes");
let big = MessageHeader {
timestamp: 0x0100_0000,
message_length: 10,
message_type_id: msg_type::AUDIO,
message_stream_id: 1,
};
assert!(big.needs_extended(0));
let mut e = Vec::new();
big.write_into(0, &mut e);
assert_eq!(e.len(), 11 + 4, "extended timestamp appends 4 bytes");
}
#[test]
fn multi_chunk_reassembly_spans_and_interleaves() {
const CHUNK: usize = 128;
let video_body: Vec<u8> = (0..300u32).map(|i| (i % 256) as u8).collect();
assert!(video_body.len() > CHUNK, "body must span multiple chunks");
let audio_body: Vec<u8> = (0..40u8).map(|i| i.wrapping_mul(7)).collect();
let mut wire = Vec::new();
BasicHeader { fmt: 0, csid: 5 }.write_into(&mut wire);
MessageHeader {
timestamp: 1000,
message_length: video_body.len() as u32,
message_type_id: msg_type::VIDEO,
message_stream_id: 1,
}
.write_into(0, &mut wire);
wire.extend_from_slice(&video_body[0..CHUNK]);
BasicHeader { fmt: 0, csid: 4 }.write_into(&mut wire);
MessageHeader {
timestamp: 1010,
message_length: audio_body.len() as u32,
message_type_id: msg_type::AUDIO,
message_stream_id: 1,
}
.write_into(0, &mut wire);
wire.extend_from_slice(&audio_body);
BasicHeader { fmt: 3, csid: 5 }.write_into(&mut wire);
wire.extend_from_slice(&video_body[CHUNK..2 * CHUNK]);
BasicHeader { fmt: 3, csid: 5 }.write_into(&mut wire);
wire.extend_from_slice(&video_body[2 * CHUNK..]);
let msgs = read_chunks(&wire).unwrap();
assert_eq!(msgs.len(), 2, "two complete messages reassembled");
let video = msgs.iter().find(|m| m.csid == 5).unwrap();
let audio = msgs.iter().find(|m| m.csid == 4).unwrap();
assert_eq!(video.body, video_body, "video body reassembles exactly");
assert_eq!(audio.body, audio_body, "audio body reassembles exactly");
assert_eq!(video.message_type_id, msg_type::VIDEO);
assert_eq!(audio.message_type_id, msg_type::AUDIO);
let out = write_chunks(
&[
Message {
csid: 5,
message_type_id: msg_type::VIDEO,
message_stream_id: 1,
timestamp: 1000,
body: video_body.clone(),
},
Message {
csid: 4,
message_type_id: msg_type::AUDIO,
message_stream_id: 1,
timestamp: 1010,
body: audio_body.clone(),
},
],
CHUNK,
);
let re = read_chunks(&out).unwrap();
assert_eq!(re.len(), 2);
assert_eq!(re[0].body, video_body, "writer→reader preserves video body");
assert_eq!(re[1].body, audio_body, "writer→reader preserves audio body");
}
#[test]
fn handshake_byte_layout() {
let c0 = Handshake0 {
version: RTMP_VERSION,
};
let c0b = c0.to_bytes();
assert_eq!(c0b.len(), 1, "C0/S0 is 1 byte");
assert_eq!(c0b[0], 3, "version byte is 3");
assert_eq!(Handshake0::parse(&c0b).unwrap(), c0);
assert!(Handshake0::parse(&[6]).is_err(), "bad version rejected");
let random: Vec<u8> = (0..1528u32).map(|i| (i % 251) as u8).collect();
let c1 = Handshake1 {
time: 0x0A0B0C0D,
random: random.clone(),
};
let c1b = c1.to_bytes();
assert_eq!(c1b.len(), HANDSHAKE_PACKET_LEN, "C1/S1 is 1536 bytes");
assert_eq!(
&c1b[0..4],
&[0x0A, 0x0B, 0x0C, 0x0D],
"C1 time offset [0..4]"
);
assert_eq!(&c1b[4..8], &[0, 0, 0, 0], "C1 zero offset [4..8]");
assert_eq!(&c1b[8..], &random[..], "C1 random offset [8..1536]");
assert_eq!(Handshake1::parse(&c1b).unwrap(), c1);
let c2 = Handshake2 {
time: 0x11223344,
time2: 0x55667788,
random_echo: random.clone(),
};
let c2b = c2.to_bytes();
assert_eq!(c2b.len(), HANDSHAKE_PACKET_LEN, "C2/S2 is 1536 bytes");
assert_eq!(&c2b[0..4], &[0x11, 0x22, 0x33, 0x44], "C2 time offset");
assert_eq!(&c2b[4..8], &[0x55, 0x66, 0x77, 0x88], "C2 time2 offset");
assert_eq!(&c2b[8..], &random[..], "C2 random echo offset");
assert_eq!(Handshake2::parse(&c2b).unwrap(), c2);
let mut c0c1 = c0.to_bytes();
c0c1.extend_from_slice(&c1.to_bytes());
assert_eq!(c0c1.len(), 1 + HANDSHAKE_PACKET_LEN, "C0C1 is 1537 bytes");
}
#[test]
fn amf0_connect_command_round_trip() {
let cmd = Command {
name: "connect".into(),
transaction_id: 1.0,
arguments: vec![AmfValue::Object(vec![
("app".into(), AmfValue::String("live".into())),
("tcUrl".into(), AmfValue::String("rtmp://host/live".into())),
("objectEncoding".into(), AmfValue::Number(0.0)),
("fpad".into(), AmfValue::Boolean(false)),
])],
};
let body = cmd.to_body();
let decoded = Command::parse(&body).expect("decode connect");
assert_eq!(decoded, cmd, "connect command round-trips");
assert_eq!(decoded.name, "connect");
assert_eq!(decoded.transaction_id, 1.0);
let mut cmd2 = cmd.clone();
if let AmfValue::Object(m) = &mut cmd2.arguments[0] {
m[2].1 = AmfValue::Number(3.0); }
assert_ne!(cmd2.to_body(), body, "mutating a member changes the bytes");
let pc = ProtocolControl::SetPeerBandwidth {
window_size: 2_500_000,
limit_type: rtmp::bandwidth_limit::DYNAMIC,
};
let pcb = pc.to_body();
assert_eq!(pcb.len(), 5, "Set Peer Bandwidth body is 5 bytes");
assert_eq!(
ProtocolControl::parse(pc.message_type_id(), &pcb).unwrap(),
pc
);
}
#[test]
fn rtmp_demux_matches_flv_demux() {
const CHUNK: usize = 128;
let mut flv_demux = FlvDemux::new();
let flv_media = flv_demux.unpackage(FLV).expect("FLV → IR");
let tags = split_flv(FLV);
assert!(!tags.is_empty(), "av.flv has tags");
let mut messages = Vec::new();
let mut saw_multichunk_video = false;
for (tag_type, ts, body) in &tags {
let (csid, mt) = match *tag_type {
8 => (4u32, msg_type::AUDIO),
9 => (5u32, msg_type::VIDEO),
18 => (6u32, msg_type::DATA_AMF0),
_ => continue,
};
if mt == msg_type::VIDEO && body.len() > CHUNK {
saw_multichunk_video = true;
}
messages.push(Message {
csid,
message_type_id: mt,
message_stream_id: 1,
timestamp: *ts,
body: body.clone(),
});
}
assert!(
saw_multichunk_video,
"at least one video message spans multiple chunks at CHUNK=128"
);
let wire = write_chunks(&messages, CHUNK);
let mut rtmp_demux = RtmpDemux::new();
let rtmp_media = rtmp_demux.unpackage(&wire).expect("RTMP → IR");
assert_eq!(
rtmp_media.tracks.len(),
flv_media.tracks.len(),
"RTMP and FLV yield the same track count"
);
for (r, f) in rtmp_media.tracks.iter().zip(&flv_media.tracks) {
assert_eq!(
r.samples.len(),
f.samples.len(),
"sample count matches per track"
);
for (i, (rs, fs)) in r.samples.iter().zip(&f.samples).enumerate() {
assert_eq!(rs.data.len(), fs.data.len(), "sample {i} payload size");
assert_eq!(rs.data, fs.data, "sample {i} payload bytes");
assert_eq!(rs.flags.is_sync, fs.flags.is_sync, "sample {i} sync flag");
assert_eq!(
rs.composition_offset(),
fs.composition_offset(),
"sample {i} composition offset"
);
}
}
}
#[test]
fn rtmp_mux_round_trip_preserves_samples() {
let mut flv_demux = FlvDemux::new();
let media = flv_demux.unpackage(FLV).expect("FLV → IR");
let mut mux = RtmpMux::new();
let wire = mux.package(&media).expect("IR → RTMP wire");
assert!(!wire.is_empty(), "muxed RTMP wire is non-empty");
let mut demux = RtmpDemux::new();
let back = demux.unpackage(&wire).expect("RTMP wire → IR");
assert_eq!(
back.tracks.len(),
media.tracks.len(),
"track count preserved"
);
for (b, m) in back.tracks.iter().zip(&media.tracks) {
assert_eq!(b.samples.len(), m.samples.len(), "sample count preserved");
for (i, (bs, ms)) in b.samples.iter().zip(&m.samples).enumerate() {
assert_eq!(bs.data, ms.data, "sample {i} bytes preserved");
assert_eq!(
bs.flags.is_sync, ms.flags.is_sync,
"sample {i} sync flag preserved"
);
}
}
}
fn split_flv(flv: &[u8]) -> Vec<(u8, u32, Vec<u8>)> {
let data_offset = u32::from_be_bytes([flv[5], flv[6], flv[7], flv[8]]) as usize;
let mut off = data_offset.max(9) + 4;
let mut tags = Vec::new();
while off + 11 <= flv.len() {
let tag_type = flv[off];
let data_size = ((flv[off + 1] as usize) << 16)
| ((flv[off + 2] as usize) << 8)
| flv[off + 3] as usize;
let ts_lo =
((flv[off + 4] as u32) << 16) | ((flv[off + 5] as u32) << 8) | flv[off + 6] as u32;
let ts = ((flv[off + 7] as u32) << 24) | ts_lo;
let body_start = off + 11;
let body_end = body_start + data_size;
if body_end + 4 > flv.len() {
break;
}
tags.push((tag_type, ts, flv[body_start..body_end].to_vec()));
off = body_end + 4;
}
tags
}