fix(linux): fix DMA-BUF mmap offset, preserve screencast session, and verify replay clipping; bump v0.2.6
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions
This commit is contained in:
parent
6be83b21e6
commit
1b696135c6
9 changed files with 114 additions and 67 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3700,7 +3700,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "scythe"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"cpal",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "scythe"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
|
||||
let (frame_tx, frame_rx) = bounded::<Frame>(5);
|
||||
let (cmd_tx, cmd_rx) = bounded::<Command>(32);
|
||||
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
|
||||
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(4);
|
||||
let (audio_tx, audio_rx) = bounded::<Vec<f32>>(500);
|
||||
|
||||
let is_recording_state = Arc::new(AtomicBool::new(false));
|
||||
|
|
@ -182,7 +182,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
std::process::exit(0);
|
||||
},
|
||||
other => {
|
||||
let _ = cmd_tx_ipc.try_send(other);
|
||||
let _ = cmd_tx_ipc.send(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -277,10 +277,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
};
|
||||
|
||||
let first_frame = match source.next_frame() {
|
||||
let first_frame = match source.next_frame_timeout(std::time::Duration::from_millis(600)) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("Initial frame capture error: {}. Using default frame.", e);
|
||||
Err(_) => {
|
||||
Frame::Raw {
|
||||
data: vec![0u8; 1920 * 1080 * 4],
|
||||
width: 1920,
|
||||
|
|
@ -295,6 +294,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
#[cfg(target_os = "windows")]
|
||||
Frame::D3D11Texture { width, height, .. } => (*width, *height),
|
||||
};
|
||||
println!("Capture stream active: resolution {}x{}", width, height);
|
||||
|
||||
let _ = frame_tx.send(first_frame);
|
||||
|
||||
|
|
@ -329,6 +329,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let mut encoder = VideoEncoder::new_with_params(width, height, config.record_bitrate_kbps, 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);
|
||||
|
||||
let mut audio_encoder = if let Some((sr, ch)) = audio_info {
|
||||
scythe::encoder::AudioEncoder::new(sr as i32, ch as i32).ok()
|
||||
|
|
@ -340,7 +341,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let video_time_base = unsafe { (*(codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext)).time_base };
|
||||
let audio_time_base = audio_codec_ctx_ptr.map(|p| unsafe { (*(p as *mut ffmpeg_next::ffi::AVCodecContext)).time_base });
|
||||
|
||||
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec * 120).max(120) as usize);
|
||||
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec as usize) * (config.fps as usize + 60) + 120);
|
||||
let mut normal_muxer: Option<Muxer> = None;
|
||||
let mut normal_recording = false;
|
||||
let mut normal_waiting_keyframe = false;
|
||||
|
|
@ -433,10 +434,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
match unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx) } {
|
||||
Ok(mut muxer) => {
|
||||
for (_, p) in prepared {
|
||||
let _ = muxer.write_packet(&p);
|
||||
if let Err(e) = muxer.write_packet(&p) {
|
||||
eprintln!("Error writing packet to replay muxer: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Err(e) = muxer.finalize() {
|
||||
eprintln!("Error finalizing replay muxer: {}", e);
|
||||
} else {
|
||||
println!("Replay saved to {}!", full_path);
|
||||
}
|
||||
let _ = muxer.finalize();
|
||||
println!("Replay saved to {}!", full_path);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create muxer for {}: {}", full_path, e);
|
||||
|
|
@ -470,7 +477,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
replay_state_clone.store(config.replay_enabled, Ordering::SeqCst);
|
||||
audio_muted_clone.store(config.audio_mode == "muted", Ordering::SeqCst);
|
||||
println!("Daemon config reloaded! Audio mode: {}", config.audio_mode);
|
||||
let new_capacity = (config.replay_duration_sec * 120).max(120) as usize;
|
||||
let new_capacity = (config.replay_duration_sec as usize) * (config.fps as usize + 60) + 120;
|
||||
if ring.capacity().get() != new_capacity {
|
||||
ring = HeapRb::<Packet>::new(new_capacity);
|
||||
println!("Replay buffer resized to {} packets.", new_capacity);
|
||||
|
|
@ -499,7 +506,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
if config.replay_enabled {
|
||||
let drain = ring.iter().cloned().collect::<Vec<_>>();
|
||||
println!("SaveReplay triggered: {} packets in ring buffer", drain.len());
|
||||
let _ = mux_tx.try_send(drain);
|
||||
if drain.is_empty() {
|
||||
eprintln!("Warning: Replay buffer is empty (no frames encoded yet).");
|
||||
} else {
|
||||
let _ = mux_tx.send(drain);
|
||||
}
|
||||
} else {
|
||||
println!("SaveReplay requested but replay is disabled in daemon config.");
|
||||
}
|
||||
},
|
||||
Command::StartRecording => {
|
||||
|
|
@ -663,7 +676,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -249,6 +249,13 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
if args.len() > 1 {
|
||||
match args[1].as_str() {
|
||||
"--save" => {
|
||||
ensure_daemon_running();
|
||||
if let Ok(st) = query_status() {
|
||||
if !st.is_replay_active {
|
||||
show_shadowplay_toast("INSTANT REPLAY", "Replay is turned off", ToastIcon::Error);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
return send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay);
|
||||
}
|
||||
"--notify-save" => {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ pub enum Frame {
|
|||
|
||||
pub trait FrameSource: Send {
|
||||
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>>;
|
||||
fn next_frame_timeout(&mut self, _timeout: std::time::Duration) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.next_frame()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod mock;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ use ashpd::desktop::screencast::{CursorMode, Screencast, SourceType};
|
|||
use ashpd::desktop::PersistMode;
|
||||
|
||||
pub struct WaylandCapture {
|
||||
_proxy: Screencast,
|
||||
_session: ashpd::desktop::Session<Screencast>,
|
||||
stream: crate::capture::wayland_stream::PipeWireStream,
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +59,7 @@ impl WaylandCapture {
|
|||
|
||||
let stream = crate::capture::wayland_stream::PipeWireStream::new(node_id, fd)?;
|
||||
|
||||
Ok(Self { stream })
|
||||
Ok(Self { _proxy: proxy, _session: session, stream })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,4 +67,8 @@ impl FrameSource for WaylandCapture {
|
|||
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.stream.next_frame()
|
||||
}
|
||||
|
||||
fn next_frame_timeout(&mut self, timeout: std::time::Duration) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.stream.next_frame_timeout(timeout)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,4 +139,8 @@ impl PipeWireStream {
|
|||
pub fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.receiver.recv().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
}
|
||||
|
||||
pub fn next_frame_timeout(&mut self, timeout: std::time::Duration) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.receiver.recv_timeout(timeout).map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
108
src/encoder.rs
108
src/encoder.rs
|
|
@ -13,6 +13,8 @@ pub struct VaapiEncoder {
|
|||
has_frame: bool,
|
||||
#[cfg(unix)]
|
||||
dma_mmap_cache: HashMap<i32, (*mut libc::c_void, usize)>,
|
||||
#[cfg(unix)]
|
||||
direct_drm_unsupported: bool,
|
||||
}
|
||||
|
||||
impl VaapiEncoder {
|
||||
|
|
@ -149,6 +151,8 @@ impl VaapiEncoder {
|
|||
has_frame: false,
|
||||
#[cfg(unix)]
|
||||
dma_mmap_cache: HashMap::new(),
|
||||
#[cfg(unix)]
|
||||
direct_drm_unsupported: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -207,63 +211,68 @@ impl VaapiEncoder {
|
|||
self.send_staged_frame(&mut packets, pts)?;
|
||||
}
|
||||
Frame::DmaBuf { width: dma_width, height: dma_height, format, modifier, fd, stride, offset } => {
|
||||
let mmap_size = (*stride as usize) * (*dma_height as usize);
|
||||
let mmap_size = (*offset as usize) + (*stride as usize) * (*dma_height as usize);
|
||||
let mut mapped_direct = false;
|
||||
|
||||
let desc = av_malloc(std::mem::size_of::<AVDRMFrameDescriptor>()) as *mut AVDRMFrameDescriptor;
|
||||
if !desc.is_null() {
|
||||
*desc = AVDRMFrameDescriptor {
|
||||
nb_objects: 1,
|
||||
objects: [AVDRMObjectDescriptor {
|
||||
fd: *fd,
|
||||
size: mmap_size,
|
||||
format_modifier: *modifier,
|
||||
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
|
||||
nb_layers: 1,
|
||||
layers: [AVDRMLayerDescriptor {
|
||||
format: *format,
|
||||
nb_planes: 1,
|
||||
planes: [AVDRMPlaneDescriptor {
|
||||
object_index: 0,
|
||||
offset: *offset as isize,
|
||||
pitch: *stride as isize,
|
||||
#[cfg(unix)]
|
||||
if !self.direct_drm_unsupported {
|
||||
let desc = av_malloc(std::mem::size_of::<AVDRMFrameDescriptor>()) as *mut AVDRMFrameDescriptor;
|
||||
if !desc.is_null() {
|
||||
*desc = AVDRMFrameDescriptor {
|
||||
nb_objects: 1,
|
||||
objects: [AVDRMObjectDescriptor {
|
||||
fd: *fd,
|
||||
size: mmap_size,
|
||||
format_modifier: *modifier,
|
||||
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
|
||||
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
|
||||
};
|
||||
nb_layers: 1,
|
||||
layers: [AVDRMLayerDescriptor {
|
||||
format: *format,
|
||||
nb_planes: 1,
|
||||
planes: [AVDRMPlaneDescriptor {
|
||||
object_index: 0,
|
||||
offset: *offset as isize,
|
||||
pitch: *stride as isize,
|
||||
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
|
||||
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
|
||||
};
|
||||
|
||||
let mut drm_frame = av_frame_alloc();
|
||||
(*drm_frame).format = AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
|
||||
(*drm_frame).width = *dma_width as i32;
|
||||
(*drm_frame).height = *dma_height as i32;
|
||||
(*drm_frame).data[0] = desc as *mut u8;
|
||||
(*drm_frame).buf[0] = av_buffer_create(
|
||||
desc as *mut u8,
|
||||
std::mem::size_of::<AVDRMFrameDescriptor>(),
|
||||
None,
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
);
|
||||
let mut drm_frame = av_frame_alloc();
|
||||
(*drm_frame).format = AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
|
||||
(*drm_frame).width = *dma_width as i32;
|
||||
(*drm_frame).height = *dma_height as i32;
|
||||
(*drm_frame).data[0] = desc as *mut u8;
|
||||
(*drm_frame).buf[0] = av_buffer_create(
|
||||
desc as *mut u8,
|
||||
std::mem::size_of::<AVDRMFrameDescriptor>(),
|
||||
None,
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
);
|
||||
|
||||
let mut direct_hw = av_frame_alloc();
|
||||
(*direct_hw).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
|
||||
(*direct_hw).hw_frames_ctx = av_buffer_ref(self.hw_frames_ctx);
|
||||
let mut direct_hw = av_frame_alloc();
|
||||
(*direct_hw).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
|
||||
(*direct_hw).hw_frames_ctx = av_buffer_ref(self.hw_frames_ctx);
|
||||
|
||||
if av_hwframe_map(direct_hw, drm_frame, 0) >= 0 {
|
||||
(*direct_hw).pts = pts;
|
||||
if av_hwframe_map(direct_hw, drm_frame, 0) >= 0 {
|
||||
(*direct_hw).pts = pts;
|
||||
|
||||
if avcodec_send_frame(self.codec_ctx, direct_hw) >= 0 {
|
||||
let mut pkt = av_packet_alloc();
|
||||
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
|
||||
let new_pkt = av_packet_alloc();
|
||||
av_packet_move_ref(new_pkt, pkt);
|
||||
packets.push(crate::ring::Packet::new(new_pkt));
|
||||
if avcodec_send_frame(self.codec_ctx, direct_hw) >= 0 {
|
||||
let mut pkt = av_packet_alloc();
|
||||
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
|
||||
let new_pkt = av_packet_alloc();
|
||||
av_packet_move_ref(new_pkt, pkt);
|
||||
packets.push(crate::ring::Packet::new(new_pkt));
|
||||
}
|
||||
av_packet_free(&mut pkt);
|
||||
mapped_direct = true;
|
||||
}
|
||||
av_packet_free(&mut pkt);
|
||||
mapped_direct = true;
|
||||
} else {
|
||||
self.direct_drm_unsupported = true;
|
||||
}
|
||||
av_frame_free(&mut direct_hw);
|
||||
av_frame_free(&mut drm_frame);
|
||||
}
|
||||
av_frame_free(&mut direct_hw);
|
||||
av_frame_free(&mut drm_frame);
|
||||
}
|
||||
|
||||
if !mapped_direct {
|
||||
|
|
@ -278,7 +287,7 @@ impl VaapiEncoder {
|
|||
libc::PROT_READ,
|
||||
libc::MAP_SHARED,
|
||||
*fd,
|
||||
*offset as libc::off_t,
|
||||
0,
|
||||
);
|
||||
if p != libc::MAP_FAILED {
|
||||
if self.dma_mmap_cache.len() >= 16 {
|
||||
|
|
@ -295,7 +304,8 @@ impl VaapiEncoder {
|
|||
};
|
||||
|
||||
if ptr != libc::MAP_FAILED {
|
||||
let src_slices: [*const u8; 4] = [ptr as *const u8, ptr::null(), ptr::null(), ptr::null()];
|
||||
let frame_ptr = (ptr as *const u8).add(*offset as usize);
|
||||
let src_slices: [*const u8; 4] = [frame_ptr, ptr::null(), ptr::null(), ptr::null()];
|
||||
let src_strides: [i32; 4] = [*stride as i32, 0, 0, 0];
|
||||
sws_scale(
|
||||
self.sws_ctx,
|
||||
|
|
|
|||
|
|
@ -1027,9 +1027,13 @@ impl ScytheOverlayApp {
|
|||
self.status.is_replay_active = cfg.replay_enabled;
|
||||
self.replay_dropdown_open = false;
|
||||
}
|
||||
if render_menu_item(ui, "Save Replay", accent, true) {
|
||||
async_send_command(Command::SaveReplay);
|
||||
self.show_hud_notification("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay);
|
||||
if render_menu_item(ui, "Save Replay", accent, is_replay_active) {
|
||||
if is_replay_active {
|
||||
async_send_command(Command::SaveReplay);
|
||||
self.show_hud_notification("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay);
|
||||
} else {
|
||||
self.show_hud_notification("INSTANT REPLAY", "Replay is turned off", crate::overlay::ToastIcon::Error);
|
||||
}
|
||||
self.replay_dropdown_open = false;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue