Fix instant replay frame pacing, SIMD color scaling, and active bitrate

This commit is contained in:
katR 2026-09-20 16:01:42 +05:30
parent 91f603918d
commit 683e4be0a1
4 changed files with 217 additions and 96 deletions

2
Cargo.lock generated
View file

@ -3700,7 +3700,7 @@ dependencies = [
[[package]]
name = "scythe"
version = "0.2.15"
version = "0.2.16"
dependencies = [
"ashpd",
"cpal",

View file

@ -1,6 +1,6 @@
[package]
name = "scythe"
version = "0.2.15"
version = "0.2.16"
edition = "2024"
[[bin]]

View file

@ -437,7 +437,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut config = scythe::config::ScytheConfig::load();
replay_state_clone.store(config.replay_enabled, Ordering::SeqCst);
let mut encoder = VideoEncoder::new_with_params(width, height, config.record_bitrate_kbps, config.fps, &config.video_codec)
let active_bitrate = if config.replay_enabled {
config.replay_bitrate_kbps
} else {
config.record_bitrate_kbps
};
let mut encoder = VideoEncoder::new_with_params(width, height, active_bitrate, config.fps, &config.video_codec)
.expect("Failed to init encoder");
let codec_ctx_ptr = encoder.codec_ctx() as usize;
println!("Background recorder engine active and ready ({} fps, replay: {}).", config.fps, config.replay_enabled);
@ -579,11 +584,68 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let stream_start = std::time::Instant::now();
let mut last_video_pts: i64 = -1;
let mut ticker = crossbeam_channel::tick(std::time::Duration::from_nanos(1_000_000_000 / config.fps.max(1) as u64));
let target_frame_duration = std::time::Duration::from_nanos(1_000_000_000 / config.fps.max(1) as u64);
let mut ticker = crossbeam_channel::tick(target_frame_duration);
let mut latest_frame: Option<Frame> = None;
let mut has_new_frame = false;
let mut last_encoded_raw_ptr: usize = 0;
let mut last_malloc_trim = std::time::Instant::now();
let mut next_frame_time = stream_start;
let mut last_frame_encoded_time = stream_start.checked_sub(target_frame_duration).unwrap_or(stream_start);
macro_rules! handle_packets {
($packets_res:expr) => {
match $packets_res {
Ok(packets) => {
for mut pkt in packets {
pkt.set_stream_index(0);
if normal_recording {
if normal_waiting_keyframe && (pkt.is_keyframe() || normal_keyframe_timeout >= 20) {
normal_waiting_keyframe = false;
normal_keyframe_timeout = 0;
rec_base_video_pts = pkt.pts();
rec_base_audio_pts = -1;
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
let filename = scythe::config::ScytheConfig::format_video_filename("Recording", "mp4");
let full_path = config.resolve_save_path(&filename);
let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext);
normal_muxer = unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx).ok() };
println!("Started normal recording to {}", full_path);
} else if normal_waiting_keyframe {
normal_keyframe_timeout += 1;
}
if !normal_waiting_keyframe
&& let Some(muxer) = normal_muxer.as_mut() {
let rebased = pkt.rebased(rec_base_video_pts);
let _ = muxer.write_packet(&rebased);
}
}
if let Some(ref tx) = stream_sender {
if stream_waiting_keyframe && pkt.is_keyframe() {
stream_waiting_keyframe = false;
stream_base_video_pts = pkt.pts();
stream_base_audio_pts = -1;
println!("Livestream keyframe received, broadcasting video...");
}
if !stream_waiting_keyframe {
let rebased = pkt.rebased(stream_base_video_pts);
let _ = tx.try_send(rebased);
}
}
if config.replay_enabled {
ring.push_overwrite(pkt);
}
}
}
Err(e) => {
eprintln!("Video encoding error: {}", e);
}
}
};
}
loop {
crossbeam_channel::select! {
@ -592,8 +654,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match cmd {
Command::ReloadConfig => {
let new_config = scythe::config::ScytheConfig::load();
if new_config.show_cursor != config.show_cursor {
println!("Cursor display changed ({} -> {}). Restarting daemon for new capture session...", config.show_cursor, new_config.show_cursor);
let video_changed = new_config.show_cursor != config.show_cursor
|| new_config.fps != config.fps
|| new_config.video_codec != config.video_codec
|| new_config.record_bitrate_kbps != config.record_bitrate_kbps
|| new_config.replay_bitrate_kbps != config.replay_bitrate_kbps
|| new_config.replay_enabled != config.replay_enabled;
if video_changed {
println!("Core video settings changed (fps/codec/bitrate/cursor/replay). Restarting daemon for new capture session...");
if let Some(mut m) = normal_muxer.take() {
let _ = m.finalize();
}
@ -881,12 +950,72 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
},
recv(frame_rx) -> frame_res => {
if let Ok(f) = frame_res {
latest_frame = Some(f);
let mut current = f;
while let Ok(newer) = frame_rx.try_recv() {
current = newer;
}
latest_frame = Some(current);
has_new_frame = true;
}
while let Ok(f) = frame_rx.try_recv() {
latest_frame = Some(f);
has_new_frame = true;
let is_streaming = stream_sender.is_some();
if !normal_recording && !config.replay_enabled && !is_streaming {
has_new_frame = false;
continue;
}
let now = std::time::Instant::now();
let jitter_margin = target_frame_duration / 8;
if now + jitter_margin >= next_frame_time {
let elapsed = now.duration_since(stream_start);
let raw_pts = (elapsed.as_secs_f64() * config.fps as f64).round() as i64;
let pts = if (raw_pts - last_video_pts).abs() <= 1 {
last_video_pts + 1
} else if raw_pts > last_video_pts {
raw_pts
} else {
last_video_pts + 1
};
last_video_pts = pts;
last_frame_encoded_time = now;
if next_frame_time + target_frame_duration <= now {
next_frame_time = now + target_frame_duration;
} else {
next_frame_time += target_frame_duration;
}
let is_same_raw_frame = match &latest_frame {
Some(Frame::Raw { data, .. }) => {
let ptr = Arc::as_ptr(data) as usize;
ptr != 0 && ptr == last_encoded_raw_ptr
}
_ => false,
};
has_new_frame = false;
let packets_res = if !is_same_raw_frame {
if let Some(ref f) = latest_frame {
if let Frame::Raw { data, .. } = f {
last_encoded_raw_ptr = Arc::as_ptr(data) as usize;
}
encoder.encode_frame(f, pts)
} else {
Ok(Vec::new())
}
} else {
match encoder.encode_cached_frame(pts) {
Ok(pkts) => Ok(pkts),
Err(_) => {
if let Some(ref f) = latest_frame {
encoder.encode_frame(f, pts)
} else {
Ok(Vec::new())
}
}
}
};
handle_packets!(packets_res);
}
},
recv(ticker) -> _ => {
@ -911,96 +1040,63 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
let now = std::time::Instant::now();
let elapsed = now.duration_since(stream_start);
let raw_pts = (elapsed.as_secs_f64() * config.fps as f64).round() as i64;
let pts = if raw_pts > last_video_pts { raw_pts } else { last_video_pts + 1 };
last_video_pts = pts;
let is_same_raw_frame = match &latest_frame {
Some(Frame::Raw { data, .. }) => {
let ptr = Arc::as_ptr(data) as usize;
ptr != 0 && ptr == last_encoded_raw_ptr
}
_ => false,
};
let packets_res = if has_new_frame && !is_same_raw_frame {
has_new_frame = false;
if let Some(ref f) = latest_frame {
if let Frame::Raw { data, .. } = f {
last_encoded_raw_ptr = Arc::as_ptr(data) as usize;
}
encoder.encode_frame(f, pts)
let jitter_margin = target_frame_duration / 8;
if now.duration_since(last_frame_encoded_time) >= target_frame_duration.saturating_sub(jitter_margin) {
let elapsed = now.duration_since(stream_start);
let raw_pts = (elapsed.as_secs_f64() * config.fps as f64).round() as i64;
let pts = if (raw_pts - last_video_pts).abs() <= 1 {
last_video_pts + 1
} else if raw_pts > last_video_pts {
raw_pts
} else {
Ok(Vec::new())
last_video_pts + 1
};
last_video_pts = pts;
last_frame_encoded_time = now;
if next_frame_time + target_frame_duration <= now {
next_frame_time = now + target_frame_duration;
} else {
next_frame_time += target_frame_duration;
}
} else {
has_new_frame = false;
match encoder.encode_cached_frame(pts) {
Ok(pkts) => Ok(pkts),
Err(_) => {
if let Some(ref f) = latest_frame {
if let Frame::Raw { data, .. } = f {
last_encoded_raw_ptr = Arc::as_ptr(data) as usize;
let is_same_raw_frame = match &latest_frame {
Some(Frame::Raw { data, .. }) => {
let ptr = Arc::as_ptr(data) as usize;
ptr != 0 && ptr == last_encoded_raw_ptr
}
_ => false,
};
let packets_res = if has_new_frame && !is_same_raw_frame {
has_new_frame = false;
if let Some(ref f) = latest_frame {
if let Frame::Raw { data, .. } = f {
last_encoded_raw_ptr = Arc::as_ptr(data) as usize;
}
encoder.encode_frame(f, pts)
} else {
Ok(Vec::new())
}
} else {
has_new_frame = false;
match encoder.encode_cached_frame(pts) {
Ok(pkts) => Ok(pkts),
Err(_) => {
if let Some(ref f) = latest_frame {
if let Frame::Raw { data, .. } = f {
last_encoded_raw_ptr = Arc::as_ptr(data) as usize;
}
encoder.encode_frame(f, pts)
} else {
Ok(Vec::new())
}
encoder.encode_frame(f, pts)
} else {
Ok(Vec::new())
}
}
}
};
};
match packets_res {
Ok(packets) => {
for mut pkt in packets {
pkt.set_stream_index(0);
if normal_recording {
if normal_waiting_keyframe && (pkt.is_keyframe() || normal_keyframe_timeout >= 20) {
normal_waiting_keyframe = false;
normal_keyframe_timeout = 0;
rec_base_video_pts = pkt.pts();
rec_base_audio_pts = -1;
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
let filename = scythe::config::ScytheConfig::format_video_filename("Recording", "mp4");
let full_path = config.resolve_save_path(&filename);
let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext);
normal_muxer = unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx).ok() };
println!("Started normal recording to {}", full_path);
} else if normal_waiting_keyframe {
normal_keyframe_timeout += 1;
}
if !normal_waiting_keyframe
&& let Some(muxer) = normal_muxer.as_mut() {
let rebased = pkt.rebased(rec_base_video_pts);
let _ = muxer.write_packet(&rebased);
}
}
if let Some(ref tx) = stream_sender {
if stream_waiting_keyframe && pkt.is_keyframe() {
stream_waiting_keyframe = false;
stream_base_video_pts = pkt.pts();
stream_base_audio_pts = -1;
println!("Livestream keyframe received, broadcasting video...");
}
if !stream_waiting_keyframe {
let rebased = pkt.rebased(stream_base_video_pts);
let _ = tx.try_send(rebased);
}
}
if config.replay_enabled {
ring.push_overwrite(pkt);
}
}
}
Err(e) => {
eprintln!("Video encoding error: {}", e);
}
handle_packets!(packets_res);
}
}
}
}
}
});

View file

@ -110,7 +110,7 @@ impl VaapiEncoder {
(*frames_ctx).sw_format = AVPixelFormat::AV_PIX_FMT_NV12;
(*frames_ctx).width = width as i32;
(*frames_ctx).height = height as i32;
(*frames_ctx).initial_pool_size = 6;
(*frames_ctx).initial_pool_size = 20;
let ret = av_hwframe_ctx_init(hw_frames_ref);
if ret < 0 {
@ -154,8 +154,8 @@ impl VaapiEncoder {
width as i32,
height as i32,
AVPixelFormat::AV_PIX_FMT_NV12,
// SWS_BICUBIC (4) | SWS_ACCURATE_RND (0x40000)
4 | 0x40000,
// SWS_FAST_BILINEAR (1) enables AVX2/SSSE3 SIMD assembly for sub-3ms color conversion
1,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
@ -971,4 +971,29 @@ mod tests {
}
}
}
#[test]
fn test_vaapi_encoder_60fps_throughput() {
if let Ok(mut enc) = VaapiEncoder::new_with_params(1920, 1080, 20_000, 60, "h264") {
let blank_frame = Frame::Raw {
width: 1920,
height: 1080,
stride: 1920 * 4,
data: std::sync::Arc::new(vec![128u8; 1920 * 1080 * 4]),
};
// Warm up
let _ = enc.encode_frame(&blank_frame, 0);
let iterations = 30;
let start = std::time::Instant::now();
for i in 1..=iterations {
let _ = enc.encode_frame(&blank_frame, i);
}
let elapsed = start.elapsed();
let avg_ms = elapsed.as_secs_f64() * 1000.0 / (iterations as f64);
println!("1080p60 VAAPI frame encode avg time: {:.2} ms per frame", avg_ms);
// Must comfortably beat the 16.66ms deadline for 60 FPS
assert!(avg_ms < 16.0, "Encoding time ({:.2}ms) must be under 16ms for smooth 60fps", avg_ms);
}
}
}