fix(video): improve clarity after scrolling by re-encoding static frames (#16128)

* fix(video): re-encode static frames for up to 10 seconds to improve clarity

Signed-off-by: 21pages <sunboeasy@gmail.com>

fix(video): limit static re-encoding to 300 frames instead of 10 seconds

Signed-off-by: 21pages <sunboeasy@gmail.com>

refactor(video): move static frame refresh into a dedicated module

Signed-off-by: 21pages <sunboeasy@gmail.com>

fix(video): preserve stuck-encoder detection state across static repeats

test(video): cover cached texture lifetime across capture restarts

fix(video): reduce static re-encoding limit to 100 frames

fix(qos): lower qos_trace logging to trace level

Signed-off-by: 21pages <sunboeasy@gmail.com>

fix(video): skip static frame re-encoding for AV1

perf(video): reuse VRAM encoder input for static refresh

Remove the DXGI repeat texture cache and use hwcodec's repeat entry point to avoid copying each captured frame. Isolate static refresh failures, pause after three errors, and resume after a real frame is sent successfully.

Cover cached input ownership, repeat failure recovery, refresh limits, and normal encoder failover with regression tests.

fix(vram): ignore cached SDK codec backends

chore(deps): temporarily use hwcodec re-encode branch

perf(video): wait 200ms before starting static refresh

chore(deps): update hwcodec for FFI exception handling

chore(deps): update hwcodec to 0.8.0

perf(video): start static refresh after 100ms

chore(deps): update hwcodec for exception-safe cleanup

chore(deps): update hwcodec for device-loss handling and noexcept FFI

chore(deps): update hwcodec to bbfc306

chore(qos): remove verbose diagnostics while retaining trace logs

* update hwcodec

Signed-off-by: 21pages <sunboeasy@gmail.com>

* refactor(scrap): keep repeat encoding in the vram module

* perf(vram): avoid intermediate frame vectors

  - Drain normal and repeated encode output into protobuf messages.
  - Cover null-texture encoding failures.
  - Clarify the count-based static refresh budget.
  - Update hwcodec to fix RAM decode buffers on resolution changes.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(scrap): preserve hardware encoder error codes

* test(video): remove uptime dependency from static refresh tests

* feat(video): configure static repeat limits by codec and quality

* fix(video): exempt VPx and AV1 from static repeat failure limit

* fix(video): count only successful static repeat frames

* fix(video): refine static repeat scheduling and VPx frame handling

Allow static refinement after a VP8/VP9 captured frame produces no output
without enabling it after a genuine encoding error. Carry this distinction
alongside the send result so the latest cached frame can still be sent when
capture becomes idle.

Keep VP8/VP9 repeats at a minimum of 100ms. Other codecs wait at least 100ms
initially, then follow the QoS frame interval aligned with the capture loop.
Use four quality bands, with VP8/VP9 limits of 30/20/20/0 and the tuned
H264/H265 limits. Include only production behavior and regression tests.

Validation: 16 static_refresh tests passed with hwcodec,vram,flutter enabled,
covering latest-frame recovery, genuine errors, timing, and repeat budgets.

* fix(video): tune static repeat limits by codec and backend

Use the measured obvious-improvement checkpoints for VP8/VP9 and separate
NVENC, QSV, and AMF H.264/HEVC limits for each quality tier. The checkpoints
recover 80% of the observed error improvement across the tested inputs.

Resolve the hardware backend from EncoderCfg during StaticRefresh
construction, including the VRAM adapter vendor. Keep the existing limits
for other hardware backends.

Validation: 19 static_refresh tests passed with hwcodec,vram,flutter.
The repeat measurements cover 192 series with no encode/decode errors.

* fix(video): refine static refresh budgets and cap repeat FPS

Add VideoToolbox HEVC budgets and tune quality bands from repeat-frame comparisons. Cap static refresh at 30 FPS and use the speed preset consistently at quality 0.5.

* remove unused variables

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(video): repair static refresh tests and throttle repeat logs

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages authored and GitHub committed 2026-10-15 20:17:49 +08:00
1 parent c0750fb50c
commit 392188596c
10 files changed
+1796 -50

No files matched your search

+35 -5
View File
@@ -64,6 +64,8 @@ use std::{
pub const OPTION_REFRESH: &'static str = "refresh";
mod static_refresh;
#[cfg(windows)]
const DXGI_RECOVERY_LIMIT: usize = 3;
#[cfg(windows)]
@@ -731,6 +733,16 @@ fn run(vs: VideoService) -> ResultType<()> {
let mut encoder_warmed_up = false;
let capture_width = c.width;
let capture_height = c.height;
let mut static_refresh = static_refresh::StaticRefresh::new(
vs.source,
codec_format,
&encoder_cfg,
&sp,
&recorder,
display_idx,
capture_width,
capture_height,
);
let (mut second_instant, mut send_counter) = (Instant::now(), 0);
while sp.ok() {
@@ -855,7 +867,8 @@ fn run(vs: VideoService) -> ResultType<()> {
}
let frame = frame.to(encoder.yuvfmt(), &mut yuv, &mut mid_data)?;
let send_conn_ids = handle_one_frame(
static_refresh.on_frame(&frame);
let result = handle_one_frame(
display_idx,
&sp,
frame,
@@ -868,7 +881,8 @@ fn run(vs: VideoService) -> ResultType<()> {
capture_width,
capture_height,
)?;
frame_controller.set_send(now, send_conn_ids);
static_refresh.on_frame_encoded(&result);
frame_controller.set_send(now, result.send_conn_ids);
send_counter += 1;
}
#[cfg(windows)]
@@ -932,11 +946,22 @@ fn run(vs: VideoService) -> ResultType<()> {
&mut encoder_warmed_up,
capture_width,
capture_height,
)?;
)?
.send_conn_ids;
static_refresh.on_encoded(!send_conn_ids.is_empty());
frame_controller.set_send(now, send_conn_ids);
send_counter += 1;
}
}
static_refresh.try_encode(
&yuv,
spf,
quality,
now,
ms,
&mut encoder,
&mut frame_controller,
)?;
}
Err(err) => {
#[cfg(windows)]
@@ -1261,7 +1286,7 @@ fn handle_one_frame(
encoder_warmed_up: &mut bool,
width: usize,
height: usize,
) -> ResultType<HashSet<i32>> {
) -> ResultType<static_refresh::FrameEncodeResult> {
sp.snapshot(|sps| {
// so that new sub and old sub share the same encoder after switch
if sps.has_subscribes() {
@@ -1272,6 +1297,7 @@ fn handle_one_frame(
})?;
let mut send_conn_ids: HashSet<i32> = Default::default();
let mut vpx_no_output = false;
let first = *first_frame;
*first_frame = false;
match encoder.encode_to_message(frame, ms) {
@@ -1289,6 +1315,7 @@ fn handle_one_frame(
send_conn_ids = sp.send_video_frame(msg);
}
Err(e) => {
vpx_no_output = e.is::<scrap::VpxNoOutput>();
*encode_fail_counter += 1;
// Encoding errors are not frequent except on Android
if !cfg!(target_os = "android") {
@@ -1319,7 +1346,10 @@ fn handle_one_frame(
}
}
}
Ok(send_conn_ids)
Ok(static_refresh::FrameEncodeResult {
send_conn_ids,
vpx_no_output,
})
}
#[inline]
+745
View File
@@ -0,0 +1,745 @@
use super::{GenericService, VideoFrameController, VideoSource};
use base::message_proto::Message;
use hbb_common::{bail, ResultType};
use scrap::{
codec::{Encoder, EncoderCfg, BR_BALANCED, BR_BEST, BR_SPEED},
record::Recorder,
CodecFormat, EncodeInput,
};
use std::{
collections::HashSet,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
const INITIAL_REPEAT_INTERVAL: Duration = Duration::from_millis(100);
const MAX_REPEAT_FPS: u32 = 30;
const MAX_REPEAT_FAILURES: usize = 3;
const MAX_REPEAT_NO_OUTPUTS: usize = 100;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HardwareBackend {
Nvenc,
Qsv,
Amf,
Vaapi,
VideoToolbox,
Other,
}
fn max_repeat_frames(codec: CodecFormat, quality: f32, backend: HardwareBackend) -> usize {
use HardwareBackend::*;
// Measured entries sample quality 0.1, BR_SPEED, BR_BALANCED, and BR_BEST.
// For measured entries, Original values come from AI-run tests. Active values
// were adjusted by comparing 1080p screenshots captured every 10 repeat outputs.
let (low, low_to_balanced, balanced_to_best, best) = match (codec, backend) {
(CodecFormat::VP8, _) => (20, 20, 20, 30), // Original: (20, 20, 20, 30)
(CodecFormat::VP9, _) => (40, 20, 20, 20), // Original: (40, 20, 20, 20)
(CodecFormat::H264, Nvenc) => (80, 50, 60, 30), // Original: (270, 50, 60, 30)
(CodecFormat::H264, Qsv) => (80, 70, 130, 110), // Original: (80, 70, 130, 110)
(CodecFormat::H264, Amf) => (150, 150, 170, 110), // Original: (560, 150, 170, 110)
(CodecFormat::H264, Vaapi) => (150, 100, 90, 60), // Original: (330, 100, 90, 60)
(CodecFormat::H265, Nvenc) => (160, 90, 60, 30), // Original: (160, 90, 60, 30)
(CodecFormat::H265, Qsv) => (150, 30, 30, 30), // Original: (150, 30, 30, 30)
(CodecFormat::H265, Amf) => (200, 200, 200, 120), // Original: (580, 200, 200, 120)
// The capture loop already repeats ten frames to drain VideoToolbox latency.
(CodecFormat::H265, VideoToolbox) => (90, 90, 50, 90), // Original: (230, 90, 50, 90)
(CodecFormat::H264, _) => (90, 60, 60, 30),
(CodecFormat::H265, _) => (90, 60, 60, 30),
(CodecFormat::AV1 | CodecFormat::Unknown, _) => return 0,
};
if quality >= BR_BEST {
best
} else if quality >= BR_BALANCED {
balanced_to_best
} else if quality >= BR_SPEED {
low_to_balanced
} else {
low
}
}
#[derive(Debug)]
pub(super) struct FrameEncodeResult {
pub(super) send_conn_ids: HashSet<i32>,
pub(super) vpx_no_output: bool,
}
pub(super) struct StaticRefresh<'a> {
source: VideoSource,
codec_format: CodecFormat,
hardware_backend: HardwareBackend,
sp: &'a GenericService,
recorder: &'a Arc<Mutex<Option<Recorder>>>,
display_idx: usize,
width: usize,
height: usize,
last_encode: Instant,
#[cfg(test)]
elapsed_since_encode: Duration,
repeat_output_counter: usize,
repeat_no_outputs: usize,
repeat_failures: usize,
source_ready: bool,
#[cfg(all(windows, feature = "vram"))]
has_texture: bool,
}
impl<'a> StaticRefresh<'a> {
pub(super) fn new(
source: VideoSource,
codec_format: CodecFormat,
encoder_config: &EncoderCfg,
sp: &'a GenericService,
recorder: &'a Arc<Mutex<Option<Recorder>>>,
display_idx: usize,
width: usize,
height: usize,
) -> Self {
let hardware_backend = match encoder_config {
#[cfg(feature = "hwcodec")]
EncoderCfg::HWRAM(config) => match config.name.as_str() {
"h264_nvenc" | "hevc_nvenc" => HardwareBackend::Nvenc,
"h264_qsv" | "hevc_qsv" => HardwareBackend::Qsv,
"h264_amf" | "hevc_amf" => HardwareBackend::Amf,
"h264_vaapi" => HardwareBackend::Vaapi,
"hevc_videotoolbox" => HardwareBackend::VideoToolbox,
_ => HardwareBackend::Other,
},
#[cfg(feature = "vram")]
// The FFMPEG VRAM driver selects the encoder from the adapter vendor.
EncoderCfg::VRAM(config) => match config.feature.vendor {
scrap::vram::Driver::NV => HardwareBackend::Nvenc,
scrap::vram::Driver::MFX => HardwareBackend::Qsv,
scrap::vram::Driver::AMF => HardwareBackend::Amf,
_ => HardwareBackend::Other,
},
_ => HardwareBackend::Other,
};
Self {
source,
codec_format,
hardware_backend,
sp,
recorder,
display_idx,
width,
height,
last_encode: Instant::now(),
#[cfg(test)]
elapsed_since_encode: Duration::ZERO,
repeat_output_counter: 0,
repeat_no_outputs: 0,
repeat_failures: 0,
source_ready: false,
#[cfg(all(windows, feature = "vram"))]
has_texture: false,
}
}
pub(super) fn on_frame(&mut self, _frame: &EncodeInput) {
self.repeat_output_counter = 0;
self.repeat_no_outputs = 0;
self.source_ready = false;
#[cfg(all(windows, feature = "vram"))]
{
self.has_texture =
matches!(_frame, EncodeInput::Texture((texture, 0)) if !texture.is_null());
}
}
pub(super) fn on_frame_encoded(&mut self, result: &FrameEncodeResult) {
// VP8/VP9 rate control may drop a frame without an encoding error.
// The latest input is still cached, so allow repeats even without output;
// otherwise the final update may never be sent once capture becomes idle.
self.on_encoded(
!result.send_conn_ids.is_empty()
|| (matches!(self.codec_format, CodecFormat::VP8 | CodecFormat::VP9)
&& result.vpx_no_output),
);
}
pub(super) fn on_encoded(&mut self, success: bool) {
self.last_encode = Instant::now();
#[cfg(test)]
{
self.elapsed_since_encode = Duration::ZERO;
}
if success {
self.source_ready = true;
self.repeat_failures = 0;
}
}
pub(super) fn try_encode(
&mut self,
yuv: &[u8],
spf: Duration,
quality: f32,
now: Instant,
ms: i64,
encoder: &mut Encoder,
frame_controller: &mut VideoFrameController,
) -> ResultType<()> {
let spf = spf.max(Duration::from_secs(1) / MAX_REPEAT_FPS);
if !self.source.is_monitor()
|| self.codec_format == CodecFormat::AV1
|| !self.source_ready
|| (self.repeat_failures >= MAX_REPEAT_FAILURES
&& !matches!(
self.codec_format,
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1
))
|| self.repeat_no_outputs >= MAX_REPEAT_NO_OUTPUTS
|| self.repeat_output_counter
>= max_repeat_frames(self.codec_format, quality, self.hardware_backend)
|| {
#[cfg(not(test))]
let elapsed = self.last_encode.elapsed();
#[cfg(test)]
let elapsed = self.elapsed_since_encode;
let interval = if matches!(self.codec_format, CodecFormat::VP8 | CodecFormat::VP9)
|| (self.repeat_output_counter == 0 && self.repeat_no_outputs == 0)
{
INITIAL_REPEAT_INTERVAL.max(spf)
} else {
spf
};
elapsed < interval
}
{
return Ok(());
}
let frame = if yuv.is_empty() {
None
} else {
Some(EncodeInput::YUV(yuv))
};
#[cfg(all(windows, feature = "vram"))]
let frame = frame.or_else(|| self.has_texture.then_some(EncodeInput::Repeat));
if let Some(frame) = frame {
self.sp.snapshot(|sps| {
if sps.has_subscribes() {
bail!("SWITCH");
}
Ok(())
})?;
let result = encoder.encode_to_message(frame, ms);
self.last_encode = if matches!(self.codec_format, CodecFormat::VP8 | CodecFormat::VP9) {
Instant::now()
} else {
// Align with the capture loop so encoding time does not skip the next tick.
now
};
#[cfg(test)]
{
self.elapsed_since_encode = Duration::ZERO;
}
let mut vf = match result {
Ok(vf) if vf.union.is_none() => {
self.repeat_no_outputs += 1;
return Ok(());
}
Ok(vf) => vf,
Err(error) => {
self.repeat_no_outputs += 1;
self.repeat_failures += 1;
if error.is::<scrap::VpxNoOutput>() {
hbb_common::throttled_log!(
Duration::from_secs(60),
trace,
"static refresh produced no output: {error:?}"
);
} else {
hbb_common::throttled_log!(
Duration::from_secs(60),
debug,
"static refresh failed ({}): {error:?}",
self.repeat_failures
);
}
return Ok(());
}
};
self.repeat_output_counter += 1;
self.repeat_failures = 0;
vf.display = self.display_idx as _;
let mut msg = Message::new();
msg.set_video_frame(vf);
self.recorder
.lock()
.unwrap()
.as_mut()
.map(|r| r.write_message(&msg, self.width, self.height));
let send_conn_ids = self.sp.send_video_frame(msg);
frame_controller.set_send(now, send_conn_ids);
// Static refinement must not count as motion for adaptive bitrate.
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::{connection::ConnInner, service::Service};
use base::message_proto::VideoFrame;
use hbb_common::tokio::sync::mpsc;
use scrap::{
codec::{EncoderApi, EncoderCfg, BR_BALANCED},
vpxcodec::{VpxEncoderConfig, VpxVideoCodecId},
EncodeYuvFormat,
};
use std::{cell::Cell, rc::Rc};
pub(super) fn new_refresh<'a>(
source: VideoSource,
codec_format: CodecFormat,
sp: &'a GenericService,
recorder: &'a Arc<Mutex<Option<Recorder>>>,
display_idx: usize,
width: usize,
height: usize,
) -> StaticRefresh<'a> {
// The mock encoders simulate each format without a hardware backend.
let config = EncoderCfg::VPX(VpxEncoderConfig {
width: width as _,
height: height as _,
quality: BR_BALANCED,
codec: VpxVideoCodecId::VP9,
keyframe_interval: None,
});
StaticRefresh::new(
source,
codec_format,
&config,
sp,
recorder,
display_idx,
width,
height,
)
}
struct CountingEncoder(Rc<Cell<usize>>);
impl EncoderApi for CountingEncoder {
fn new(_: EncoderCfg, _: bool) -> ResultType<Self> {
unreachable!()
}
fn encode_to_message(&mut self, frame: EncodeInput, _: i64) -> ResultType<VideoFrame> {
match frame {
EncodeInput::YUV(yuv) => assert_eq!(yuv, &[1]),
#[cfg(all(windows, feature = "vram"))]
EncodeInput::Repeat => {}
_ => panic!("unexpected encoder input"),
}
self.0.set(self.0.get() + 1);
let mut vf = VideoFrame::new();
vf.set_h264s(Default::default());
Ok(vf)
}
fn yuvfmt(&self) -> EncodeYuvFormat {
unreachable!()
}
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool {
false
}
fn set_quality(&mut self, _: f32) -> ResultType<()> {
unreachable!()
}
fn bitrate(&self) -> u32 {
0
}
fn support_changing_quality(&self) -> bool {
false
}
fn latency_free(&self) -> bool {
true
}
fn is_hardware(&self) -> bool {
false
}
fn disable(&self) {
unreachable!()
}
}
fn attempt(
refresh: &mut StaticRefresh<'_>,
calls: &Rc<Cell<usize>>,
yuv: &[u8],
spf: Duration,
) {
let mut encoder = Encoder {
codec: Box::new(CountingEncoder(Rc::clone(calls))),
};
refresh
.try_encode(
yuv,
spf,
BR_BALANCED,
Instant::now(),
0,
&mut encoder,
&mut VideoFrameController::new(0),
)
.unwrap();
}
#[test]
fn long_network_waits_preserve_budget_and_new_frames_restart_it() {
let sp = GenericService::new("static-refresh-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
let mut refresh = new_refresh(
VideoSource::Monitor,
CodecFormat::VP9,
&sp,
&recorder,
0,
1,
1,
);
let calls = Rc::new(Cell::new(0));
let spf = Duration::from_millis(100);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
let limit = max_repeat_frames(CodecFormat::VP9, BR_BALANCED, HardwareBackend::Other);
for _ in 0..limit {
refresh.elapsed_since_encode = Duration::from_secs(60);
attempt(&mut refresh, &calls, &[1], spf);
}
assert_eq!(calls.get(), limit);
refresh.elapsed_since_encode = Duration::from_secs(600);
attempt(&mut refresh, &calls, &[1], spf);
assert_eq!(calls.get(), limit);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
refresh.elapsed_since_encode = Duration::from_secs(1);
attempt(&mut refresh, &calls, &[1], spf);
assert_eq!(calls.get(), limit + 1);
}
#[test]
fn initial_delay_repeat_interval_and_qos_limit_refresh_rate() {
let sp = GenericService::new("static-refresh-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
let mut refresh = new_refresh(
VideoSource::Monitor,
CodecFormat::H264,
&sp,
&recorder,
0,
1,
1,
);
let calls = Rc::new(Cell::new(0));
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
attempt(&mut refresh, &calls, &[1], Duration::from_millis(10));
assert_eq!(calls.get(), 0);
refresh.elapsed_since_encode = Duration::from_millis(50);
attempt(&mut refresh, &calls, &[1], Duration::from_millis(10));
assert_eq!(calls.get(), 0);
refresh.elapsed_since_encode = Duration::from_millis(100);
attempt(&mut refresh, &calls, &[1], Duration::from_millis(10));
assert_eq!(calls.get(), 1);
refresh.elapsed_since_encode = Duration::from_millis(33);
attempt(&mut refresh, &calls, &[1], Duration::from_millis(10));
assert_eq!(calls.get(), 1);
refresh.elapsed_since_encode = Duration::from_secs(1) / 30;
attempt(&mut refresh, &calls, &[1], Duration::from_millis(10));
assert_eq!(calls.get(), 2);
refresh.elapsed_since_encode = Duration::from_millis(49);
attempt(&mut refresh, &calls, &[1], Duration::from_millis(50));
assert_eq!(calls.get(), 2);
refresh.elapsed_since_encode = Duration::from_millis(50);
attempt(&mut refresh, &calls, &[1], Duration::from_millis(50));
assert_eq!(calls.get(), 3);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
refresh.elapsed_since_encode = Duration::from_millis(50);
attempt(&mut refresh, &calls, &[1], Duration::from_millis(10));
assert_eq!(calls.get(), 3);
refresh.elapsed_since_encode = Duration::from_secs(1);
attempt(&mut refresh, &calls, &[1], Duration::from_secs(2));
assert_eq!(calls.get(), 3);
refresh.elapsed_since_encode = Duration::from_secs(3);
attempt(&mut refresh, &calls, &[1], Duration::from_secs(2));
assert_eq!(calls.get(), 4);
attempt(&mut refresh, &calls, &[1], Duration::from_secs(2));
assert_eq!(calls.get(), 4);
refresh.elapsed_since_encode = Duration::from_millis(1999);
attempt(&mut refresh, &calls, &[1], Duration::from_secs(2));
assert_eq!(calls.get(), 4);
refresh.elapsed_since_encode = Duration::from_millis(2000);
attempt(&mut refresh, &calls, &[1], Duration::from_secs(2));
assert_eq!(calls.get(), 5);
}
#[test]
#[cfg(all(windows, feature = "vram"))]
fn texture_frames_request_repeats_without_accessing_capture_resources() {
let sp = GenericService::new("static-refresh-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
let mut refresh = new_refresh(
VideoSource::Monitor,
CodecFormat::H264,
&sp,
&recorder,
0,
1,
1,
);
let calls = Rc::new(Cell::new(0));
// Deliberately not a D3D resource: neither hook may dereference it.
let texture = std::ptr::NonNull::<u8>::dangling().as_ptr() as _;
for _ in 0..60 {
refresh.on_frame(&EncodeInput::Texture((texture, 0)));
}
refresh.on_encoded(true);
refresh.elapsed_since_encode = Duration::from_secs(1);
attempt(&mut refresh, &calls, &[], Duration::from_millis(16));
assert_eq!(calls.get(), 1);
for frame in [
EncodeInput::YUV(&[1]),
EncodeInput::Texture((texture, 90)),
EncodeInput::Texture((std::ptr::null_mut(), 0)),
] {
refresh.on_frame(&frame);
refresh.on_encoded(true);
refresh.elapsed_since_encode = Duration::from_secs(1);
attempt(&mut refresh, &calls, &[], Duration::from_millis(16));
assert_eq!(calls.get(), 1);
}
}
#[test]
fn cameras_av1_and_missing_frames_are_not_refreshed() {
let sp = GenericService::new("static-refresh-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
let calls = Rc::new(Cell::new(0));
for (source, codec_format, yuv) in [
(VideoSource::Camera, CodecFormat::VP9, &[1][..]),
(VideoSource::Monitor, CodecFormat::AV1, &[1][..]),
(VideoSource::Monitor, CodecFormat::VP9, &[][..]),
] {
let mut refresh = new_refresh(source, codec_format, &sp, &recorder, 0, 1, 1);
refresh.on_frame(&EncodeInput::YUV(yuv));
refresh.on_encoded(true);
refresh.elapsed_since_encode = Duration::from_secs(60);
attempt(&mut refresh, &calls, yuv, Duration::from_millis(100));
}
assert_eq!(calls.get(), 0);
}
fn assert_output_budget(
refresh: &mut StaticRefresh<'_>,
sp: &GenericService,
quality: f32,
expected: usize,
) {
let (tx, rx) = mpsc::unbounded_channel();
sp.on_subscribe(ConnInner::new(1, None, Some(tx)));
let mut encoder = Encoder {
codec: Box::new(CountingEncoder(Rc::new(Cell::new(0)))),
};
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
for _ in 0..=expected {
refresh.elapsed_since_encode = Duration::from_secs(1);
refresh
.try_encode(
&[1],
Duration::from_millis(33),
quality,
Instant::now(),
0,
&mut encoder,
&mut VideoFrameController::new(0),
)
.unwrap();
}
assert_eq!(rx.len(), expected);
sp.on_unsubscribe(1);
}
#[test]
fn quality_bands_stop_each_backend_at_its_output_budget() {
let sp = GenericService::new("repeat-quality-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
assert_eq!(
[0.1, BR_SPEED, BR_BALANCED, BR_BEST]
.map(|q| { max_repeat_frames(CodecFormat::H264, q, HardwareBackend::Vaapi) }),
[150, 100, 90, 60]
);
for codec in [
CodecFormat::VP8,
CodecFormat::VP9,
CodecFormat::H264,
CodecFormat::H265,
] {
for backend in [
HardwareBackend::Nvenc,
HardwareBackend::Qsv,
HardwareBackend::Amf,
HardwareBackend::Vaapi,
HardwareBackend::VideoToolbox,
HardwareBackend::Other,
] {
let mut refresh = new_refresh(VideoSource::Monitor, codec, &sp, &recorder, 0, 1, 1);
refresh.hardware_backend = backend;
for quality in [
0.1,
BR_BEST,
BR_SPEED,
BR_BALANCED,
BR_SPEED + 0.01,
BR_BEST + 0.1,
] {
let limit = max_repeat_frames(codec, quality, backend);
assert_output_budget(&mut refresh, &sp, quality, limit);
}
}
}
}
#[test]
fn speed_boundary_uses_the_speed_preset_for_all_backends() {
for codec in [
CodecFormat::VP8,
CodecFormat::VP9,
CodecFormat::H264,
CodecFormat::H265,
] {
for backend in [
HardwareBackend::Nvenc,
HardwareBackend::Qsv,
HardwareBackend::Amf,
HardwareBackend::Vaapi,
HardwareBackend::VideoToolbox,
HardwareBackend::Other,
] {
assert_eq!(
max_repeat_frames(codec, BR_SPEED, backend),
max_repeat_frames(codec, BR_SPEED + 0.01, backend),
"{codec:?} {backend:?}"
);
}
}
}
#[test]
#[cfg(feature = "hwcodec")]
fn ram_config_selects_the_actual_backend_and_preserves_other_encoders() {
let sp = GenericService::new("repeat-backend-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for (name, expected) in [
("h264_nvenc", HardwareBackend::Nvenc),
("hevc_nvenc", HardwareBackend::Nvenc),
("h264_qsv", HardwareBackend::Qsv),
("hevc_qsv", HardwareBackend::Qsv),
("h264_amf", HardwareBackend::Amf),
("hevc_amf", HardwareBackend::Amf),
("h264_vaapi", HardwareBackend::Vaapi),
("hevc_vaapi", HardwareBackend::Other),
("hevc_videotoolbox", HardwareBackend::VideoToolbox),
("h264_videotoolbox", HardwareBackend::Other),
] {
let config = EncoderCfg::HWRAM(scrap::hwcodec::HwRamEncoderConfig {
name: name.to_owned(),
mc_name: None,
width: 1920,
height: 1080,
quality: BR_BALANCED,
keyframe_interval: None,
});
let codec_format = if name.starts_with("hevc_") {
CodecFormat::H265
} else {
CodecFormat::H264
};
let mut refresh = StaticRefresh::new(
VideoSource::Monitor,
codec_format,
&config,
&sp,
&recorder,
0,
1920,
1080,
);
for quality in [0.1, BR_SPEED, BR_BALANCED, BR_BEST] {
let limit = max_repeat_frames(codec_format, quality, expected);
assert_output_budget(&mut refresh, &sp, quality, limit);
}
}
}
#[test]
#[cfg(feature = "vram")]
fn vram_config_resolves_ffmpeg_to_the_actual_hardware_backend() {
let sp = GenericService::new("repeat-vram-backend-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for (vendor, expected) in [
("NV", HardwareBackend::Nvenc),
("MFX", HardwareBackend::Qsv),
("AMF", HardwareBackend::Amf),
("FFMPEG", HardwareBackend::Other),
] {
let config = EncoderCfg::VRAM(scrap::vram::VRamEncoderConfig {
device: Default::default(),
width: 1920,
height: 1080,
quality: BR_BALANCED,
keyframe_interval: None,
feature: serde_json::from_value(serde_json::json!({
"driver": "FFMPEG",
"vendor": vendor,
"luid": 0,
"data_format": "H264",
}))
.unwrap(),
});
let mut refresh = StaticRefresh::new(
VideoSource::Monitor,
CodecFormat::H264,
&config,
&sp,
&recorder,
0,
1920,
1080,
);
for quality in [0.1, BR_SPEED, BR_BALANCED, BR_BEST] {
let limit = max_repeat_frames(CodecFormat::H264, quality, expected);
assert_output_budget(&mut refresh, &sp, quality, limit);
}
}
}
}
#[cfg(test)]
mod error_tests;
@@ -0,0 +1,579 @@
use super::{tests::new_refresh, *};
use crate::server::{connection::ConnInner, service::Service};
use base::message_proto::VideoFrame;
use hbb_common::tokio::sync::mpsc;
use scrap::{
codec::{EncoderApi, EncoderCfg, BR_BALANCED},
EncodeYuvFormat,
};
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
#[derive(Clone, Copy)]
enum Outcome {
Frame,
Empty,
Dropped,
Error,
}
struct State {
outcomes: VecDeque<Outcome>,
calls: usize,
disabled: usize,
inputs: Vec<Vec<u8>>,
}
struct ScriptedEncoder(Rc<RefCell<State>>);
impl EncoderApi for ScriptedEncoder {
fn new(_: EncoderCfg, _: bool) -> ResultType<Self> {
unreachable!()
}
fn encode_to_message(&mut self, input: EncodeInput, _: i64) -> ResultType<VideoFrame> {
let mut state = self.0.borrow_mut();
state.calls += 1;
if let EncodeInput::YUV(yuv) = input {
state.inputs.push(yuv.to_vec());
}
match state.outcomes.pop_front().expect("unexpected encode") {
Outcome::Frame => {
let mut vf = VideoFrame::new();
vf.set_h264s(Default::default());
Ok(vf)
}
Outcome::Empty => Ok(VideoFrame::new()),
Outcome::Dropped => Err(scrap::VpxNoOutput.into()),
Outcome::Error => bail!("injected encoding error"),
}
}
fn yuvfmt(&self) -> EncodeYuvFormat {
unreachable!()
}
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool {
false
}
fn set_quality(&mut self, _: f32) -> ResultType<()> {
unreachable!()
}
fn bitrate(&self) -> u32 {
0
}
fn support_changing_quality(&self) -> bool {
false
}
fn latency_free(&self) -> bool {
true
}
fn is_hardware(&self) -> bool {
true
}
fn disable(&self) {
self.0.borrow_mut().disabled += 1;
}
}
fn encoder(outcomes: &[Outcome]) -> (Encoder, Rc<RefCell<State>>) {
let state = Rc::new(RefCell::new(State {
outcomes: outcomes.iter().copied().collect(),
calls: 0,
disabled: 0,
inputs: Vec::new(),
}));
(
Encoder {
codec: Box::new(ScriptedEncoder(Rc::clone(&state))),
},
state,
)
}
fn attempt(refresh: &mut StaticRefresh<'_>, encoder: &mut Encoder) {
refresh.elapsed_since_encode = Duration::from_secs(1);
refresh
.try_encode(
&[1],
Duration::from_millis(100),
BR_BALANCED,
Instant::now(),
0,
encoder,
&mut VideoFrameController::new(0),
)
.unwrap();
}
#[test]
fn failures_pause_refresh_until_a_real_frame_succeeds() {
use Outcome::*;
let sp = GenericService::new("repeat-errors-test".to_owned(), false);
let (tx, rx) = mpsc::unbounded_channel();
sp.on_subscribe(ConnInner::new(1, None, Some(tx)));
let recorder = Arc::new(Mutex::new(None));
let mut refresh = new_refresh(
VideoSource::Monitor,
CodecFormat::H264,
&sp,
&recorder,
0,
1,
1,
);
let (mut encoder, state) = encoder(&[Error, Error, Empty, Frame, Error, Error, Error, Frame]);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
attempt(&mut refresh, &mut encoder);
refresh
.try_encode(
&[1],
Duration::from_millis(100),
BR_BALANCED,
Instant::now(),
0,
&mut encoder,
&mut VideoFrameController::new(0),
)
.unwrap();
assert_eq!(
state.borrow().calls,
1,
"failure must retain the rate limit"
);
attempt(&mut refresh, &mut encoder);
attempt(&mut refresh, &mut encoder);
assert_eq!(rx.len(), 0);
attempt(&mut refresh, &mut encoder);
assert_eq!(rx.len(), 1, "empty output must not stop further refreshes");
for _ in 0..3 {
attempt(&mut refresh, &mut encoder);
}
attempt(&mut refresh, &mut encoder);
assert_eq!(state.borrow().calls, 7);
assert_eq!(rx.len(), 1);
assert_eq!(state.borrow().disabled, 0);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(false);
attempt(&mut refresh, &mut encoder);
assert_eq!(
state.borrow().calls,
7,
"a failed real frame must not restart refresh"
);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
attempt(&mut refresh, &mut encoder);
assert_eq!(state.borrow().calls, 8);
assert_eq!(rx.len(), 2);
assert_eq!(state.borrow().disabled, 0);
}
#[test]
fn empty_outputs_exhaust_the_no_output_budget_without_disabling_the_encoder() {
let sp = GenericService::new("repeat-empty-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
let mut refresh = new_refresh(
VideoSource::Monitor,
CodecFormat::H264,
&sp,
&recorder,
0,
1,
1,
);
let (mut encoder, state) = encoder(&[Outcome::Empty; 100]);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
for _ in 0..101 {
attempt(&mut refresh, &mut encoder);
}
assert_eq!(state.borrow().calls, 100);
assert_eq!(state.borrow().disabled, 0);
}
#[test]
fn vpx_drops_exhaust_the_no_output_budget_without_disabling_the_encoder() {
let sp = GenericService::new("repeat-vpx-drops-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for codec in [CodecFormat::VP8, CodecFormat::VP9] {
let mut refresh = new_refresh(VideoSource::Monitor, codec, &sp, &recorder, 0, 1, 1);
let limit = MAX_REPEAT_NO_OUTPUTS;
let (mut encoder, state) = encoder(&vec![Outcome::Dropped; limit]);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
for _ in 0..=limit {
attempt(&mut refresh, &mut encoder);
}
assert_eq!(state.borrow().calls, limit, "{codec:?}");
assert_eq!(state.borrow().disabled, 0);
}
}
#[test]
fn vpx_dropped_capture_repeats_the_latest_frame() {
use super::super::handle_one_frame;
use Outcome::*;
let sp = GenericService::new("repeat-dropped-capture-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for codec in [CodecFormat::VP8, CodecFormat::VP9] {
let (tx, mut rx) = mpsc::unbounded_channel();
sp.on_subscribe(ConnInner::new(1, None, Some(tx)));
let mut refresh = new_refresh(VideoSource::Monitor, codec, &sp, &recorder, 0, 1, 1);
let (mut encoder, state) = encoder(&[Frame, Dropped, Dropped, Frame]);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
attempt(&mut refresh, &mut encoder);
assert!(rx.try_recv().unwrap().1.has_video_frame());
let latest_yuv = [2];
refresh.on_frame(&EncodeInput::YUV(&latest_yuv));
let mut normal_failures = 0;
let mut first_frame = false;
let mut encoder_warmed_up = false;
let result = handle_one_frame(
0,
&sp,
EncodeInput::YUV(&latest_yuv),
100,
&mut encoder,
recorder.clone(),
&mut normal_failures,
&mut first_frame,
&mut encoder_warmed_up,
1,
1,
)
.unwrap();
assert!(result.send_conn_ids.is_empty());
assert!(result.vpx_no_output);
refresh.on_frame_encoded(&result);
assert_eq!(rx.len(), 0);
for (elapsed_ms, expected_calls, expected_outputs) in
[(99, 2, 0), (100, 3, 0), (100, 4, 1)]
{
refresh.elapsed_since_encode = Duration::from_millis(elapsed_ms);
refresh
.try_encode(
&latest_yuv,
Duration::from_nanos(33_333_333),
BR_BALANCED,
Instant::now(),
expected_calls as i64 * 100,
&mut encoder,
&mut VideoFrameController::new(0),
)
.unwrap();
assert_eq!(state.borrow().calls, expected_calls, "{codec:?}");
assert_eq!(rx.len(), expected_outputs);
}
assert_eq!(state.borrow().inputs, vec![vec![1], vec![2], vec![2], vec![2]]);
assert_eq!(state.borrow().disabled, 0);
sp.on_unsubscribe(1);
}
}
#[test]
fn captured_frame_errors_do_not_enable_repeat() {
use super::super::handle_one_frame;
let sp = GenericService::new("repeat-capture-error-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for (codec, outcome, vpx_no_output) in [
(CodecFormat::VP8, Outcome::Error, false),
(CodecFormat::VP9, Outcome::Error, false),
(CodecFormat::H264, Outcome::Dropped, true),
(CodecFormat::H265, Outcome::Dropped, true),
] {
let mut refresh = new_refresh(VideoSource::Monitor, codec, &sp, &recorder, 0, 1, 1);
let (mut encoder, state) = encoder(&[outcome, Outcome::Frame]);
refresh.on_frame(&EncodeInput::YUV(&[1]));
let mut normal_failures = 0;
let mut first_frame = false;
let mut encoder_warmed_up = false;
let result = handle_one_frame(
0,
&sp,
EncodeInput::YUV(&[1]),
0,
&mut encoder,
recorder.clone(),
&mut normal_failures,
&mut first_frame,
&mut encoder_warmed_up,
1,
1,
)
.unwrap();
assert!(result.send_conn_ids.is_empty());
assert_eq!(result.vpx_no_output, vpx_no_output);
refresh.on_frame_encoded(&result);
attempt(&mut refresh, &mut encoder);
assert_eq!(state.borrow().calls, 1, "{codec:?}");
assert_eq!(state.borrow().disabled, 0);
}
}
#[test]
fn unsuccessful_repeats_preserve_the_frame_budget() {
let sp = GenericService::new("repeat-frame-budget-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for codec in [CodecFormat::VP8, CodecFormat::VP9] {
let (tx, rx) = mpsc::unbounded_channel();
sp.on_subscribe(ConnInner::new(1, None, Some(tx)));
let mut refresh = new_refresh(VideoSource::Monitor, codec, &sp, &recorder, 0, 1, 1);
let limit = max_repeat_frames(codec, BR_BALANCED, HardwareBackend::Other);
let mut outcomes = vec![Outcome::Dropped; 40];
outcomes.extend(vec![Outcome::Empty; 40]);
outcomes.extend(vec![Outcome::Frame; limit + 1]);
let (mut encoder, state) = encoder(&outcomes);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
for _ in 0..80 {
attempt(&mut refresh, &mut encoder);
}
assert_eq!(rx.len(), 0, "{codec:?}");
for _ in 0..=limit {
attempt(&mut refresh, &mut encoder);
}
assert_eq!(state.borrow().calls, 80 + limit, "{codec:?}");
assert_eq!(rx.len(), limit);
assert_eq!(state.borrow().disabled, 0);
sp.on_unsubscribe(1);
}
}
#[test]
fn repeat_intervals_are_codec_specific_after_the_first_attempt() {
let sp = GenericService::new("repeat-interval-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for (codec, spf_ms, first_interval_ms, repeat_interval_ms) in [
(CodecFormat::VP8, 10, 100, 100),
(CodecFormat::VP9, 10, 100, 100),
(CodecFormat::VP8, 200, 200, 200),
(CodecFormat::VP9, 200, 200, 200),
(CodecFormat::H264, 10, 100, 34),
(CodecFormat::H265, 10, 100, 34),
(CodecFormat::H264, 16, 100, 34),
(CodecFormat::H265, 16, 100, 34),
(CodecFormat::H264, 50, 100, 50),
(CodecFormat::H265, 50, 100, 50),
(CodecFormat::H264, 200, 200, 200),
(CodecFormat::H265, 200, 200, 200),
] {
for first in [
Outcome::Frame,
Outcome::Empty,
Outcome::Dropped,
Outcome::Error,
] {
let mut refresh = new_refresh(VideoSource::Monitor, codec, &sp, &recorder, 0, 1, 1);
let (mut encoder, state) = encoder(&[first, Outcome::Frame]);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
for (elapsed_ms, expected_calls) in [
(first_interval_ms - 1, 0),
(first_interval_ms, 1),
(repeat_interval_ms - 1, 1),
(repeat_interval_ms, 2),
] {
refresh.elapsed_since_encode = Duration::from_millis(elapsed_ms);
refresh
.try_encode(
&[1],
Duration::from_millis(spf_ms),
BR_BALANCED,
Instant::now(),
0,
&mut encoder,
&mut VideoFrameController::new(0),
)
.unwrap();
assert_eq!(state.borrow().calls, expected_calls, "{codec:?}");
}
}
}
}
#[test]
fn repeat_timing_uses_the_capture_loop_start() {
let sp = GenericService::new("repeat-loop-clock-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
let spf = Duration::from_nanos(33_333_333);
let clock_start = Instant::now() + Duration::from_secs(1);
for first in [
Outcome::Frame,
Outcome::Empty,
Outcome::Dropped,
Outcome::Error,
] {
let mut refresh = new_refresh(
VideoSource::Monitor,
CodecFormat::H264,
&sp,
&recorder,
0,
1,
1,
);
let (mut encoder, state) = encoder(&[first, Outcome::Frame]);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
refresh.last_encode = clock_start;
for (index, loop_start) in [
clock_start + INITIAL_REPEAT_INTERVAL,
clock_start + INITIAL_REPEAT_INTERVAL + spf,
]
.into_iter()
.enumerate()
{
// Capture processing finishes partway through the current loop.
let capture_end = loop_start + Duration::from_millis(5);
refresh.elapsed_since_encode = capture_end.duration_since(refresh.last_encode);
refresh
.try_encode(
&[1],
spf,
BR_BALANCED,
loop_start,
0,
&mut encoder,
&mut VideoFrameController::new(0),
)
.unwrap();
assert_eq!(state.borrow().calls, index + 1);
}
}
}
#[test]
fn no_output_budget_is_cumulative_and_restarts_with_a_new_frame() {
let sp = GenericService::new("repeat-no-output-budget-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for codec in [CodecFormat::VP8, CodecFormat::VP9] {
let mut refresh = new_refresh(VideoSource::Monitor, codec, &sp, &recorder, 0, 1, 1);
let mut outcomes = Vec::new();
for _ in 0..10 {
outcomes.push(Outcome::Frame);
for _ in 0..5 {
outcomes.extend([Outcome::Empty, Outcome::Dropped]);
}
}
outcomes.push(Outcome::Empty);
outcomes.push(Outcome::Frame);
let (mut encoder, state) = encoder(&outcomes);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
for _ in 0..111 {
attempt(&mut refresh, &mut encoder);
}
assert_eq!(state.borrow().calls, 110, "{codec:?}");
assert_eq!(state.borrow().disabled, 0);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(false);
attempt(&mut refresh, &mut encoder);
assert_eq!(state.borrow().calls, 110);
refresh.on_encoded(true);
attempt(&mut refresh, &mut encoder);
assert_eq!(state.borrow().calls, 111);
attempt(&mut refresh, &mut encoder);
assert_eq!(state.borrow().calls, 112);
}
}
#[test]
fn repeat_error_limits_are_codec_specific() {
let sp = GenericService::new("repeat-codec-errors-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
for (codec, expected_calls) in [
(CodecFormat::VP8, 100),
(CodecFormat::VP9, 100),
(CodecFormat::AV1, 0),
(CodecFormat::H264, 3),
(CodecFormat::H265, 3),
] {
let mut refresh = new_refresh(VideoSource::Monitor, codec, &sp, &recorder, 0, 1, 1);
let limit = MAX_REPEAT_NO_OUTPUTS;
let (mut encoder, state) = encoder(&vec![Outcome::Error; limit]);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
for _ in 0..=limit {
attempt(&mut refresh, &mut encoder);
}
assert_eq!(state.borrow().calls, expected_calls, "{codec:?}");
assert_eq!(state.borrow().disabled, 0);
}
}
#[test]
fn repeats_leave_normal_failure_tracking_and_switching_intact() {
use super::super::handle_one_frame;
use Outcome::*;
let sp = GenericService::new("repeat-normal-errors-test".to_owned(), false);
let recorder = Arc::new(Mutex::new(None));
let mut refresh = new_refresh(
VideoSource::Monitor,
CodecFormat::H264,
&sp,
&recorder,
0,
1,
1,
);
let normal_limit = if cfg!(target_os = "android") { 9 } else { 3 };
let mut outcomes = vec![Error, Error, Frame];
outcomes.extend(vec![Error; normal_limit - 1]);
let (mut encoder, state) = encoder(&outcomes);
refresh.on_frame(&EncodeInput::YUV(&[1]));
refresh.on_encoded(true);
let mut normal_failures = 0;
let mut first_frame = false;
let mut encoder_warmed_up = false;
handle_one_frame(
0,
&sp,
EncodeInput::YUV(&[1]),
0,
&mut encoder,
recorder.clone(),
&mut normal_failures,
&mut first_frame,
&mut encoder_warmed_up,
1,
1,
)
.unwrap();
attempt(&mut refresh, &mut encoder);
attempt(&mut refresh, &mut encoder);
assert_eq!(state.borrow().disabled, 0);
for _ in 2..normal_limit {
handle_one_frame(
0,
&sp,
EncodeInput::YUV(&[1]),
0,
&mut encoder,
recorder.clone(),
&mut normal_failures,
&mut first_frame,
&mut encoder_warmed_up,
1,
1,
)
.unwrap();
}
let result = handle_one_frame(
0,
&sp,
EncodeInput::YUV(&[1]),
0,
&mut encoder,
recorder.clone(),
&mut normal_failures,
&mut first_frame,
&mut encoder_warmed_up,
1,
1,
);
assert_eq!(result.unwrap_err().to_string(), "SWITCH");
assert_eq!(state.borrow().disabled, 1);
}