Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions src/packet/av1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
3 changes: 3 additions & 0 deletions src/packet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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};

Expand Down
109 changes: 109 additions & 0 deletions src/packet/vp8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]));
}
}
37 changes: 37 additions & 0 deletions src/packet/vp9.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
Loading