use std::io::{self, Read, Write};
use crate::amf::{self, Amf0Value};
use crate::error::{Error, Result};
use crate::flv::{self, AudioTag, VideoTag};
pub const FLV_TAG_TYPE_AUDIO: u8 = 8;
pub const FLV_TAG_TYPE_VIDEO: u8 = 9;
pub const FLV_TAG_TYPE_SCRIPT_DATA: u8 = 18;
pub const FLV_VERSION: u8 = 1;
pub const FLV_HEADER_SIZE: u32 = 9;
const UI24_MAX: u32 = 0x00FF_FFFF;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FlvHeaderFlags {
pub audio: bool,
pub video: bool,
}
impl FlvHeaderFlags {
pub fn to_byte(self) -> u8 {
(if self.audio { 0x04 } else { 0 }) | (if self.video { 0x01 } else { 0 })
}
pub fn from_byte(b: u8) -> Self {
Self {
audio: b & 0x04 != 0,
video: b & 0x01 != 0,
}
}
}
pub fn build_flv_header(flags: FlvHeaderFlags) -> [u8; 9] {
let mut out = [0u8; 9];
out[0] = b'F';
out[1] = b'L';
out[2] = b'V';
out[3] = FLV_VERSION;
out[4] = flags.to_byte();
out[5..9].copy_from_slice(&FLV_HEADER_SIZE.to_be_bytes());
out
}
pub fn build_flv_tag(tag_type: u8, timestamp_ms: u32, payload: &[u8]) -> io::Result<Vec<u8>> {
if payload.len() > UI24_MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"flv tag payload {} bytes exceeds UI24 max {}",
payload.len(),
UI24_MAX
),
));
}
let data_size = payload.len() as u32;
let mut out = Vec::with_capacity(11 + payload.len());
out.push(tag_type);
out.push(((data_size >> 16) & 0xFF) as u8);
out.push(((data_size >> 8) & 0xFF) as u8);
out.push((data_size & 0xFF) as u8);
out.push(((timestamp_ms >> 16) & 0xFF) as u8);
out.push(((timestamp_ms >> 8) & 0xFF) as u8);
out.push((timestamp_ms & 0xFF) as u8);
out.push(((timestamp_ms >> 24) & 0xFF) as u8);
out.push(0);
out.push(0);
out.push(0);
out.extend_from_slice(payload);
Ok(out)
}
pub struct FlvWriter<W: Write> {
inner: W,
prev_tag_size: u32,
bytes_written: u64,
flags: FlvHeaderFlags,
finished: bool,
}
impl<W: Write> FlvWriter<W> {
pub fn new(mut inner: W, flags: FlvHeaderFlags) -> io::Result<Self> {
let header = build_flv_header(flags);
inner.write_all(&header)?;
inner.write_all(&0u32.to_be_bytes())?;
Ok(Self {
inner,
prev_tag_size: 0,
bytes_written: header.len() as u64 + 4,
flags,
finished: false,
})
}
pub fn flags(&self) -> FlvHeaderFlags {
self.flags
}
pub fn bytes_written(&self) -> u64 {
self.bytes_written
}
pub fn last_tag_size(&self) -> u32 {
self.prev_tag_size
}
pub fn get_ref(&self) -> &W {
&self.inner
}
pub fn write_video_tag(&mut self, timestamp_ms: u32, tag: &VideoTag) -> io::Result<()> {
self.write_payload(FLV_TAG_TYPE_VIDEO, timestamp_ms, &flv::build_video(tag))
}
pub fn write_audio_tag(&mut self, timestamp_ms: u32, tag: &AudioTag) -> io::Result<()> {
self.write_payload(FLV_TAG_TYPE_AUDIO, timestamp_ms, &flv::build_audio(tag))
}
pub fn write_script_data(
&mut self,
timestamp_ms: u32,
name: &str,
value: &Amf0Value,
) -> io::Result<()> {
let mut payload = Vec::with_capacity(32);
amf::encode(&mut payload, &Amf0Value::String(name.to_owned()));
amf::encode(&mut payload, value);
self.write_payload(FLV_TAG_TYPE_SCRIPT_DATA, timestamp_ms, &payload)
}
pub fn write_raw_tag(
&mut self,
tag_type: u8,
timestamp_ms: u32,
payload: &[u8],
) -> io::Result<()> {
self.write_payload(tag_type, timestamp_ms, payload)
}
fn write_payload(&mut self, tag_type: u8, timestamp_ms: u32, payload: &[u8]) -> io::Result<()> {
if self.finished {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"FlvWriter::write_* called after finish()",
));
}
let tag = build_flv_tag(tag_type, timestamp_ms, payload)?;
let tag_size = tag.len() as u32;
self.inner.write_all(&tag)?;
self.inner.write_all(&tag_size.to_be_bytes())?;
self.prev_tag_size = tag_size;
self.bytes_written += u64::from(tag_size) + 4;
Ok(())
}
pub fn finish(mut self) -> io::Result<W> {
if !self.finished {
self.inner.flush()?;
self.finished = true;
}
Ok(self.inner)
}
}
pub const DEFAULT_MAX_TAG_SIZE: u32 = UI24_MAX;
#[derive(Debug, Clone, PartialEq)]
pub enum FlvTag {
Audio { timestamp_ms: u32, tag: AudioTag },
Video { timestamp_ms: u32, tag: VideoTag },
Script {
timestamp_ms: u32,
name: String,
value: Amf0Value,
},
Unknown {
tag_type: u8,
timestamp_ms: u32,
body: Vec<u8>,
},
}
impl FlvTag {
pub fn tag_type(&self) -> u8 {
match self {
FlvTag::Audio { .. } => FLV_TAG_TYPE_AUDIO,
FlvTag::Video { .. } => FLV_TAG_TYPE_VIDEO,
FlvTag::Script { .. } => FLV_TAG_TYPE_SCRIPT_DATA,
FlvTag::Unknown { tag_type, .. } => *tag_type,
}
}
pub fn timestamp_ms(&self) -> u32 {
match self {
FlvTag::Audio { timestamp_ms, .. }
| FlvTag::Video { timestamp_ms, .. }
| FlvTag::Script { timestamp_ms, .. }
| FlvTag::Unknown { timestamp_ms, .. } => *timestamp_ms,
}
}
}
#[derive(Debug)]
pub struct FlvReader<R: Read> {
inner: R,
flags: FlvHeaderFlags,
max_tag_size: u32,
last_tag_size: u32,
exhausted: bool,
bytes_read: u64,
}
impl<R: Read> FlvReader<R> {
pub fn new(inner: R) -> Result<Self> {
Self::with_max_tag_size(inner, DEFAULT_MAX_TAG_SIZE)
}
pub fn with_max_tag_size(mut inner: R, max_tag_size: u32) -> Result<Self> {
let mut header = [0u8; 9];
read_exact_eof(&mut inner, &mut header)?;
if header[0] != b'F' || header[1] != b'L' || header[2] != b'V' {
return Err(Error::Other(format!(
"FLV: bad signature {:02x} {:02x} {:02x}, want 'F' 'L' 'V'",
header[0], header[1], header[2],
)));
}
if header[3] != FLV_VERSION {
return Err(Error::Other(format!(
"FLV: unsupported version {:#x}, only {:#x} (version 1) is defined",
header[3], FLV_VERSION,
)));
}
let flags = FlvHeaderFlags::from_byte(header[4]);
let data_offset = u32::from_be_bytes([header[5], header[6], header[7], header[8]]);
if data_offset < FLV_HEADER_SIZE {
return Err(Error::Other(format!(
"FLV: DataOffset {data_offset} < header size {FLV_HEADER_SIZE}"
)));
}
let mut bytes_read = u64::from(FLV_HEADER_SIZE);
if data_offset > FLV_HEADER_SIZE {
let extra = (data_offset - FLV_HEADER_SIZE) as usize;
let mut skip = vec![0u8; extra];
read_exact_eof(&mut inner, &mut skip)?;
bytes_read += extra as u64;
}
let mut p0 = [0u8; 4];
read_exact_eof(&mut inner, &mut p0)?;
let prev0 = u32::from_be_bytes(p0);
if prev0 != 0 {
return Err(Error::Other(format!(
"FLV: PreviousTagSize0 must be 0, got {prev0}"
)));
}
bytes_read += 4;
let cap = max_tag_size.min(UI24_MAX);
Ok(Self {
inner,
flags,
max_tag_size: cap,
last_tag_size: 0,
exhausted: false,
bytes_read,
})
}
pub fn flags(&self) -> FlvHeaderFlags {
self.flags
}
pub fn max_tag_size(&self) -> u32 {
self.max_tag_size
}
pub fn bytes_read(&self) -> u64 {
self.bytes_read
}
pub fn last_tag_size(&self) -> u32 {
self.last_tag_size
}
pub fn get_ref(&self) -> &R {
&self.inner
}
pub fn read_tag(&mut self) -> Result<Option<FlvTag>> {
if self.exhausted {
return Ok(None);
}
let mut header = [0u8; 11];
match self.inner.read(&mut header[..1]) {
Ok(0) => {
self.exhausted = true;
return Ok(None);
}
Ok(1) => {}
Ok(_) => unreachable!("read into 1-byte slice returned > 1"),
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => {
self.exhausted = true;
return Ok(None);
}
Err(e) => return Err(Error::Io(e)),
}
read_exact_eof(&mut self.inner, &mut header[1..])?;
self.bytes_read += 11;
let raw_type = header[0];
let filter = (raw_type >> 5) & 0x01;
if filter != 0 {
return Err(Error::Other(
"FLV: encrypted tag (Filter=1, Annex F) — decryption not implemented".into(),
));
}
let tag_type = raw_type & 0x1F;
let data_size = ((header[1] as u32) << 16) | ((header[2] as u32) << 8) | header[3] as u32;
if data_size > self.max_tag_size {
return Err(Error::Other(format!(
"FLV: DataSize {data_size} exceeds max_tag_size {}",
self.max_tag_size
)));
}
let ts_lo = ((header[4] as u32) << 16) | ((header[5] as u32) << 8) | header[6] as u32;
let ts_hi = header[7] as u32;
let timestamp_ms = (ts_hi << 24) | ts_lo;
let stream_id = ((header[8] as u32) << 16) | ((header[9] as u32) << 8) | header[10] as u32;
if stream_id != 0 {
return Err(Error::Other(format!(
"FLV: StreamID {stream_id} != 0 (§E.4.1 'Always 0')"
)));
}
let mut body = vec![0u8; data_size as usize];
read_exact_eof(&mut self.inner, &mut body)?;
self.bytes_read += data_size as u64;
let mut prev = [0u8; 4];
read_exact_eof(&mut self.inner, &mut prev)?;
let prev_tag_size = u32::from_be_bytes(prev);
let expected = 11u32.saturating_add(data_size);
if prev_tag_size != expected {
return Err(Error::Other(format!(
"FLV: PreviousTagSize {prev_tag_size} != 11 + DataSize {expected} (§E.3)"
)));
}
self.bytes_read += 4;
self.last_tag_size = expected;
let decoded = match tag_type {
FLV_TAG_TYPE_AUDIO => {
let tag = flv::parse_audio(&body)
.map_err(|e| Error::Other(format!("FLV audio body: {e}")))?;
FlvTag::Audio { timestamp_ms, tag }
}
FLV_TAG_TYPE_VIDEO => {
let tag = flv::parse_video(&body)
.map_err(|e| Error::Other(format!("FLV video body: {e}")))?;
FlvTag::Video { timestamp_ms, tag }
}
FLV_TAG_TYPE_SCRIPT_DATA => match parse_script_body(&body) {
Ok((name, value)) => FlvTag::Script {
timestamp_ms,
name,
value,
},
Err(_) => FlvTag::Unknown {
tag_type,
timestamp_ms,
body,
},
},
_ => FlvTag::Unknown {
tag_type,
timestamp_ms,
body,
},
};
Ok(Some(decoded))
}
pub fn read_all(mut self) -> Result<Vec<FlvTag>> {
let mut out = Vec::new();
while let Some(t) = self.read_tag()? {
out.push(t);
}
Ok(out)
}
}
fn parse_script_body(body: &[u8]) -> Result<(String, Amf0Value)> {
let mut pos = 0;
let name_v = amf::decode(body, &mut pos)?;
let name = match name_v {
Amf0Value::String(s) => s,
other => {
return Err(Error::InvalidAmf0(format!(
"script tag name must be String (§E.4.4), got {other:?}"
)));
}
};
let value = amf::decode(body, &mut pos)?;
while pos < body.len() {
if amf::decode(body, &mut pos).is_err() {
break;
}
}
Ok((name, value))
}
fn read_exact_eof<R: Read>(inner: &mut R, buf: &mut [u8]) -> Result<()> {
match inner.read_exact(buf) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Err(Error::UnexpectedEof),
Err(e) => Err(Error::Io(e)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flv::{
parse_audio, parse_video, AAC_PACKET_TYPE_RAW, AAC_PACKET_TYPE_SEQUENCE_HEADER,
AUDIO_FORMAT_AAC, AVC_PACKET_TYPE_NALU, AVC_PACKET_TYPE_SEQUENCE_HEADER,
EX_PACKET_TYPE_CODED_FRAMES, FOURCC_HEVC, VIDEO_CODEC_AVC, VIDEO_FRAME_INTER,
VIDEO_FRAME_KEYFRAME,
};
fn aac_seq_header_tag() -> AudioTag {
AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_AAC,
sound_rate: 3,
sound_size_16bit: true,
stereo: true,
aac_packet_type: Some(AAC_PACKET_TYPE_SEQUENCE_HEADER),
body: vec![0x12, 0x10],
ex_packet_type: None,
audio_fourcc: None,
multitrack: None,
}
}
fn aac_raw_tag(body: Vec<u8>) -> AudioTag {
AudioTag {
mod_ex: Vec::new(),
sound_format: AUDIO_FORMAT_AAC,
sound_rate: 3,
sound_size_16bit: true,
stereo: true,
aac_packet_type: Some(AAC_PACKET_TYPE_RAW),
body,
ex_packet_type: None,
audio_fourcc: None,
multitrack: None,
}
}
fn avc_seq_header_tag() -> VideoTag {
VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: VIDEO_CODEC_AVC,
avc_packet_type: Some(AVC_PACKET_TYPE_SEQUENCE_HEADER),
composition_time: 0,
body: vec![0x01, 0x42, 0x80, 0x1E],
ex_packet_type: None,
fourcc: None,
multitrack: None,
}
}
fn avc_inter_nalu_tag() -> VideoTag {
VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_INTER,
codec_id: VIDEO_CODEC_AVC,
avc_packet_type: Some(AVC_PACKET_TYPE_NALU),
composition_time: 7,
body: vec![0x00, 0x00, 0x00, 0x03, 0x41, 0x9A, 0x00],
ex_packet_type: None,
fourcc: None,
multitrack: None,
}
}
#[test]
fn header_flags_round_trip_through_byte() {
for a in [false, true] {
for v in [false, true] {
let f = FlvHeaderFlags { audio: a, video: v };
assert_eq!(FlvHeaderFlags::from_byte(f.to_byte()), f);
}
}
assert_eq!(
FlvHeaderFlags::from_byte(0xFF),
FlvHeaderFlags {
audio: true,
video: true
}
);
assert_eq!(
FlvHeaderFlags::from_byte(0xFE),
FlvHeaderFlags {
audio: true,
video: false
}
);
}
#[test]
fn build_flv_header_signature_and_offset() {
let header = build_flv_header(FlvHeaderFlags {
audio: true,
video: true,
});
assert_eq!(header, [b'F', b'L', b'V', 0x01, 0x05, 0, 0, 0, 9]);
}
#[test]
fn build_flv_header_video_only() {
let header = build_flv_header(FlvHeaderFlags {
audio: false,
video: true,
});
assert_eq!(header, [b'F', b'L', b'V', 0x01, 0x01, 0, 0, 0, 9]);
}
#[test]
fn build_flv_tag_layout_matches_spec() {
let body = b"abc".to_vec();
let tag = build_flv_tag(FLV_TAG_TYPE_VIDEO, 0x12_3456, &body).expect("build");
assert_eq!(
tag,
vec![
0x09, 0x00, 0x00, 0x03, 0x12, 0x34, 0x56, 0x00, 0x00, 0x00, 0x00, b'a', b'b', b'c',
]
);
}
#[test]
fn build_flv_tag_timestamp_extended_carries_high_byte() {
let tag = build_flv_tag(FLV_TAG_TYPE_AUDIO, 0x0ABB_CCDD, &[]).expect("build");
assert_eq!(tag[0], 0x08); assert_eq!(&tag[1..4], &[0, 0, 0]); assert_eq!(&tag[4..7], &[0xBB, 0xCC, 0xDD]); assert_eq!(tag[7], 0x0A); }
#[test]
fn build_flv_tag_rejects_payload_over_ui24() {
let huge = vec![0u8; (UI24_MAX as usize) + 1];
let err = build_flv_tag(FLV_TAG_TYPE_AUDIO, 0, &huge).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn writer_emits_header_and_previous_tag_size0() {
let buf: Vec<u8> = Vec::new();
let w = FlvWriter::new(
buf,
FlvHeaderFlags {
audio: false,
video: true,
},
)
.expect("new");
let buf = w.finish().expect("finish");
assert_eq!(
buf,
vec![b'F', b'L', b'V', 0x01, 0x01, 0, 0, 0, 9, 0, 0, 0, 0]
);
}
#[test]
fn writer_one_video_tag_round_trips_with_back_pointer() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: false,
video: true,
},
)
.expect("new");
let tag = avc_seq_header_tag();
w.write_video_tag(0, &tag).expect("write");
let prev = w.last_tag_size();
let bytes_so_far = w.bytes_written();
let buf = w.finish().expect("finish");
assert_eq!(prev, 20);
assert_eq!(bytes_so_far, buf.len() as u64);
assert_eq!(buf.len(), 9 + 4 + 11 + 9 + 4);
assert_eq!(&buf[0..9], &[b'F', b'L', b'V', 0x01, 0x01, 0, 0, 0, 9]);
assert_eq!(&buf[9..13], &[0, 0, 0, 0]);
assert_eq!(buf[13], FLV_TAG_TYPE_VIDEO);
assert_eq!(&buf[14..17], &[0, 0, 9]); assert_eq!(&buf[17..20], &[0, 0, 0]); assert_eq!(buf[20], 0); assert_eq!(&buf[21..24], &[0, 0, 0]); assert_eq!(buf[24], 0x17);
assert_eq!(buf[25], 0x00);
assert_eq!(&buf[26..29], &[0, 0, 0]);
assert_eq!(&buf[29..33], &[0x01, 0x42, 0x80, 0x1E]);
assert_eq!(&buf[33..37], &20u32.to_be_bytes());
let parsed = parse_video(&buf[24..33]).expect("parse");
assert_eq!(parsed.frame_type, VIDEO_FRAME_KEYFRAME);
assert_eq!(parsed.codec_id, VIDEO_CODEC_AVC);
assert_eq!(
parsed.avc_packet_type,
Some(AVC_PACKET_TYPE_SEQUENCE_HEADER)
);
assert_eq!(parsed.body, vec![0x01, 0x42, 0x80, 0x1E]);
}
#[test]
fn writer_audio_aac_seq_header_round_trips() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: false,
},
)
.expect("new");
let tag = aac_seq_header_tag();
w.write_audio_tag(0, &tag).expect("write");
let buf = w.finish().expect("finish");
let body_start = 9 + 4 + 11;
let body_end = body_start + 4;
let parsed = parse_audio(&buf[body_start..body_end]).expect("parse");
assert_eq!(parsed.sound_format, AUDIO_FORMAT_AAC);
assert_eq!(
parsed.aac_packet_type,
Some(AAC_PACKET_TYPE_SEQUENCE_HEADER)
);
assert_eq!(parsed.body, vec![0x12, 0x10]);
assert_eq!(&buf[buf.len() - 4..], &15u32.to_be_bytes());
}
#[test]
fn writer_back_pointer_tracks_each_tag_independently() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: true,
},
)
.expect("new");
let v1 = avc_seq_header_tag(); let a1 = aac_raw_tag(vec![0xAA, 0xBB, 0xCC]); let v2 = avc_inter_nalu_tag(); w.write_video_tag(0, &v1).expect("v1");
assert_eq!(w.last_tag_size(), 20);
w.write_audio_tag(0, &a1).expect("a1");
assert_eq!(w.last_tag_size(), 16);
w.write_video_tag(33, &v2).expect("v2");
assert_eq!(w.last_tag_size(), 23);
let buf = w.finish().expect("finish");
assert_eq!(buf.len(), 84);
assert_eq!(&buf[9..13], &0u32.to_be_bytes());
assert_eq!(&buf[33..37], &20u32.to_be_bytes());
assert_eq!(&buf[53..57], &16u32.to_be_bytes());
assert_eq!(&buf[buf.len() - 4..], &23u32.to_be_bytes());
}
#[test]
fn writer_script_data_amf0_name_then_value() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: true,
},
)
.expect("new");
let meta = Amf0Value::EcmaArray(vec![
("width".into(), Amf0Value::Number(1280.0)),
("height".into(), Amf0Value::Number(720.0)),
("duration".into(), Amf0Value::Number(0.0)),
]);
w.write_script_data(0, "onMetaData", &meta)
.expect("write meta");
let buf = w.finish().expect("finish");
let body_start = 9 + 4 + 11;
let prev_size = u32::from_be_bytes(buf[buf.len() - 4..].try_into().unwrap());
let data_size = prev_size - 11;
let body = &buf[body_start..body_start + data_size as usize];
let mut pos = 0;
let name = amf::decode(body, &mut pos).expect("name");
let value = amf::decode(body, &mut pos).expect("value");
assert_eq!(name, Amf0Value::String("onMetaData".to_string()));
assert_eq!(value, meta);
assert_eq!(pos, body.len());
assert_eq!(buf[13], FLV_TAG_TYPE_SCRIPT_DATA);
}
#[test]
fn writer_timestamp_extended_round_trips_high_byte() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: false,
},
)
.expect("new");
let tag = aac_raw_tag(vec![0x11, 0x22]);
let ts: u32 = 0x0A_BBCCDD;
w.write_audio_tag(ts, &tag).expect("write");
let buf = w.finish().expect("finish");
let hdr_off = 9 + 4;
assert_eq!(&buf[hdr_off + 4..hdr_off + 7], &[0xBB, 0xCC, 0xDD]);
assert_eq!(buf[hdr_off + 7], 0x0A);
}
#[test]
fn writer_enhanced_rtmp_v2_video_round_trips() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 17,
body: b"NALU-payload".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_HEVC),
multitrack: None,
};
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: false,
video: true,
},
)
.expect("new");
w.write_video_tag(100, &tag).expect("write");
let buf = w.finish().expect("finish");
let body_start = 9 + 4 + 11;
let prev = u32::from_be_bytes(buf[buf.len() - 4..].try_into().unwrap());
let data_size = prev - 11;
let parsed = parse_video(&buf[body_start..body_start + data_size as usize]).expect("parse");
assert_eq!(parsed.fourcc, Some(FOURCC_HEVC));
assert_eq!(parsed.ex_packet_type, Some(EX_PACKET_TYPE_CODED_FRAMES));
assert_eq!(parsed.composition_time, 17);
assert_eq!(parsed.body, b"NALU-payload".to_vec());
}
#[test]
fn writer_finish_is_idempotent_but_rejects_further_writes() {
let mut w = FlvWriter::new(Vec::new(), FlvHeaderFlags::default()).expect("new");
let tag = aac_raw_tag(vec![0x00]);
w.write_audio_tag(0, &tag).expect("write");
let _buf = w.finish().expect("finish first time");
}
#[test]
fn writer_returns_broken_pipe_after_finish() {
struct Sink {
inner: Vec<u8>,
done: bool,
}
impl Write for Sink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.done {
return Err(io::Error::new(io::ErrorKind::BrokenPipe, "post-finish"));
}
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let sink = Sink {
inner: Vec::new(),
done: false,
};
let w = FlvWriter::new(sink, FlvHeaderFlags::default()).expect("new");
let mut w2 = w;
w2.finished = true;
let err = w2.write_audio_tag(0, &aac_raw_tag(vec![])).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::BrokenPipe);
}
#[test]
fn writer_raw_tag_lets_caller_pass_their_own_payload() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: false,
video: true,
},
)
.expect("new");
w.write_raw_tag(FLV_TAG_TYPE_VIDEO, 5, &[0x99, 0x88, 0x77])
.expect("write");
let buf = w.finish().expect("finish");
assert_eq!(&buf[24..27], &[0x99, 0x88, 0x77]);
assert_eq!(&buf[buf.len() - 4..], &14u32.to_be_bytes());
}
use std::io::Cursor;
#[test]
fn reader_empty_stream_round_trips_through_writer() {
let w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: false,
},
)
.expect("new");
let buf = w.finish().expect("finish");
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
assert_eq!(
r.flags(),
FlvHeaderFlags {
audio: true,
video: false
}
);
assert_eq!(r.bytes_read(), 13); assert!(r.read_tag().expect("read").is_none());
assert!(r.read_tag().expect("read again").is_none());
}
#[test]
fn reader_rejects_bad_signature() {
let buf = vec![b'X', b'L', b'V', 1, 0x05, 0, 0, 0, 9, 0, 0, 0, 0];
let err = FlvReader::new(Cursor::new(buf)).unwrap_err();
assert!(
matches!(err, Error::Other(ref m) if m.contains("bad signature")),
"got {err:?}"
);
}
#[test]
fn reader_rejects_wrong_version() {
let buf = vec![b'F', b'L', b'V', 9, 0x05, 0, 0, 0, 9, 0, 0, 0, 0];
let err = FlvReader::new(Cursor::new(buf)).unwrap_err();
assert!(
matches!(err, Error::Other(ref m) if m.contains("unsupported version")),
"got {err:?}"
);
}
#[test]
fn reader_rejects_nonzero_previous_tag_size_0() {
let buf = vec![b'F', b'L', b'V', 1, 0x05, 0, 0, 0, 9, 0, 0, 0, 0x42];
let err = FlvReader::new(Cursor::new(buf)).unwrap_err();
assert!(
matches!(err, Error::Other(ref m) if m.contains("PreviousTagSize0")),
"got {err:?}"
);
}
#[test]
fn reader_rejects_data_offset_below_header_size() {
let buf = vec![b'F', b'L', b'V', 1, 0x05, 0, 0, 0, 8, 0, 0, 0, 0];
let err = FlvReader::new(Cursor::new(buf)).unwrap_err();
assert!(
matches!(err, Error::Other(ref m) if m.contains("DataOffset")),
"got {err:?}"
);
}
#[test]
fn reader_skips_forward_compatible_header_padding() {
let mut buf = vec![b'F', b'L', b'V', 1, 0x01, 0, 0, 0, 11];
buf.extend_from_slice(&[0xAA, 0xBB]); buf.extend_from_slice(&0u32.to_be_bytes()); let r = FlvReader::new(Cursor::new(buf)).expect("new");
assert_eq!(r.bytes_read(), 11 + 4);
assert_eq!(
r.flags(),
FlvHeaderFlags {
audio: false,
video: true
}
);
}
#[test]
fn reader_avc_seq_header_round_trips_through_writer() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: false,
video: true,
},
)
.expect("new");
w.write_video_tag(0, &avc_seq_header_tag()).expect("write");
let buf = w.finish().expect("finish");
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let tag = r.read_tag().expect("read").expect("Some");
match tag {
FlvTag::Video { timestamp_ms, tag } => {
assert_eq!(timestamp_ms, 0);
assert!(tag.is_avc_sequence_header());
assert_eq!(tag.body, vec![0x01, 0x42, 0x80, 0x1E]);
}
other => panic!("expected Video, got {other:?}"),
}
assert_eq!(r.last_tag_size(), 20);
assert!(r.read_tag().expect("end").is_none());
}
#[test]
fn reader_aac_seq_header_round_trips_through_writer() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: false,
},
)
.expect("new");
w.write_audio_tag(0, &aac_seq_header_tag()).expect("write");
let buf = w.finish().expect("finish");
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let tag = r.read_tag().expect("read").expect("Some");
match tag {
FlvTag::Audio { timestamp_ms, tag } => {
assert_eq!(timestamp_ms, 0);
assert_eq!(tag.sound_format, AUDIO_FORMAT_AAC);
assert_eq!(tag.aac_packet_type, Some(AAC_PACKET_TYPE_SEQUENCE_HEADER));
assert_eq!(tag.body, vec![0x12, 0x10]);
}
other => panic!("expected Audio, got {other:?}"),
}
assert!(r.read_tag().expect("end").is_none());
}
#[test]
fn reader_walks_interleaved_video_audio_video() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: true,
},
)
.expect("new");
w.write_video_tag(0, &avc_seq_header_tag()).expect("v1");
w.write_audio_tag(7, &aac_raw_tag(vec![0xAA, 0xBB, 0xCC]))
.expect("a1");
w.write_video_tag(33, &avc_inter_nalu_tag()).expect("v2");
let buf = w.finish().expect("finish");
let r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let tags = r.read_all().expect("read_all");
assert_eq!(tags.len(), 3);
match &tags[0] {
FlvTag::Video { timestamp_ms, tag } => {
assert_eq!(*timestamp_ms, 0);
assert!(tag.is_avc_sequence_header());
}
other => panic!("tag 0: {other:?}"),
}
match &tags[1] {
FlvTag::Audio { timestamp_ms, tag } => {
assert_eq!(*timestamp_ms, 7);
assert_eq!(tag.sound_format, AUDIO_FORMAT_AAC);
assert_eq!(tag.aac_packet_type, Some(AAC_PACKET_TYPE_RAW));
assert_eq!(tag.body, vec![0xAA, 0xBB, 0xCC]);
}
other => panic!("tag 1: {other:?}"),
}
match &tags[2] {
FlvTag::Video { timestamp_ms, tag } => {
assert_eq!(*timestamp_ms, 33);
assert_eq!(tag.frame_type, VIDEO_FRAME_INTER);
assert_eq!(tag.composition_time, 7);
}
other => panic!("tag 2: {other:?}"),
}
}
#[test]
fn reader_script_tag_round_trips_amf0_name_and_value() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: true,
},
)
.expect("new");
let meta = Amf0Value::EcmaArray(vec![
("width".into(), Amf0Value::Number(1920.0)),
("height".into(), Amf0Value::Number(1080.0)),
("framerate".into(), Amf0Value::Number(30.0)),
]);
w.write_script_data(0, "onMetaData", &meta).expect("write");
let buf = w.finish().expect("finish");
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let tag = r.read_tag().expect("read").expect("Some");
match tag {
FlvTag::Script {
timestamp_ms,
name,
value,
} => {
assert_eq!(timestamp_ms, 0);
assert_eq!(name, "onMetaData");
assert_eq!(value, meta);
}
other => panic!("expected Script, got {other:?}"),
}
}
#[test]
fn reader_timestamp_extended_high_byte_round_trips() {
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: false,
},
)
.expect("new");
let ts: u32 = 0x0A_BBCCDD;
w.write_audio_tag(ts, &aac_raw_tag(vec![0x11, 0x22]))
.expect("write");
let buf = w.finish().expect("finish");
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let tag = r.read_tag().expect("read").expect("Some");
assert_eq!(tag.timestamp_ms(), ts);
assert_eq!(tag.tag_type(), FLV_TAG_TYPE_AUDIO);
}
#[test]
fn reader_enhanced_rtmp_v2_video_round_trips() {
let tag = VideoTag {
mod_ex: Vec::new(),
frame_type: VIDEO_FRAME_KEYFRAME,
codec_id: 0,
avc_packet_type: None,
composition_time: 42,
body: b"NALU-payload".to_vec(),
ex_packet_type: Some(EX_PACKET_TYPE_CODED_FRAMES),
fourcc: Some(FOURCC_HEVC),
multitrack: None,
};
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: false,
video: true,
},
)
.expect("new");
w.write_video_tag(123, &tag).expect("write");
let buf = w.finish().expect("finish");
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let read = r.read_tag().expect("read").expect("Some");
match read {
FlvTag::Video { timestamp_ms, tag } => {
assert_eq!(timestamp_ms, 123);
assert_eq!(tag.fourcc, Some(FOURCC_HEVC));
assert_eq!(tag.ex_packet_type, Some(EX_PACKET_TYPE_CODED_FRAMES));
assert_eq!(tag.composition_time, 42);
assert_eq!(tag.body, b"NALU-payload".to_vec());
}
other => panic!("expected Video, got {other:?}"),
}
}
#[test]
fn reader_rejects_corrupt_previous_tag_size() {
let header = build_flv_header(FlvHeaderFlags {
audio: false,
video: true,
});
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&header);
buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&[
9, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, ]);
buf.extend_from_slice(&[0x17, 0x01, 0x00]);
buf.extend_from_slice(&99u32.to_be_bytes());
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let err = r.read_tag().unwrap_err();
assert!(
matches!(err, Error::Other(ref m) if m.contains("PreviousTagSize")),
"got {err:?}"
);
}
#[test]
fn reader_rejects_data_size_above_cap() {
let header = build_flv_header(FlvHeaderFlags {
audio: true,
video: false,
});
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&header);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[
8, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0,
]);
let mut r = FlvReader::with_max_tag_size(Cursor::new(buf), 100).expect("reader new");
assert_eq!(r.max_tag_size(), 100);
let err = r.read_tag().unwrap_err();
assert!(
matches!(err, Error::Other(ref m) if m.contains("exceeds max_tag_size")),
"got {err:?}"
);
}
#[test]
fn reader_rejects_nonzero_stream_id() {
let header = build_flv_header(FlvHeaderFlags {
audio: true,
video: false,
});
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&header);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[
8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, ]);
buf.extend_from_slice(&11u32.to_be_bytes());
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let err = r.read_tag().unwrap_err();
assert!(
matches!(err, Error::Other(ref m) if m.contains("StreamID")),
"got {err:?}"
);
}
#[test]
fn reader_rejects_encrypted_filter_bit() {
let header = build_flv_header(FlvHeaderFlags {
audio: false,
video: true,
});
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&header);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[
0x29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]);
buf.extend_from_slice(&11u32.to_be_bytes());
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let err = r.read_tag().unwrap_err();
assert!(
matches!(err, Error::Other(ref m) if m.contains("encrypted")),
"got {err:?}"
);
}
#[test]
fn reader_truncated_tag_header_surfaces_unexpected_eof() {
let header = build_flv_header(FlvHeaderFlags {
audio: true,
video: true,
});
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&header);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[8, 0, 0, 5]);
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let err = r.read_tag().unwrap_err();
assert!(matches!(err, Error::UnexpectedEof), "got {err:?}");
}
#[test]
fn reader_truncated_payload_surfaces_unexpected_eof() {
let header = build_flv_header(FlvHeaderFlags {
audio: true,
video: false,
});
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&header);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[
8, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0,
]);
buf.extend_from_slice(&[0xAA, 0xBB]);
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let err = r.read_tag().unwrap_err();
assert!(matches!(err, Error::UnexpectedEof), "got {err:?}");
}
#[test]
fn reader_unknown_tag_type_preserved_verbatim() {
let header = build_flv_header(FlvHeaderFlags {
audio: false,
video: false,
});
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&header);
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[
5, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, ]);
buf.extend_from_slice(&[0xDE, 0xAD, 0xBE]);
buf.extend_from_slice(&14u32.to_be_bytes());
let mut r = FlvReader::new(Cursor::new(buf)).expect("reader new");
let tag = r.read_tag().expect("read").expect("Some");
match tag {
FlvTag::Unknown {
tag_type,
timestamp_ms,
body,
} => {
assert_eq!(tag_type, 5);
assert_eq!(timestamp_ms, 0);
assert_eq!(body, vec![0xDE, 0xAD, 0xBE]);
}
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn reader_full_writer_round_trip_byte_for_byte() {
let meta = Amf0Value::EcmaArray(vec![
("encoder".into(), Amf0Value::String("oxideav-rtmp".into())),
("hasAudio".into(), Amf0Value::Boolean(true)),
]);
let mut w = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: true,
},
)
.expect("new");
w.write_script_data(0, "onMetaData", &meta).expect("meta");
w.write_video_tag(0, &avc_seq_header_tag()).expect("v0");
w.write_audio_tag(0, &aac_seq_header_tag()).expect("a0");
w.write_video_tag(100, &avc_inter_nalu_tag()).expect("v1");
let original = w.finish().expect("finish");
let r = FlvReader::new(Cursor::new(original.clone())).expect("reader");
let tags = r.read_all().expect("read_all");
assert_eq!(tags.len(), 4);
let mut w2 = FlvWriter::new(
Vec::new(),
FlvHeaderFlags {
audio: true,
video: true,
},
)
.expect("new2");
for t in &tags {
match t {
FlvTag::Script {
timestamp_ms,
name,
value,
} => w2.write_script_data(*timestamp_ms, name, value).unwrap(),
FlvTag::Video { timestamp_ms, tag } => {
w2.write_video_tag(*timestamp_ms, tag).unwrap()
}
FlvTag::Audio { timestamp_ms, tag } => {
w2.write_audio_tag(*timestamp_ms, tag).unwrap()
}
FlvTag::Unknown {
tag_type,
timestamp_ms,
body,
} => w2.write_raw_tag(*tag_type, *timestamp_ms, body).unwrap(),
}
}
let re = w2.finish().expect("finish2");
assert_eq!(re, original);
}
}