Skip to content

Commit 737b750

Browse files
fix(video-streamer): add codec-aware VP9 keyframe detection (#1702)
## Summary - Add VP9 keyframe detection alongside existing VP8 support, based on the VP9 bitstream specification (profiles 0-3) - Thread the `VpxCodec` type through the iterator and block tag layers so keyframe checks use the correct codec-specific logic - Set `VpxEncoderPreset::BestPerformance` on the re-encoding encoder for improved throughput during session shadowing --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1f8e7a0 commit 737b750

4 files changed

Lines changed: 164 additions & 18 deletions

File tree

crates/video-streamer/src/streamer/block_tag.rs

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::fmt;
22

33
use anyhow::Context;
4+
use cadeau::xmf::vpx::VpxCodec;
45
use webm_iterable::matroska_spec::{Block, Master, MatroskaSpec, SimpleBlock};
56

67
#[derive(Clone)]
@@ -35,7 +36,7 @@ impl fmt::Debug for VideoBlock {
3536
}
3637

3738
impl VideoBlock {
38-
pub(crate) fn new(tag: MatroskaSpec, cluster_timestamp: Option<u64>) -> anyhow::Result<Self> {
39+
pub(crate) fn new(tag: MatroskaSpec, cluster_timestamp: Option<u64>, codec: VpxCodec) -> anyhow::Result<Self> {
3940
let result = match tag {
4041
MatroskaSpec::BlockGroup(Master::Full(children)) => {
4142
let block = children
@@ -51,7 +52,10 @@ impl VideoBlock {
5152

5253
let block = Block::try_from(block)?;
5354
let timestamp = block.timestamp;
54-
let is_key_frame = block.read_frame_data()?.iter().any(|frame| is_key_frame(frame.data));
55+
let is_key_frame = block
56+
.read_frame_data()?
57+
.iter()
58+
.any(|frame| is_vpx_key_frame(frame.data, codec));
5559

5660
Self {
5761
cluster_timestamp,
@@ -121,9 +125,144 @@ impl VideoBlock {
121125
}
122126
}
123127

124-
fn is_key_frame(buffer: &[u8]) -> bool {
128+
pub(crate) fn is_vpx_key_frame(buffer: &[u8], codec: VpxCodec) -> bool {
129+
match codec {
130+
VpxCodec::VP8 => is_vp8_key_frame(buffer),
131+
VpxCodec::VP9 => is_vp9_key_frame(buffer),
132+
}
133+
}
134+
135+
/// VP8 keyframe detection.
136+
///
137+
/// RFC 6386 Section 9.1 "Uncompressed Data Chunk":
138+
/// https://datatracker.ietf.org/doc/html/rfc6386#section-9.1
139+
///
140+
/// First byte layout (LSB-first bitstream):
141+
/// bit 0: frame_type (0 = key frame, 1 = inter frame)
142+
/// bits 1-2: version
143+
/// bit 3: show_frame
144+
/// bits 4-7: first_part_size (bits 0-3)
145+
///
146+
/// We only need bit 0: `buffer[0] & 0x1 == 0` means keyframe.
147+
fn is_vp8_key_frame(buffer: &[u8]) -> bool {
125148
if buffer.is_empty() {
126149
return false;
127150
}
128151
buffer[0] & 0x1 == 0
129152
}
153+
154+
/// VP9 keyframe detection.
155+
///
156+
/// VP9 Bitstream & Decoding Process Specification v0.6, Section 6.2 "Uncompressed header syntax":
157+
/// https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf
158+
///
159+
/// Unlike VP8 which uses a LSB-first bitstream, VP9 uses a MSB-first bitstream.
160+
/// The first byte layout depends on the profile:
161+
///
162+
/// For profiles 0-2 (first byte, MSB to LSB):
163+
/// bits 7-6: frame_marker (must be 0b10 to identify VP9)
164+
/// bit 5: profile_low_bit
165+
/// bit 4: profile_high_bit
166+
/// bit 3: show_existing_frame (if 1, frame is a reference to an already-decoded frame, not a keyframe)
167+
/// bit 2: frame_type (0 = KEY_FRAME, 1 = NON_KEY_FRAME)
168+
/// bits 1-0: (remaining header fields)
169+
///
170+
/// For profile 3 (first byte, MSB to LSB):
171+
/// bits 7-6: frame_marker (must be 0b10)
172+
/// bit 5: profile_low_bit (1)
173+
/// bit 4: profile_high_bit (1)
174+
/// bit 3: reserved_zero
175+
/// bit 2: show_existing_frame
176+
/// bit 1: frame_type (0 = KEY_FRAME, 1 = NON_KEY_FRAME)
177+
/// bit 0: (remaining header fields)
178+
///
179+
/// Profile 3 has an extra reserved_zero bit after the profile bits, which shifts
180+
/// show_existing_frame and frame_type one position to the right.
181+
///
182+
/// Note: the profile is encoded with swapped bit order: `profile = (high_bit << 1) | low_bit`,
183+
/// i.e. `profile = (bit4 << 1) | bit5`.
184+
///
185+
/// A frame is a keyframe when: show_existing_frame == 0 AND frame_type == 0.
186+
pub(crate) fn is_vp9_key_frame(buffer: &[u8]) -> bool {
187+
if buffer.is_empty() {
188+
return false;
189+
}
190+
let b0 = buffer[0];
191+
192+
// Validate frame_marker (bits 7-6) is 0b10
193+
if (b0 >> 6) != 0b10 {
194+
return false;
195+
}
196+
197+
// profile = (high_bit << 1) | low_bit = (bit4 << 1) | bit5
198+
let profile = (((b0 >> 4) & 1) << 1) | ((b0 >> 5) & 1);
199+
200+
if profile == 3 {
201+
// Profile 3: show_existing_frame is bit 2, frame_type is bit 1
202+
(b0 & 0x04) == 0 && (b0 & 0x02) == 0
203+
} else {
204+
// Profiles 0-2: show_existing_frame is bit 3, frame_type is bit 2
205+
(b0 & 0x08) == 0 && (b0 & 0x04) == 0
206+
}
207+
}
208+
209+
#[cfg(test)]
210+
mod tests {
211+
use super::is_vp9_key_frame;
212+
213+
#[test]
214+
fn vp9_empty_buffer_is_not_keyframe() {
215+
assert!(!is_vp9_key_frame(&[]));
216+
}
217+
218+
#[test]
219+
fn vp9_marker_mismatch_is_not_keyframe() {
220+
// frame_marker (bits 7-6) != 0b10 → rejected even if other bits look like keyframe.
221+
// 0x00 → frame_marker = 0b00
222+
assert!(!is_vp9_key_frame(&[0x00]));
223+
}
224+
225+
#[test]
226+
fn vp9_profiles_0_to_2_keyframe_detected() {
227+
// Profile 0: frame_marker=0b10, profile_low=0, profile_high=0,
228+
// show_existing_frame(bit3)=0, frame_type(bit2)=0.
229+
// 0b1000_0000 = 0x80
230+
assert!(is_vp9_key_frame(&[0x80]));
231+
}
232+
233+
#[test]
234+
fn vp9_profiles_0_to_2_show_existing_frame_is_not_keyframe() {
235+
// Profile 0: show_existing_frame(bit3)=1, frame_type(bit2)=0.
236+
// 0b1000_1000 = 0x88
237+
assert!(!is_vp9_key_frame(&[0x88]));
238+
}
239+
240+
#[test]
241+
fn vp9_profiles_0_to_2_inter_frame_is_not_keyframe() {
242+
// Profile 0: show_existing_frame(bit3)=0, frame_type(bit2)=1.
243+
// 0b1000_0100 = 0x84
244+
assert!(!is_vp9_key_frame(&[0x84]));
245+
}
246+
247+
#[test]
248+
fn vp9_profile_3_keyframe_detected() {
249+
// Profile 3: frame_marker=0b10, profile_low(bit5)=1, profile_high(bit4)=1,
250+
// reserved_zero(bit3)=0, show_existing_frame(bit2)=0, frame_type(bit1)=0.
251+
// 0b1011_0000 = 0xB0
252+
assert!(is_vp9_key_frame(&[0xB0]));
253+
}
254+
255+
#[test]
256+
fn vp9_profile_3_show_existing_frame_is_not_keyframe() {
257+
// Profile 3: show_existing_frame(bit2)=1, frame_type(bit1)=0.
258+
// 0b1011_0100 = 0xB4
259+
assert!(!is_vp9_key_frame(&[0xB4]));
260+
}
261+
262+
#[test]
263+
fn vp9_profile_3_inter_frame_is_not_keyframe() {
264+
// Profile 3: show_existing_frame(bit2)=0, frame_type(bit1)=1.
265+
// 0b1011_0010 = 0xB2
266+
assert!(!is_vp9_key_frame(&[0xB2]));
267+
}
268+
}

crates/video-streamer/src/streamer/iter.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::io::Seek;
22

33
use anyhow::Context;
4-
use cadeau::xmf::vpx::is_key_frame;
4+
use cadeau::xmf::vpx::VpxCodec;
55
use thiserror::Error;
66
use webm_iterable::WebmIterator;
77
use webm_iterable::errors::TagIteratorError;
88
use webm_iterable::matroska_spec::{Block, Master, MatroskaSpec, SimpleBlock};
99

10+
use super::block_tag::is_vpx_key_frame;
1011
use crate::reopenable::Reopenable;
1112

1213
#[derive(Debug, Clone, Copy)]
@@ -40,6 +41,9 @@ pub(crate) struct WebmPositionedIterator<R: std::io::Read + Seek + Reopenable> {
4041
rolled_back_between_cluster: bool,
4142

4243
should_emit_cache: Option<MatroskaSpec>,
44+
45+
// VPX codec type for codec-aware keyframe detection.
46+
codec: VpxCodec,
4347
}
4448

4549
#[derive(Debug, Error)]
@@ -60,19 +64,20 @@ impl<R> WebmPositionedIterator<R>
6064
where
6165
R: std::io::Read + Seek + Reopenable,
6266
{
63-
pub(crate) fn new(mut inner: WebmIterator<R>) -> Self {
67+
pub(crate) fn new(mut inner: WebmIterator<R>, codec: VpxCodec, cluster_start_position: usize) -> Self {
6468
inner.emit_master_end_when_eof(false);
6569
Self {
6670
inner: Some(inner),
67-
previous_emitted_tag_postion: 0,
68-
last_cluster_position: None,
71+
previous_emitted_tag_postion: cluster_start_position,
72+
last_cluster_position: Some(cluster_start_position),
6973
rollback_record: None,
7074
rolled_back_between_cluster: false,
7175
should_emit_cache: None,
7276
last_key_frame_info: LastKeyFrameInfo::NotMet {
7377
cluster_timestamp: None,
74-
cluster_start_position: None,
78+
cluster_start_position: Some(cluster_start_position),
7579
},
80+
codec,
7681
}
7782
}
7883

@@ -132,7 +137,7 @@ where
132137
return result.map(|result| result.map_err(|err| err.into()));
133138
}
134139

135-
match Self::is_key_frame(tag) {
140+
match self.is_key_frame(tag) {
136141
Err(e) => {
137142
return Some(Err(e));
138143
}
@@ -289,7 +294,7 @@ where
289294
Ok(())
290295
}
291296

292-
fn is_key_frame(tag: &MatroskaSpec) -> Result<bool, IteratorError> {
297+
fn is_key_frame(&self, tag: &MatroskaSpec) -> Result<bool, IteratorError> {
293298
match tag {
294299
MatroskaSpec::BlockGroup(Master::Full(children)) => {
295300
let block = children
@@ -308,7 +313,7 @@ where
308313
let block = Block::try_from(block)?;
309314
let frame = block.read_frame_data()?;
310315

311-
Ok(frame.into_iter().any(|frame| is_key_frame(frame.data)))
316+
Ok(frame.into_iter().any(|frame| is_vpx_key_frame(frame.data, self.codec)))
312317
}
313318
MatroskaSpec::SimpleBlock(data) => {
314319
let simple_block = SimpleBlock::try_from(data)?;

crates/video-streamer/src/streamer/mod.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,11 @@ pub fn webm_stream(
3535
config: StreamingConfig,
3636
when_new_chunk_appended: impl Fn() -> tokio::sync::oneshot::Receiver<()>,
3737
) -> anyhow::Result<()> {
38-
let mut webm_itr = WebmPositionedIterator::new(WebmIterator::new(
39-
input_stream,
40-
&[MatroskaSpec::BlockGroup(Master::Start)],
41-
));
38+
let mut raw_itr = WebmIterator::new(input_stream, &[MatroskaSpec::BlockGroup(Master::Start)]);
4239
let mut headers = vec![];
4340

4441
// we extract all the headers before the first cluster
45-
while let Some(tag) = webm_itr.next() {
42+
for tag in raw_itr.by_ref() {
4643
let tag = tag?;
4744
if matches!(tag, MatroskaSpec::Cluster(Master::Start)) {
4845
break;
@@ -51,6 +48,8 @@ pub fn webm_stream(
5148
headers.push(tag);
5249
}
5350
let encode_writer_config = EncodeWriterConfig::try_from((headers.as_slice(), &config))?;
51+
let cluster_start_position = raw_itr.last_emitted_tag_offset();
52+
let mut webm_itr = WebmPositionedIterator::new(raw_itr, encode_writer_config.codec, cluster_start_position);
5453

5554
// we run to the last cluster, skipping everything that has been played
5655
while let Some(tag) = webm_itr.next() {

crates/video-streamer/src/streamer/tag_writers.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::time::{Duration, Instant};
22

33
use anyhow::Context;
4-
use cadeau::xmf::vpx::{VpxCodec, VpxDecoder, VpxEncoder};
4+
use cadeau::xmf::vpx::{VpxCodec, VpxDecoder, VpxEncoder, VpxEncoderPreset};
55
use webm_iterable::errors::TagWriterError;
66
use webm_iterable::matroska_spec::{Master, MatroskaSpec, SimpleBlock};
77
use webm_iterable::{WebmWriter, WriteOptions};
@@ -108,6 +108,7 @@ where
108108
cluster_timestamp: Option<u64>,
109109
encoder: VpxEncoder,
110110
decoder: VpxDecoder,
111+
codec: VpxCodec,
111112
cut_block_state: CutBlockState,
112113
last_encoded_abs_time: Option<u64>,
113114

@@ -177,6 +178,7 @@ where
177178
.height(config.height)
178179
.threads(config.threads)
179180
.bitrate(256 * 1024)
181+
.preset(VpxEncoderPreset::BestPerformance)
180182
.build()
181183
.inspect_err(|error| {
182184
error!(
@@ -199,6 +201,7 @@ where
199201
cluster_timestamp: None,
200202
encoder,
201203
decoder,
204+
codec: config.codec,
202205
cut_block_state: CutBlockState::HaventMet,
203206
last_encoded_abs_time: None,
204207
#[cfg(feature = "perf-diagnostics")]
@@ -267,7 +270,7 @@ where
267270
}
268271
}
269272

270-
let video_block = VideoBlock::new(tag, self.cluster_timestamp)?;
273+
let video_block = VideoBlock::new(tag, self.cluster_timestamp, self.codec)?;
271274
perf_trace!(
272275
block_timestamp = video_block.timestamp,
273276
cluster_timestamp = ?self.cluster_timestamp,

0 commit comments

Comments
 (0)