Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,28 @@ jobs:
with:
toolchain: "1.88.0"
- run: cargo check --locked --all-targets

# The crate's `unsafe` is all libvpx FFI, which Miri refuses to execute.
# Scoped to the codec tests: the other C dependencies hold one-time
# allocations that LeakSanitizer reports as leaks.
sanitizers:
name: Sanitizers
runs-on: ubuntu-latest
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
sanitizer: [address, leak]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
with:
toolchain: nightly
components: rust-src
- run: >
cargo test --locked --lib
-Zbuild-std
--target x86_64-unknown-linux-gnu
rtc::vpx
env:
RUSTFLAGS: -Zsanitizer=${{ matrix.sanitizer }}
64 changes: 60 additions & 4 deletions src/rtc/vpx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,20 +239,21 @@ impl VpxEncoder {
),
"enc_init",
)?;
let mut ctx = ctx.assume_init();
let ctx = ctx.assume_init();
let mut encoder = Self { ctx, width, height };

// Fastest realtime speed setting (CPUUSED is 0..=9 for VP9, higher is
// faster / lower quality — fine for a solid backend frame).
check(
vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as c_int, 8 as c_int),
vpx_codec_control_(&mut encoder.ctx, VP8E_SET_CPUUSED as c_int, 8 as c_int),
"set_cpuused",
)?;
if codec == VpxCodec::Vp9 {
// Row-based multithreading; ignore errors on builds without it.
let _ = vpx_codec_control_(&mut ctx, VP9E_SET_ROW_MT as c_int, 1 as c_int);
let _ = vpx_codec_control_(&mut encoder.ctx, VP9E_SET_ROW_MT as c_int, 1 as c_int);
}

Ok(Self { ctx, width, height })
Ok(encoder)
}
}

Expand Down Expand Up @@ -728,6 +729,61 @@ mod tests {
}
}

/// `unsafe impl Send` claims a libvpx context is not pinned to the thread
/// that created it. Build here, encode there.
#[test]
fn encoder_built_on_one_thread_encodes_on_another() {
let mut encoder = VpxEncoder::new(VpxCodec::Vp9, 160, 120, 400).expect("vp9 encoder");
let input = blue_i420(160, 120);

let frames = std::thread::spawn(move || encoder.encode(&input, 0, 33, true))
.join()
.expect("encoder thread panicked")
.expect("encode");

assert!(
frames
.iter()
.any(|frame| frame.key && !frame.data.is_empty())
);
}

/// The SVC callback's `user_priv` points into a boxed buffer, so relocating
/// the encoder after registration must not invalidate it. The second push
/// reallocates the `Vec` and moves the first encoder to a new address.
#[test]
fn vp9_svc_encoder_encodes_after_being_relocated() {
let mode = Vp9SvcMode::new(2, 1).expect("L2T1 mode");
let input = blue_i420(160, 120);
let mut encoders = vec![VpxSvcEncoder::new(160, 120, 500, mode).expect("VP9 SVC encoder")];
encoders.push(VpxSvcEncoder::new(160, 120, 500, mode).expect("VP9 SVC encoder"));

let frames = encoders[0].encode(&input, 0, 33, true).expect("encode");

assert_eq!(frames.len(), 2);
assert!(frames.iter().all(|frame| !frame.data.is_empty()));
}

/// A size that does not divide by four leaves libvpx to round each spatial
/// layer itself. Every reported layer must still be usable.
#[test]
fn vp9_svc_reports_usable_dimensions_for_odd_sizes() {
let mode = Vp9SvcMode::new(3, 1).expect("L3T1 mode");
let mut encoder = VpxSvcEncoder::new(322, 178, 900, mode).expect("VP9 SVC encoder");
let input = blue_i420(322, 178);

let frames = encoder.encode(&input, 0, 33, true).expect("encode");

assert_eq!(frames.len(), 3);
let top = frames.last().expect("top spatial layer");
assert_eq!((top.width, top.height), (322, 178));
assert!(
frames
.iter()
.all(|frame| frame.width > 0 && frame.height > 0 && !frame.data.is_empty())
);
}

#[test]
fn vp9_svc_uses_codec_reported_0212_temporal_pattern_and_resets_on_key() {
let mode = Vp9SvcMode::new(1, 3).expect("L1T3 mode");
Expand Down
49 changes: 49 additions & 0 deletions src/rtc/vpx_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,55 @@ mod tests {
round_trip(VpxCodec::Vp9, 322, 178);
}

/// `copy_i420` documents that libvpx planes die at the next `decode` call,
/// so a frame handed out earlier must own its pixels.
#[test]
fn decoded_frames_survive_the_next_decode_call() {
let mut enc = VpxEncoder::new(VpxCodec::Vp9, 320, 240, 1_000).expect("encoder");
let mut dec = VpxDecoder::new(VpxCodec::Vp9).expect("decoder");
let source = ramp_i420(320, 240);

let mut held = Vec::new();
for packet in &enc.encode(&source, 0, 33, true).expect("encode key") {
held.extend(dec.decode(&packet.data, 1).expect("decode key"));
}
assert!(!held.is_empty(), "keyframe produced no frame");

for packet in &enc.encode(&source, 33, 33, false).expect("encode delta") {
dec.decode(&packet.data, 2).expect("decode delta");
}

let frame = &held[0];
let w = 320;
let y = &frame.data[..w * 240];
assert_eq!(frame.rtp_timestamp, 1);
assert!(
y[0] < 40 && y[w - 1] > 215,
"the held frame lost its ramp after a later decode"
);
}

/// `copy_i420` reads the dimensions off each image, so one decoder must
/// follow a stream whose resolution changes.
#[test]
fn decoder_follows_a_resolution_change() {
let mut dec = VpxDecoder::new(VpxCodec::Vp9).expect("decoder");
let mut sizes = Vec::new();

for (width, height) in [(320u32, 240u32), (160, 120)] {
let mut enc = VpxEncoder::new(VpxCodec::Vp9, width, height, 800).expect("encoder");
let source = ramp_i420(width, height);
for packet in &enc.encode(&source, 0, 33, true).expect("encode") {
for frame in dec.decode(&packet.data, 0).expect("decode") {
assert_eq!(frame.data.len(), i420_len(frame.width, frame.height));
sizes.push((frame.width, frame.height));
}
}
}

assert_eq!(sizes, vec![(320, 240), (160, 120)]);
}

#[test]
fn empty_input_yields_no_frames() {
let mut dec = VpxDecoder::new(VpxCodec::Vp9).expect("decoder");
Expand Down
Loading