librtmp2 0.4.1

librtmp2 — RTMP/RTMPS protocol library
Documentation
//! Init-frame classification and Frame population for the relay hot path.

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};

/// How a relayed media frame should be treated by the init-frame cache.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheFrameKind {
    LiveOnly,
    VideoSequenceHeader,
    AudioSequenceHeader,
    VideoKeyframe,
}

/// Classify a publisher media frame for init-cache storage.
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
    }
}

/// Fill codec/header fields on a [`Frame`] from raw RTMP A/V payload bytes.
/// Fill codec/header fields for a per-track callback extracted from an E-RTMP
/// multitrack container. Inner track payloads do not include the enhanced tag
/// header or FourCC, so metadata comes from the track descriptor.
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,
    }
}

/// Returns true when `payload` is an AMF0 onMetaData (or @setDataFrame/onMetaData) message.
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
        );
    }
}