feat(ui): complete overhaul of settings modal and notification engine
This commit is contained in:
parent
0925b65d63
commit
4d28bb7ed5
15 changed files with 330 additions and 68 deletions
|
|
@ -1,4 +1,5 @@
|
|||
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
|
||||
#![allow(clippy::chunks_exact_to_as_chunks)]
|
||||
|
||||
use scythe::capture::{Frame, FrameSource};
|
||||
use scythe::encoder::VideoEncoder;
|
||||
|
|
@ -105,7 +106,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
{
|
||||
Ok(file) => {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
let res = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
|
||||
let mut res = -1;
|
||||
for _ in 0..15 {
|
||||
res = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
|
||||
if res == 0 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(80));
|
||||
}
|
||||
if res != 0 {
|
||||
eprintln!("Another instance of scythe-daemon is already running. Exiting.");
|
||||
return Ok(());
|
||||
|
|
@ -616,8 +624,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
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);
|
||||
match unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx) } {
|
||||
Ok(m) => {
|
||||
normal_muxer = Some(m);
|
||||
println!("Started normal recording to {}", full_path);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to start normal recording to {}: {}", full_path, e);
|
||||
normal_recording = false;
|
||||
rec_state_clone.store(false, Ordering::SeqCst);
|
||||
rec_start_clone.store(0, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
} else if normal_waiting_keyframe {
|
||||
normal_keyframe_timeout += 1;
|
||||
}
|
||||
|
|
@ -663,17 +681,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let new_config = scythe::config::ScytheConfig::load();
|
||||
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;
|
||||
|| new_config.video_codec != config.video_codec;
|
||||
|
||||
if video_changed {
|
||||
println!("Core video settings changed (fps/codec/bitrate/cursor/replay). Restarting daemon for new capture session...");
|
||||
println!("Core video settings changed (fps/codec/cursor). Restarting daemon for new capture session...");
|
||||
if let Some(mut m) = normal_muxer.take() {
|
||||
let _ = m.finalize();
|
||||
}
|
||||
let _ = stream_sender.take();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if let Ok(status) = std::process::Command::new("systemctl")
|
||||
.args(["--user", "is-active", "--quiet", "scythe-daemon.service"])
|
||||
.status()
|
||||
&& status.success() {
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(["--user", "restart", "scythe-daemon.service"])
|
||||
.spawn();
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
let mut cmd = std::process::Command::new(exe);
|
||||
cmd.stdin(std::process::Stdio::null())
|
||||
|
|
@ -748,6 +776,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
normal_recording = true;
|
||||
normal_waiting_keyframe = true;
|
||||
normal_keyframe_timeout = 0;
|
||||
has_new_frame = true;
|
||||
encoder.force_keyframe();
|
||||
rec_state_clone.store(true, Ordering::SeqCst);
|
||||
rec_base_audio_pts = -1;
|
||||
|
|
@ -790,6 +819,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
normal_recording = true;
|
||||
normal_waiting_keyframe = true;
|
||||
normal_keyframe_timeout = 0;
|
||||
has_new_frame = true;
|
||||
encoder.force_keyframe();
|
||||
rec_state_clone.store(true, Ordering::SeqCst);
|
||||
rec_base_audio_pts = -1;
|
||||
|
|
@ -976,9 +1006,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
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 {
|
||||
let pts = if last_video_pts < 0 {
|
||||
0
|
||||
} else if raw_pts > last_video_pts + 4 {
|
||||
raw_pts
|
||||
} else {
|
||||
last_video_pts + 1
|
||||
|
|
@ -1051,9 +1081,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
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 {
|
||||
let pts = if last_video_pts < 0 {
|
||||
0
|
||||
} else if raw_pts > last_video_pts + 4 {
|
||||
raw_pts
|
||||
} else {
|
||||
last_video_pts + 1
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
|
||||
#![allow(clippy::chunks_exact_to_as_chunks)]
|
||||
|
||||
use scythe::ipc::{Command, send_command, query_status};
|
||||
use scythe::overlay::{show_shadowplay_toast, ToastIcon};
|
||||
|
|
@ -122,14 +123,36 @@ fn check_and_toggle_overlay() -> bool {
|
|||
if let Ok(pid) = content.trim().parse::<u32>() {
|
||||
if pid != std::process::id() {
|
||||
unsafe {
|
||||
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
|
||||
use windows::Win32::Foundation::CloseHandle;
|
||||
if let Ok(h) = OpenProcess(PROCESS_TERMINATE, false, pid) {
|
||||
let _ = TerminateProcess(h, 0);
|
||||
let _ = CloseHandle(h);
|
||||
let _ = std::fs::remove_file(&pid_path);
|
||||
return true;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
EnumWindows, GetWindowThreadProcessId, PostMessageW, WM_CLOSE,
|
||||
};
|
||||
use windows::Win32::Foundation::{BOOL, HWND, LPARAM, CloseHandle};
|
||||
use windows::Win32::System::Threading::{
|
||||
OpenProcess, TerminateProcess, PROCESS_TERMINATE, PROCESS_SYNCHRONIZE, WaitForSingleObject,
|
||||
};
|
||||
|
||||
unsafe extern "system" fn close_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let target = lparam.0 as u32;
|
||||
let mut pid = 0u32;
|
||||
GetWindowThreadProcessId(hwnd, Some(&mut pid));
|
||||
if pid == target {
|
||||
let _ = PostMessageW(hwnd, WM_CLOSE, None, None);
|
||||
}
|
||||
BOOL(1)
|
||||
}
|
||||
|
||||
let _ = EnumWindows(Some(close_proc), LPARAM(pid as isize));
|
||||
|
||||
// Wait up to 150ms for graceful shutdown before fallback termination
|
||||
if let Ok(h) = OpenProcess(PROCESS_SYNCHRONIZE | PROCESS_TERMINATE, false, pid) {
|
||||
let wait_res = WaitForSingleObject(h, 150);
|
||||
if wait_res.0 != 0 {
|
||||
let _ = TerminateProcess(h, 0);
|
||||
}
|
||||
let _ = CloseHandle(h);
|
||||
}
|
||||
let _ = std::fs::remove_file(&pid_path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -594,6 +617,14 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
});
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
MsgWaitForMultipleObjectsEx, QS_ALLINPUT, MWMO_INPUTAVAILABLE,
|
||||
};
|
||||
let _ = MsgWaitForMultipleObjectsEx(None, 200, QS_ALLINPUT.0, MWMO_INPUTAVAILABLE);
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -528,9 +528,13 @@ impl AudioCapture {
|
|||
let stream_config: cpal::StreamConfig = config.into();
|
||||
let lvl_f32 = Arc::clone(&levels_for_cpal);
|
||||
let lvl_i16 = Arc::clone(&levels_for_cpal);
|
||||
let lvl_i32 = Arc::clone(&levels_for_cpal);
|
||||
let lvl_u16 = Arc::clone(&levels_for_cpal);
|
||||
|
||||
let mut resample_state_f32 = ResampleState::default();
|
||||
let mut resample_state_i16 = ResampleState::default();
|
||||
let mut resample_state_i32 = ResampleState::default();
|
||||
let mut resample_state_u16 = ResampleState::default();
|
||||
|
||||
let stream = match format {
|
||||
cpal::SampleFormat::F32 => {
|
||||
|
|
@ -566,6 +570,40 @@ impl AudioCapture {
|
|||
None,
|
||||
)?
|
||||
}
|
||||
cpal::SampleFormat::I32 => {
|
||||
dev.build_input_stream(
|
||||
stream_config,
|
||||
move |data: &[i32], _: &_| {
|
||||
let converted_f32: Vec<f32> = data.iter().map(|&s| s as f32 / 2147483648.0).collect();
|
||||
let f32_data = convert_to_stereo_48k(&converted_f32, in_channels, in_sample_rate, gain, &mut resample_state_i32);
|
||||
if is_mic {
|
||||
lvl_i32.update_mic(&f32_data);
|
||||
} else {
|
||||
lvl_i32.update_system(&f32_data);
|
||||
}
|
||||
let _ = tx.try_send(f32_data);
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?
|
||||
}
|
||||
cpal::SampleFormat::U16 => {
|
||||
dev.build_input_stream(
|
||||
stream_config,
|
||||
move |data: &[u16], _: &_| {
|
||||
let converted_f32: Vec<f32> = data.iter().map(|&s| (s as f32 - 32768.0) / 32768.0).collect();
|
||||
let f32_data = convert_to_stereo_48k(&converted_f32, in_channels, in_sample_rate, gain, &mut resample_state_u16);
|
||||
if is_mic {
|
||||
lvl_u16.update_mic(&f32_data);
|
||||
} else {
|
||||
lvl_u16.update_system(&f32_data);
|
||||
}
|
||||
let _ = tx.try_send(f32_data);
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?
|
||||
}
|
||||
_ => return Err("Unsupported audio format".into()),
|
||||
};
|
||||
stream.play()?;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ pub struct WaylandCapture {
|
|||
_proxy: Screencast,
|
||||
_session: ashpd::desktop::Session<Screencast>,
|
||||
stream: crate::capture::wayland_stream::PipeWireStream,
|
||||
show_cursor: bool,
|
||||
last_frame: Option<Frame>,
|
||||
last_reconnect_attempt: std::time::Instant,
|
||||
}
|
||||
|
||||
impl WaylandCapture {
|
||||
|
|
@ -59,16 +62,79 @@ impl WaylandCapture {
|
|||
|
||||
let stream = crate::capture::wayland_stream::PipeWireStream::new(node_id, fd)?;
|
||||
|
||||
Ok(Self { _proxy: proxy, _session: session, stream })
|
||||
Ok(Self {
|
||||
_proxy: proxy,
|
||||
_session: session,
|
||||
stream,
|
||||
show_cursor,
|
||||
last_frame: None,
|
||||
last_reconnect_attempt: std::time::Instant::now() - std::time::Duration::from_secs(5),
|
||||
})
|
||||
}
|
||||
|
||||
fn try_reconnect(&mut self) -> bool {
|
||||
if self.last_reconnect_attempt.elapsed() < std::time::Duration::from_millis(1500) {
|
||||
return false;
|
||||
}
|
||||
self.last_reconnect_attempt = std::time::Instant::now();
|
||||
eprintln!("Wayland PipeWire stream disconnected. Attempting auto-reconnect...");
|
||||
|
||||
let cursor = self.show_cursor;
|
||||
// Run on an independent OS thread so block_on never conflicts with Tokio runtime threads
|
||||
let res = std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.ok()?;
|
||||
rt.block_on(WaylandCapture::new_with_cursor(cursor)).ok()
|
||||
}).join();
|
||||
|
||||
if let Ok(Some(new_cap)) = res {
|
||||
eprintln!("Wayland ScreenCast session reconnected successfully.");
|
||||
self._proxy = new_cap._proxy;
|
||||
self._session = new_cap._session;
|
||||
self.stream = new_cap.stream;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameSource for WaylandCapture {
|
||||
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.stream.next_frame()
|
||||
match self.stream.next_frame() {
|
||||
Ok(f) => {
|
||||
self.last_frame = Some(f.clone());
|
||||
Ok(f)
|
||||
}
|
||||
Err(e) => {
|
||||
if self.try_reconnect() {
|
||||
return self.stream.next_frame();
|
||||
}
|
||||
if let Some(ref f) = self.last_frame {
|
||||
return Ok(f.clone());
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
match self.stream.next_frame_timeout(timeout) {
|
||||
Ok(f) => {
|
||||
self.last_frame = Some(f.clone());
|
||||
Ok(f)
|
||||
}
|
||||
Err(e) => {
|
||||
if self.try_reconnect() {
|
||||
return self.stream.next_frame_timeout(timeout);
|
||||
}
|
||||
if let Some(ref f) = self.last_frame {
|
||||
return Ok(f.clone());
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ pub struct WindowsCapture {
|
|||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub last_stride: u32,
|
||||
buffer_pool: Vec<Option<Arc<Vec<u8>>>>,
|
||||
buffer_pool: Vec<Arc<Vec<u8>>>,
|
||||
last_frame: Option<Arc<Vec<u8>>>,
|
||||
last_reinit_attempt: std::time::Instant,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
@ -52,13 +53,21 @@ impl WindowsCapture {
|
|||
|
||||
let mut a_idx = 0;
|
||||
'adapter_loop: while let Ok(adapter) = factory.EnumAdapters1(a_idx) {
|
||||
if let Ok(desc1) = adapter.GetDesc1() {
|
||||
// Skip software rasterizers (e.g. Microsoft Basic Render Driver) when hardware GPU exists
|
||||
if (desc1.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0) != 0 {
|
||||
a_idx += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let mut o_idx = 0;
|
||||
while let Ok(output) = adapter.EnumOutputs(o_idx) {
|
||||
if let Ok(output1) = output.cast::<IDXGIOutput1>() {
|
||||
if let Ok(desc) = output1.GetDesc() {
|
||||
let is_attached = desc.AttachedToDesktop.as_bool();
|
||||
let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs();
|
||||
let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
|
||||
if width > 0 && height > 0 {
|
||||
if is_attached && width > 0 && height > 0 {
|
||||
if let Ok(adapter_base) = adapter.cast::<IDXGIAdapter>() {
|
||||
let mut device: Option<ID3D11Device> = None;
|
||||
let mut context: Option<ID3D11DeviceContext> = None;
|
||||
|
|
@ -133,15 +142,19 @@ impl WindowsCapture {
|
|||
width,
|
||||
height,
|
||||
last_stride: width * 4,
|
||||
buffer_pool: Vec::with_capacity(4),
|
||||
buffer_pool: Vec::with_capacity(8),
|
||||
last_frame: None,
|
||||
last_reinit_attempt: std::time::Instant::now() - std::time::Duration::from_secs(5),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn reinit_duplication(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if self.last_reinit_attempt.elapsed() < std::time::Duration::from_millis(250) {
|
||||
return Err("Reinitialization throttled".into());
|
||||
}
|
||||
self.last_reinit_attempt = std::time::Instant::now();
|
||||
self.duplication = None;
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
if let Ok(new_cap) = WindowsCapture::new() {
|
||||
self.device = new_cap.device;
|
||||
self.context = new_cap.context;
|
||||
|
|
@ -196,14 +209,12 @@ impl FrameSource for WindowsCapture {
|
|||
let target_stride = (self.width * 4) as usize;
|
||||
let total_bytes = target_stride * self.height as usize;
|
||||
|
||||
// Acquire buffer from pool
|
||||
// Acquire buffer from pool or allocate if pool has room
|
||||
let mut arc_buf = None;
|
||||
for slot in &mut self.buffer_pool {
|
||||
if let Some(buf) = slot {
|
||||
if Arc::strong_count(buf) == 1 {
|
||||
arc_buf = slot.take();
|
||||
break;
|
||||
}
|
||||
for i in 0..self.buffer_pool.len() {
|
||||
if Arc::strong_count(&self.buffer_pool[i]) == 1 {
|
||||
arc_buf = Some(self.buffer_pool.swap_remove(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut arc_buf = arc_buf.unwrap_or_else(|| Arc::new(vec![0u8; total_bytes]));
|
||||
|
|
@ -234,8 +245,8 @@ impl FrameSource for WindowsCapture {
|
|||
self.last_frame = Some(Arc::clone(&arc_buf));
|
||||
self.last_stride = target_stride as u32;
|
||||
|
||||
if self.buffer_pool.len() < 4 {
|
||||
self.buffer_pool.push(Some(arc_buf));
|
||||
if self.buffer_pool.len() < 8 {
|
||||
self.buffer_pool.push(arc_buf);
|
||||
}
|
||||
|
||||
return Ok(Frame::Raw {
|
||||
|
|
@ -254,14 +265,12 @@ impl FrameSource for WindowsCapture {
|
|||
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST
|
||||
|| e.code() == DXGI_ERROR_ACCESS_DENIED
|
||||
|| e.code() == DXGI_ERROR_INVALID_CALL => {
|
||||
let _ = dup.ReleaseFrame();
|
||||
drop(dup);
|
||||
self.duplication = None;
|
||||
let _ = self.reinit_duplication();
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = dup.ReleaseFrame();
|
||||
drop(dup);
|
||||
self.duplication = None;
|
||||
let _ = self.reinit_duplication();
|
||||
|
|
|
|||
|
|
@ -66,19 +66,21 @@ impl VaapiEncoder {
|
|||
(*codec_ctx).gop_size = fps; // Emit IDR keyframe every second
|
||||
(*codec_ctx).max_b_frames = 0; // Zero latency, no frame reordering
|
||||
|
||||
// High Quality & Clean VBR Rate Control
|
||||
// High Quality & Clean VBR Rate Control matching GPU Screen Recorder standards
|
||||
let rate = (bitrate_kbps as i64) * 1000;
|
||||
(*codec_ctx).bit_rate = rate;
|
||||
(*codec_ctx).rc_max_rate = rate * 3 / 2; // Allow burst for fast motion scenes
|
||||
(*codec_ctx).rc_buffer_size = (rate / 2) as i32;
|
||||
(*codec_ctx).qmin = 16;
|
||||
(*codec_ctx).qmax = 28; // Prevent compression blockiness
|
||||
(*codec_ctx).rc_max_rate = rate * 2; // Allow 2x burst for high-motion gaming scenes
|
||||
(*codec_ctx).rc_buffer_size = (rate * 2) as i32; // 2 seconds rate control window
|
||||
(*codec_ctx).qmin = 1; // Allow high-fidelity crisp detail
|
||||
(*codec_ctx).qmax = 32;
|
||||
(*codec_ctx).profile = match codec_pref.to_lowercase().as_str() {
|
||||
"hevc" | "h265" => FF_PROFILE_HEVC_MAIN,
|
||||
"av1" => FF_PROFILE_UNKNOWN,
|
||||
_ => FF_PROFILE_H264_HIGH,
|
||||
};
|
||||
(*codec_ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int;
|
||||
let _ = av_opt_set((*codec_ctx).priv_data, c"rc_mode".as_ptr(), c"VBR".as_ptr(), 0);
|
||||
let _ = av_opt_set_int((*codec_ctx).priv_data, c"async_depth".as_ptr(), 4, 0);
|
||||
|
||||
// Rec.709 Color Space & MPEG Limited Range (Broadcast / Web standard, avoids yellow tint)
|
||||
(*codec_ctx).colorspace = AVColorSpace::AVCOL_SPC_BT709;
|
||||
|
|
@ -310,6 +312,7 @@ impl VaapiEncoder {
|
|||
}
|
||||
av_packet_free(&mut pkt);
|
||||
mapped_direct = true;
|
||||
self.has_frame = true;
|
||||
}
|
||||
} else {
|
||||
self.direct_drm_unsupported = true;
|
||||
|
|
@ -378,7 +381,7 @@ impl VaapiEncoder {
|
|||
pub fn encode_cached_frame(&mut self, pts: i64) -> Result<Vec<crate::ring::Packet>, String> {
|
||||
let mut packets = Vec::new();
|
||||
if !self.has_frame {
|
||||
return Ok(packets);
|
||||
return Err("No cached frame available in encoder".into());
|
||||
}
|
||||
unsafe {
|
||||
self.send_staged_frame(&mut packets, pts)?;
|
||||
|
|
@ -498,10 +501,10 @@ impl WindowsHwEncoder {
|
|||
|
||||
let rate = (bitrate_kbps as i64) * 1000;
|
||||
(*ctx).bit_rate = rate;
|
||||
(*ctx).rc_max_rate = rate * 3 / 2;
|
||||
(*ctx).rc_buffer_size = (rate / 2) as i32;
|
||||
(*ctx).qmin = 16;
|
||||
(*ctx).qmax = 28;
|
||||
(*ctx).rc_max_rate = rate * 2;
|
||||
(*ctx).rc_buffer_size = (rate * 2) as i32;
|
||||
(*ctx).qmin = 1;
|
||||
(*ctx).qmax = 32;
|
||||
let codec_id = (*c).id;
|
||||
(*ctx).profile = if codec_id == AVCodecID::AV_CODEC_ID_HEVC {
|
||||
FF_PROFILE_HEVC_MAIN
|
||||
|
|
@ -520,11 +523,12 @@ impl WindowsHwEncoder {
|
|||
(*ctx).color_range = AVColorRange::AVCOL_RANGE_MPEG;
|
||||
|
||||
if desc.contains("NVENC") {
|
||||
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"p1".as_ptr(), 0);
|
||||
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"p3".as_ptr(), 0);
|
||||
let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"ull".as_ptr(), 0);
|
||||
let _ = av_opt_set((*ctx).priv_data, c"forced-idr".as_ptr(), c"1".as_ptr(), 0);
|
||||
} else if desc.contains("AMF") {
|
||||
let _ = av_opt_set((*ctx).priv_data, c"usage".as_ptr(), c"ultralowlatency".as_ptr(), 0);
|
||||
let _ = av_opt_set((*ctx).priv_data, c"rc".as_ptr(), c"vbr_latency".as_ptr(), 0);
|
||||
} else if desc.contains("QuickSync") {
|
||||
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0);
|
||||
} else {
|
||||
|
|
@ -672,7 +676,7 @@ impl WindowsHwEncoder {
|
|||
pub fn encode_cached_frame(&mut self, pts: i64) -> Result<Vec<crate::ring::Packet>, String> {
|
||||
let mut packets = Vec::new();
|
||||
if !self.has_frame {
|
||||
return Ok(packets);
|
||||
return Err("No cached frame available in encoder".into());
|
||||
}
|
||||
unsafe {
|
||||
if self.sw_frame.is_null() {
|
||||
|
|
|
|||
18
src/ipc.rs
18
src/ipc.rs
|
|
@ -93,6 +93,18 @@ pub fn read_active_ipc_port() -> u16 {
|
|||
42069
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn get_candidate_ipc_ports() -> Vec<u16> {
|
||||
let active_port = read_active_ipc_port();
|
||||
let mut ports = vec![active_port];
|
||||
for p in [42069, 42070, 42071, 42072] {
|
||||
if !ports.contains(&p) {
|
||||
ports.push(p);
|
||||
}
|
||||
}
|
||||
ports
|
||||
}
|
||||
|
||||
pub fn is_daemon_running() -> bool {
|
||||
query_status().is_ok()
|
||||
}
|
||||
|
|
@ -133,8 +145,7 @@ pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send
|
|||
#[cfg(windows)]
|
||||
{
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
let active_port = read_active_ipc_port();
|
||||
let candidate_ports = [active_port, 42069, 42070, 42071, 42072];
|
||||
let candidate_ports = get_candidate_ipc_ports();
|
||||
for &port in &candidate_ports {
|
||||
if let Ok(addr) = format!("127.0.0.1:{}", port).parse::<SocketAddr>() {
|
||||
if let Ok(mut stream) = TcpStream::connect_timeout(&addr, Duration::from_millis(150)) {
|
||||
|
|
@ -203,8 +214,7 @@ pub fn query_status() -> Result<DaemonStatus, Box<dyn std::error::Error + Send +
|
|||
#[cfg(windows)]
|
||||
let stream_res = {
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
let active_port = read_active_ipc_port();
|
||||
let candidate_ports = [active_port, 42069, 42070, 42071, 42072];
|
||||
let candidate_ports = get_candidate_ipc_ports();
|
||||
let mut conn = None;
|
||||
for &port in &candidate_ports {
|
||||
if let Ok(addr) = format!("127.0.0.1:{}", port).parse::<SocketAddr>() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::chunks_exact_to_as_chunks)]
|
||||
|
||||
pub mod capture;
|
||||
pub mod encoder;
|
||||
pub mod ring;
|
||||
|
|
|
|||
|
|
@ -353,18 +353,34 @@ fn run_auto_update(info: &ReleaseInfo, status: &Arc<Mutex<UpdateStatus>>) -> Res
|
|||
let current_exe = std::env::current_exe().map_err(|e| format!("Cannot locate current executable: {}", e))?;
|
||||
let exe_dir = current_exe.parent().ok_or_else(|| "Invalid executable path".to_string())?;
|
||||
|
||||
set_progress("Replacing binaries...");
|
||||
for bin_name in ["scythe-ui.exe", "scythe-daemon.exe"] {
|
||||
let src = temp_dir.join(bin_name);
|
||||
if src.exists() {
|
||||
let dest = exe_dir.join(bin_name);
|
||||
let dest_old = exe_dir.join(format!("{}.old", bin_name));
|
||||
let _ = std::fs::remove_file(&dest_old);
|
||||
let _ = std::fs::rename(&dest, &dest_old);
|
||||
let _ = std::fs::copy(&src, &dest);
|
||||
set_progress("Replacing binaries and libraries...");
|
||||
let mut files_to_update = Vec::new();
|
||||
let mut search_dirs = vec![temp_dir.clone()];
|
||||
while let Some(dir) = search_dirs.pop() {
|
||||
if let Ok(entries) = std::fs::read_dir(&dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
search_dirs.push(path);
|
||||
} else if path.is_file() {
|
||||
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||
let lower = name.to_lowercase();
|
||||
if lower.ends_with(".exe") || lower.ends_with(".dll") {
|
||||
files_to_update.push((path, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (src, name) in files_to_update {
|
||||
let dest = exe_dir.join(&name);
|
||||
let dest_old = exe_dir.join(format!("{}.old", name));
|
||||
let _ = std::fs::remove_file(&dest_old);
|
||||
let _ = std::fs::rename(&dest, &dest_old);
|
||||
let _ = std::fs::copy(&src, &dest);
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
return Ok(format!("Updated binaries to v{} in {}", info.version, exe_dir.display()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ SOCKET_PATH="${XDG_RUNTIME_DIR:-/tmp}/scythe.sock"
|
|||
rm -f "$SOCKET_PATH"
|
||||
rm -f record_*.mp4 replay_*.mp4
|
||||
|
||||
SAVE_DIR=$(python3 -c 'import json, os; p=os.path.expanduser("~/.config/scythe/config.json"); print(json.load(open(p))["output_directory"]) if os.path.exists(p) else print(os.path.expanduser("~/Videos/Scythe"))' 2>/dev/null || echo "$HOME/Videos/Scythe")
|
||||
SAVE_DIR=$(python3 -c 'import json, os; p=os.path.expanduser("~/.config/scythe/config.json"); print(os.path.expanduser(json.load(open(p))["output_directory"])) if os.path.exists(p) else print(os.path.expanduser("~/Videos/Scythe"))' 2>/dev/null || echo "$HOME/Videos/Scythe")
|
||||
mkdir -p "$SAVE_DIR"
|
||||
|
||||
echo "[2/6] Starting scythe-daemon with mock capture..."
|
||||
|
|
@ -72,7 +72,7 @@ echo " Verifying Generated Recordings with ffprobe"
|
|||
echo "============================================="
|
||||
|
||||
FOUND_ANY=0
|
||||
for f in $(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdepth 2 \( -name "record_*.mp4" -o -name "replay_*.mp4" \) 2>/dev/null); do
|
||||
while IFS= read -r -d '' f; do
|
||||
if [ -f "$f" ]; then
|
||||
FOUND_ANY=1
|
||||
echo "--> Testing file: $f ($(ls -lh "$f" | awk '{print $5}'))"
|
||||
|
|
@ -87,7 +87,7 @@ for f in $(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdept
|
|||
echo "---------------------------------------------"
|
||||
rm -f "$f"
|
||||
fi
|
||||
done
|
||||
done < <(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdepth 2 -mmin -2 \( -iname "record*.mp4" -o -iname "replay*.mp4" \) -print0 2>/dev/null)
|
||||
|
||||
if [ $FOUND_ANY -eq 0 ]; then
|
||||
echo "FAILED: No recording or replay files were produced!"
|
||||
|
|
|
|||
16
tests/manual_ipc/send_ipc.py
Normal file
16
tests/manual_ipc/send_ipc.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import socket
|
||||
import json
|
||||
import struct
|
||||
import os
|
||||
|
||||
def send(cmd):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
|
||||
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
|
||||
sock.connect(sock_path)
|
||||
payload = json.dumps(cmd).encode('utf-8')
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
|
||||
import sys
|
||||
send(sys.argv[1])
|
||||
13
tests/manual_ipc/test_ipc.py
Normal file
13
tests/manual_ipc/test_ipc.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import socket
|
||||
import json
|
||||
import struct
|
||||
import os
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
|
||||
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
|
||||
sock.connect(sock_path)
|
||||
payload = b'"SaveReplay"'
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
print("Sent SaveReplay")
|
||||
9
tests/manual_ipc/test_record.py
Normal file
9
tests/manual_ipc/test_record.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import socket, struct, os
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
|
||||
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
|
||||
sock.connect(sock_path)
|
||||
payload = b'"StartRecording"'
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
print("Sent StartRecording")
|
||||
9
tests/manual_ipc/test_reload.py
Normal file
9
tests/manual_ipc/test_reload.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import socket, struct, os
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
|
||||
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
|
||||
sock.connect(sock_path)
|
||||
payload = b'"ReloadConfig"'
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
print("Sent ReloadConfig")
|
||||
9
tests/manual_ipc/test_stop.py
Normal file
9
tests/manual_ipc/test_stop.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import socket, struct, os
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
|
||||
sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock"
|
||||
sock.connect(sock_path)
|
||||
payload = b'"StopRecording"'
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
print("Sent StopRecording")
|
||||
Loading…
Add table
Reference in a new issue