use crate::ertmp::multitrack_media::{
is_multitrack_container, multitrack_has_keyframe, multitrack_has_sequence_start,
};
use crate::ertmp::{exaudio, exvideo, fourcc};
use crate::types::{AudioCodec, AudioHeader, FourCc, Frame, FrameType, VideoCodec, VideoHeader};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheFrameKind {
LiveOnly,
VideoSequenceHeader,
AudioSequenceHeader,
VideoKeyframe,
}
pub fn classify_cache_frame(frame_type: FrameType, payload: &[u8]) -> CacheFrameKind {
match frame_type {
FrameType::Video => classify_video(payload),
FrameType::Audio => classify_audio(payload),
FrameType::Script | FrameType::Metadata => CacheFrameKind::LiveOnly,
}
}
fn classify_video(payload: &[u8]) -> CacheFrameKind {
if is_multitrack_container(FrameType::Video, payload) {
if multitrack_has_sequence_start(FrameType::Video, payload) {
return CacheFrameKind::VideoSequenceHeader;
}
if multitrack_has_keyframe(payload) {
return CacheFrameKind::VideoKeyframe;
}
return CacheFrameKind::LiveOnly;
}
let mut hdr = VideoHeader::default();
if exvideo::exvideo_parse(payload, &mut hdr).is_err() {
return CacheFrameKind::LiveOnly;
}
if hdr.is_ex_header != 0 {
return match hdr.packet_type {
0 => CacheFrameKind::VideoSequenceHeader,
1 | 3 if hdr.frame_type == 1 => CacheFrameKind::VideoKeyframe,
_ => CacheFrameKind::LiveOnly,
};
}
let codec_id = payload[0] & 0x0F;
let frame_type_nibble = (payload[0] >> 4) & 0x0F;
if codec_id == 7 && payload.len() >= 2 && payload[1] == 0 {
return CacheFrameKind::VideoSequenceHeader;
}
if frame_type_nibble == 1 {
return CacheFrameKind::VideoKeyframe;
}
CacheFrameKind::LiveOnly
}
fn classify_audio(payload: &[u8]) -> CacheFrameKind {
if is_multitrack_container(FrameType::Audio, payload)
&& multitrack_has_sequence_start(FrameType::Audio, payload)
{
return CacheFrameKind::AudioSequenceHeader;
}
let mut hdr = AudioHeader::default();
if exaudio::exaudio_parse(payload, &mut hdr).is_err() {
return CacheFrameKind::LiveOnly;
}
if hdr.is_ex_header != 0 {
if hdr.packet_type == 0 {
CacheFrameKind::AudioSequenceHeader
} else {
CacheFrameKind::LiveOnly
}
} else if hdr.audio_codec == AudioCodec::Aac && hdr.aac_packet_type == 0 {
CacheFrameKind::AudioSequenceHeader
} else {
CacheFrameKind::LiveOnly
}
}
pub fn populate_multitrack_frame(
frame: &mut Frame,
fourcc_value: [u8; 4],
packet_type: u8,
video_frame_type: u8,
) {
let mut cc = [0u8; 5];
cc[..4].copy_from_slice(&fourcc_value);
match frame.frame_type {
FrameType::Video => {
frame.video_fourcc = FourCc { cc };
frame.video_frame_type = video_frame_type;
if let Ok(codec) = fourcc::fourcc_to_video_codec(&fourcc_value) {
frame.video_codec = codec;
}
frame.is_metadata = u8::from(packet_type == crate::types::ERTMP_PACKET_TYPE_METADATA);
}
FrameType::Audio => {
frame.audio_fourcc = FourCc { cc };
if let Ok(codec) = fourcc::fourcc_to_audio_codec(&fourcc_value) {
frame.audio_codec = codec;
}
frame.is_metadata =
u8::from(packet_type == crate::types::ERTMP_AUDIO_PACKET_TYPE_METADATA);
}
FrameType::Script | FrameType::Metadata => {
frame.is_metadata = 1;
}
}
}
pub fn populate_av_frame(frame: &mut Frame, payload: &[u8]) {
match frame.frame_type {
FrameType::Video => populate_video_frame(frame, payload),
FrameType::Audio => populate_audio_frame(frame, payload),
FrameType::Script | FrameType::Metadata => {
frame.is_metadata = 1;
}
}
}
fn populate_video_frame(frame: &mut Frame, payload: &[u8]) {
let mut hdr = VideoHeader::default();
if exvideo::exvideo_parse(payload, &mut hdr).is_err() {
return;
}
frame.composition_time = hdr.composition_time;
frame.video_frame_type = hdr.frame_type;
frame.video_fourcc = FourCc { cc: hdr.fourcc };
frame.video_codec = if hdr.is_ex_header != 0 {
fourcc::fourcc_to_video_codec(&hdr.fourcc).unwrap_or(VideoCodec::H264)
} else {
legacy_video_codec(payload[0] & 0x0F)
};
}
fn populate_audio_frame(frame: &mut Frame, payload: &[u8]) {
let mut hdr = AudioHeader::default();
if exaudio::exaudio_parse(payload, &mut hdr).is_err() {
return;
}
frame.audio_codec = hdr.audio_codec;
frame.audio_sample_rate = hdr.sample_rate as u32;
frame.audio_channels = hdr.channels;
frame.audio_bit_depth = if hdr.sample_size != 0 { 16 } else { 8 };
frame.audio_fourcc = FourCc { cc: hdr.fourcc };
}
fn legacy_video_codec(codec_id: u8) -> VideoCodec {
match codec_id {
7 => VideoCodec::H264,
12 => VideoCodec::H265,
13 => VideoCodec::Av1,
14 => VideoCodec::Vp9,
_ => VideoCodec::H264,
}
}
pub fn is_on_metadata_payload(payload: &[u8]) -> bool {
let mut buf = crate::buffer::Buffer::from_slice(payload);
let first_byte = match buf.peek().first().copied() {
Some(b) => b,
None => return false,
};
if first_byte != crate::amf::amf0::Amf0Type::String as u8
&& first_byte != crate::amf::amf0::Amf0Type::LongString as u8
{
return false;
}
let mut name = [0u8; 64];
let name_len = match read_data_event_name(
&mut buf,
first_byte == crate::amf::amf0::Amf0Type::String as u8,
&mut name,
) {
Some(n) => n,
None => return false,
};
let name_str = std::str::from_utf8(&name[..name_len]).unwrap_or("");
if name_str == "@setDataFrame" {
let next_byte = match buf.peek().first().copied() {
Some(b) => b,
None => return false,
};
if next_byte != crate::amf::amf0::Amf0Type::String as u8
&& next_byte != crate::amf::amf0::Amf0Type::LongString as u8
{
return false;
}
let mut inner = [0u8; 64];
let inner_len = match read_data_event_name(
&mut buf,
next_byte == crate::amf::amf0::Amf0Type::String as u8,
&mut inner,
) {
Some(n) => n,
None => return false,
};
let inner_str = std::str::from_utf8(&inner[..inner_len]).unwrap_or("");
inner_str == "onMetaData"
} else {
name_str == "onMetaData"
}
}
fn read_data_event_name(
buf: &mut crate::buffer::Buffer,
is_string: bool,
out: &mut [u8; 64],
) -> Option<usize> {
match if is_string {
crate::amf::amf0::read_string(buf, out)
} else {
crate::amf::amf0::read_long_string(buf, out)
} {
Ok(n) => Some(n),
Err(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn multitrack_video_callback_metadata_uses_track_descriptor() {
let mut frame = Frame {
frame_type: FrameType::Video,
..Default::default()
};
populate_multitrack_frame(&mut frame, *b"hvc1", 1, 1);
assert_eq!(&frame.video_fourcc.cc[..4], b"hvc1");
assert_eq!(frame.video_codec, VideoCodec::H265);
assert_eq!(frame.video_frame_type, 1);
}
#[test]
fn multitrack_audio_callback_metadata_uses_track_descriptor() {
let mut frame = Frame {
frame_type: FrameType::Audio,
..Default::default()
};
populate_multitrack_frame(&mut frame, *b"Opus", 1, 0);
assert_eq!(&frame.audio_fourcc.cc[..4], b"Opus");
assert_eq!(frame.audio_codec, AudioCodec::Opus);
}
#[test]
fn enhanced_hevc_sequence_start_is_cached() {
let payload = [0x90, b'h', b'v', b'c', b'1', 0x01, 0x02];
assert_eq!(
classify_cache_frame(FrameType::Video, &payload),
CacheFrameKind::VideoSequenceHeader
);
}
#[test]
fn enhanced_av1_keyframe_is_cached() {
let payload = [0x91, b'a', b'v', b'0', b'1', 0xDE, 0xAD];
assert_eq!(
classify_cache_frame(FrameType::Video, &payload),
CacheFrameKind::VideoKeyframe
);
}
#[test]
fn enhanced_coded_frames_x_keyframe_is_cached() {
let payload = [0x93, b'a', b'v', b'c', b'1', 0xDE, 0xAD];
assert_eq!(
classify_cache_frame(FrameType::Video, &payload),
CacheFrameKind::VideoKeyframe
);
}
#[test]
fn enhanced_opus_sequence_start_is_cached() {
let payload = [0x90, b'O', b'p', b'u', b's'];
assert_eq!(
classify_cache_frame(FrameType::Audio, &payload),
CacheFrameKind::AudioSequenceHeader
);
}
#[test]
fn multitrack_video_sequence_start_is_cached() {
let payload = vec![
0x86, 0x10, b'a', b'v', b'c', b'1', 0x00, 0x00, 0x00, 0x03, 0xAA, 0xBB, 0xCC,
];
assert_eq!(
classify_cache_frame(FrameType::Video, &payload),
CacheFrameKind::VideoSequenceHeader
);
}
#[test]
fn multitrack_video_keyframe_is_cached_from_outer_header() {
let payload = vec![
0x96, 0x11, b'a', b'v', b'c', b'1', 0x00, 0x00, 0x00, 0x02, 0xDE, 0xAD,
];
assert_eq!(
classify_cache_frame(FrameType::Video, &payload),
CacheFrameKind::VideoKeyframe
);
}
#[test]
fn legacy_avc_header_still_cached() {
let payload = [0x17, 0x00, 0x01];
assert_eq!(
classify_cache_frame(FrameType::Video, &payload),
CacheFrameKind::VideoSequenceHeader
);
}
}