diff --git a/src/format/mod.rs b/src/format/mod.rs index 911779222..499084ab0 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -2,6 +2,7 @@ // These really don't belong anywhere, but I guess they're kind of related // to codecs etc. +pub use crate::packet::{detect_av1_keyframe, detect_vp8_keyframe, detect_vp9_keyframe}; pub use crate::packet::{CodecExtra, H264CodecExtra, Vp8CodecExtra, Vp9CodecExtra}; mod codec; diff --git a/src/packet/av1.rs b/src/packet/av1.rs index f6c26dd2f..d38fd4fc4 100644 --- a/src/packet/av1.rs +++ b/src/packet/av1.rs @@ -6,6 +6,21 @@ const OBU_TYPE_MASK: u8 = 0b0111_1000; const AGGREGATION_HEADER_SIZE: usize = 1; const MAX_NUM_OBUS_TO_OMTI_SIZE: usize = 3; +/// Detect whether an AV1 RTP payload contains a keyframe. +/// +/// Checks the N bit (new coded video sequence) in the AV1 aggregation header. +/// N=1 indicates the first packet of a keyframe (random access point). +/// +/// AV1 aggregation header layout: `Z|Y|W W|N|reserved` +/// - N (bit 3): 1 = new coded video sequence starts +pub fn detect_av1_keyframe(payload: &[u8]) -> bool { + if payload.is_empty() { + return false; + } + // N bit is bit 3 of the aggregation header + payload[0] & 0x08 != 0 +} + /// AV1 information describing the depacketized / packetized data #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Av1CodecExtra { @@ -1203,4 +1218,26 @@ mod test { ); } } + + #[test] + fn test_detect_av1_keyframe() { + // Empty + assert!(!detect_av1_keyframe(&[])); + + // AV1 aggregation header: Z|Y|W W|N|reserved + // N bit is bit 3 (0x08) + + // N=1 → keyframe + assert!(detect_av1_keyframe(&[0x08])); + assert!(detect_av1_keyframe(&[0x18])); // Z=0,Y=0,W=01,N=1 + assert!(detect_av1_keyframe(&[0x78])); // Z=0,Y=1,W=11,N=1 + assert!(detect_av1_keyframe(&[0x88])); // Z=1,Y=0,W=00,N=1 + assert!(detect_av1_keyframe(&[0x0F])); // N=1, reserved bits set + + // N=0 → not a keyframe + assert!(!detect_av1_keyframe(&[0x00])); + assert!(!detect_av1_keyframe(&[0x10])); // W=01, N=0 + assert!(!detect_av1_keyframe(&[0x70])); // Y=1, W=11, N=0 + assert!(!detect_av1_keyframe(&[0xF0])); // Z=1, Y=1, W=11, N=0 + } } diff --git a/src/packet/mod.rs b/src/packet/mod.rs index 1e05efe30..13f6e6744 100644 --- a/src/packet/mod.rs +++ b/src/packet/mod.rs @@ -9,6 +9,7 @@ use crate::sdp::MediaType; use crate::rtp::vla::encode_leb_u63; mod av1; +pub use av1::detect_av1_keyframe; pub use av1::Av1CodecExtra; use av1::{Av1Depacketizer, Av1Packetizer}; @@ -34,10 +35,12 @@ mod opus; pub use opus::{OpusDepacketizer, OpusPacketizer}; mod vp8; +pub use vp8::detect_vp8_keyframe; pub use vp8::Vp8CodecExtra; pub use vp8::{Vp8Depacketizer, Vp8Packetizer}; mod vp9; +pub use vp9::detect_vp9_keyframe; pub use vp9::Vp9CodecExtra; use vp9::{Vp9Depacketizer, Vp9Packetizer}; diff --git a/src/packet/vp8.rs b/src/packet/vp8.rs index c9deaf62f..0eb68ce92 100644 --- a/src/packet/vp8.rs +++ b/src/packet/vp8.rs @@ -26,6 +26,61 @@ pub struct Vp8CodecExtra { pub is_keyframe: bool, } +/// Detect whether a VP8 RTP payload contains a keyframe. +/// +/// Parses the VP8 RTP payload descriptor (RFC 7741) to skip past the +/// variable-length header, then checks the P bit in the VP8 payload header. +/// P=0 means keyframe, P=1 means interframe. +/// +/// Returns `true` only for the first packet of a keyframe (S=1, PID=0). +pub fn detect_vp8_keyframe(payload: &[u8]) -> bool { + if payload.is_empty() { + return false; + } + let b0 = payload[0]; + let s = (b0 & 0x10) >> 4; // Start of VP8 partition + let pid = b0 & 0x07; // Partition index + // Only the first packet of a frame (S=1, PID=0) contains the payload header + if s != 1 || pid != 0 { + return false; + } + let x = (b0 & 0x80) >> 7; // Extension bit + let mut idx = 1; + if x == 1 { + if idx >= payload.len() { + return false; + } + let ext = payload[idx]; + idx += 1; + let i = (ext & 0x80) >> 7; // PictureID present + let l = (ext & 0x40) >> 6; // TL0PICIDX present + let t = (ext & 0x20) >> 5; // TID present + let k = (ext & 0x10) >> 4; // KEYIDX present + if i == 1 { + if idx >= payload.len() { + return false; + } + if payload[idx] & 0x80 != 0 { + idx += 2; // 16-bit PictureID + } else { + idx += 1; // 7-bit PictureID + } + } + if l == 1 { + idx += 1; // tl0picidx + } + if t == 1 || k == 1 { + idx += 1; // TID/KEYIDX + } + } + if idx >= payload.len() { + return false; + } + // VP8 Payload Header: P bit is bit 0 of the first byte + // P=0 → keyframe, P=1 → interframe + payload[idx] & 0x01 == 0 +} + /// Packetizes VP8 RTP packets. /// /// ## Unversioned API surface @@ -596,4 +651,58 @@ mod test { Ok(()) } + + #[test] + fn test_detect_vp8_keyframe() { + // Empty payload + assert!(!detect_vp8_keyframe(&[])); + + // Minimal keyframe: S=1, PID=0, no extensions, P=0 + // Byte 0: X=0, R=0, N=0, S=1, PID=0 → 0x10 + // Byte 1: VP8 payload header with P=0 (keyframe) → 0x00 + assert!(detect_vp8_keyframe(&[0x10, 0x00])); + + // Minimal interframe: S=1, PID=0, no extensions, P=1 + // Byte 1: VP8 payload header with P=1 → 0x01 + assert!(!detect_vp8_keyframe(&[0x10, 0x01])); + + // Not the first packet (S=0) — cannot detect keyframe + assert!(!detect_vp8_keyframe(&[0x00, 0x00])); + + // Continuation packet (PID != 0) + assert!(!detect_vp8_keyframe(&[0x11, 0x00])); + + // With extension (X=1), 7-bit PictureID, keyframe + // Byte 0: X=1, S=1, PID=0 → 0x90 + // Byte 1: I=1, L=0, T=0, K=0 → 0x80 + // Byte 2: 7-bit PictureID (M=0) → 0x42 + // Byte 3: VP8 payload header P=0 → 0x00 + assert!(detect_vp8_keyframe(&[0x90, 0x80, 0x42, 0x00])); + + // With extension, 7-bit PictureID, interframe + assert!(!detect_vp8_keyframe(&[0x90, 0x80, 0x42, 0x01])); + + // With extension, 16-bit PictureID (M=1), keyframe + // Byte 2: M=1 → 0x80 | PID_high + // Byte 3: PID_low + // Byte 4: VP8 payload header P=0 + assert!(detect_vp8_keyframe(&[0x90, 0x80, 0x80, 0x42, 0x00])); + + // With all extensions: I=1(16-bit), L=1, T=1 + // Byte 0: X=1, S=1 → 0x90 + // Byte 1: I=1, L=1, T=1 → 0xE0 + // Byte 2-3: 16-bit PictureID → 0x80, 0x42 + // Byte 4: TL0PICIDX + // Byte 5: TID/KEYIDX + // Byte 6: VP8 payload header P=0 + assert!(detect_vp8_keyframe(&[ + 0x90, 0xE0, 0x80, 0x42, 0x01, 0x00, 0x00 + ])); + + // Truncated: extension says PictureID but no bytes left + assert!(!detect_vp8_keyframe(&[0x90, 0x80])); + + // Truncated: header consumed all bytes + assert!(!detect_vp8_keyframe(&[0x90, 0x80, 0x42])); + } } diff --git a/src/packet/vp9.rs b/src/packet/vp9.rs index 05fb8943d..d7bd11d1c 100644 --- a/src/packet/vp9.rs +++ b/src/packet/vp9.rs @@ -100,6 +100,21 @@ pub struct Vp9CodecExtra { pub is_keyframe: bool, } +/// Detect whether a VP9 RTP payload contains a keyframe by inspecting the P bit. +/// +/// VP9 RTP descriptor byte 0: `I|P|L|F|B|E|V|Z` +/// - P=0: independently decodable frame (keyframe) +/// - P=1: inter-picture predicted frame +/// +/// Works with both flexible (F=1) and non-flexible (F=0) mode packets. +pub fn detect_vp9_keyframe(payload: &[u8]) -> bool { + if payload.is_empty() { + return false; + } + // P bit is bit 6 of byte 0. P=0 means keyframe. + (payload[0] & 0x40) == 0 +} + /// Packetizes VP9 RTP packets. #[derive(Default, Clone)] pub struct Vp9Packetizer { @@ -1046,4 +1061,26 @@ mod test { Ok(()) } + + #[test] + fn test_detect_vp9_keyframe() { + // Empty payload + assert!(!detect_vp9_keyframe(&[])); + + // VP9 RTP descriptor byte 0: I|P|L|F|B|E|V|Z + // P=0 (bit 6 clear) → keyframe + // P=1 (bit 6 set) → inter-frame + + // Keyframes (P=0) + assert!(detect_vp9_keyframe(&[0x80])); // I=1 + assert!(detect_vp9_keyframe(&[0xA0])); // I=1, L=1 + assert!(detect_vp9_keyframe(&[0xAE])); // I=1, L=1, B=1, E=1, V=1 + assert!(detect_vp9_keyframe(&[0x00])); // all flags clear + + // Inter-frames (P=1) + assert!(!detect_vp9_keyframe(&[0xE8])); // I=1, P=1, L=1, B=1 + assert!(!detect_vp9_keyframe(&[0xEC])); // I=1, P=1, L=1, B=1, E=1 + assert!(!detect_vp9_keyframe(&[0x40])); // P=1 only + assert!(!detect_vp9_keyframe(&[0xD5])); // I=1, P=1, F=1, E=1, Z=1 + } }