feat(ui): screenshot resolution presets, low-end hardware optimizations, and all 8 keybind tests
This commit is contained in:
parent
cc6512edb7
commit
f41b827b41
7 changed files with 103 additions and 28 deletions
|
|
@ -481,7 +481,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let audio_time_base = audio_codec_ctx_ptr.map(|p| unsafe { (*(p as *mut ffmpeg_next::ffi::AVCodecContext)).time_base });
|
||||
|
||||
let ring_capacity = if config.replay_enabled {
|
||||
(config.replay_duration_sec as usize) * (config.fps as usize + 50) + 60
|
||||
if config.record_low_power {
|
||||
(config.replay_duration_sec as usize) * (config.fps as usize + 15) + 30
|
||||
} else {
|
||||
(config.replay_duration_sec as usize) * (config.fps as usize + 50) + 60
|
||||
}
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
|
|
|||
|
|
@ -322,12 +322,10 @@ fn handle_screenshot() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
grim.args(["-t", "jpeg", "-q", "90"]);
|
||||
}
|
||||
grim.arg(&out_path);
|
||||
let res = grim.status();
|
||||
if res.is_ok() && out_path.exists() {
|
||||
show_shadowplay_toast("SCREENSHOT", &format!("Saved to {}", filename), ToastIcon::Screenshot);
|
||||
return Ok(());
|
||||
let _ = grim.status();
|
||||
if !out_path.exists() {
|
||||
let _ = std::process::Command::new("import").args(["-window", "root"]).arg(&out_path).status();
|
||||
}
|
||||
let _ = std::process::Command::new("import").args(["-window", "root"]).arg(&out_path).status();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
@ -346,6 +344,24 @@ fn handle_screenshot() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
.status();
|
||||
}
|
||||
|
||||
let scale_dims = match cfg.screenshot_resolution.as_str() {
|
||||
"720p" => Some("1280:720"),
|
||||
"1080p" => Some("1920:1080"),
|
||||
"1440p" => Some("2560:1440"),
|
||||
"2160p" => Some("3840:2160"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(scale) = scale_dims
|
||||
&& out_path.exists() {
|
||||
let tmp_path = dir.join(format!("tmp_{}", filename));
|
||||
if std::fs::rename(&out_path, &tmp_path).is_ok() {
|
||||
let _ = std::process::Command::new("ffmpeg")
|
||||
.args(["-v", "error", "-y", "-i", &tmp_path.to_string_lossy(), "-vf", &format!("scale={}:flags=lanczos", scale), &out_path.to_string_lossy()])
|
||||
.status();
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
}
|
||||
}
|
||||
|
||||
show_shadowplay_toast("SCREENSHOT", &format!("Saved to {}", filename), ToastIcon::Screenshot);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ fn default_screenshot_preset() -> String { "balanced".to_string() }
|
|||
fn default_pause_hotkey() -> String { "Ctrl+Shift+F8".to_string() }
|
||||
fn default_mute_mic_hotkey() -> String { "Ctrl+Shift+M".to_string() }
|
||||
fn default_mute_sys_hotkey() -> String { "Ctrl+Shift+N".to_string() }
|
||||
fn default_screenshot_resolution() -> String { "native".to_string() }
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ScytheConfig {
|
||||
|
|
@ -154,6 +155,8 @@ pub struct ScytheConfig {
|
|||
pub mute_mic_hotkey: String,
|
||||
#[serde(default = "default_mute_sys_hotkey")]
|
||||
pub mute_sys_hotkey: String,
|
||||
#[serde(default = "default_screenshot_resolution")]
|
||||
pub screenshot_resolution: String,
|
||||
#[serde(default)]
|
||||
pub use_monospace_font: bool,
|
||||
}
|
||||
|
|
@ -223,6 +226,7 @@ impl Default for ScytheConfig {
|
|||
pause_hotkey: default_pause_hotkey(),
|
||||
mute_mic_hotkey: default_mute_mic_hotkey(),
|
||||
mute_sys_hotkey: default_mute_sys_hotkey(),
|
||||
screenshot_resolution: default_screenshot_resolution(),
|
||||
use_monospace_font: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -514,6 +518,7 @@ mod tests {
|
|||
assert_eq!(parsed.pause_hotkey, "Ctrl+Shift+F8");
|
||||
assert_eq!(parsed.mute_mic_hotkey, "Ctrl+Shift+M");
|
||||
assert_eq!(parsed.mute_sys_hotkey, "Ctrl+Shift+N");
|
||||
assert_eq!(parsed.screenshot_resolution, "native");
|
||||
assert!(!parsed.use_monospace_font);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,10 +104,12 @@ impl VaapiEncoder {
|
|||
_ => FF_PROFILE_H264_HIGH,
|
||||
};
|
||||
(*codec_ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int;
|
||||
let _ = av_opt_set_int((*codec_ctx).priv_data, c"async_depth".as_ptr(), 4, 0);
|
||||
let async_depth = if low_power { 2 } else { 4 };
|
||||
let _ = av_opt_set_int((*codec_ctx).priv_data, c"async_depth".as_ptr(), async_depth, 0);
|
||||
|
||||
if low_power {
|
||||
let _ = av_opt_set_int((*codec_ctx).priv_data, c"low_power".as_ptr(), 1, 0);
|
||||
let _ = av_opt_set_int((*codec_ctx).priv_data, c"compression_level".as_ptr(), 1, 0);
|
||||
}
|
||||
|
||||
// Rec.709 Color Space & Color Range
|
||||
|
|
@ -592,11 +594,15 @@ impl WindowsHwEncoder {
|
|||
let rc = if is_cbr { c"cbr".as_ptr() } else { c"vbr_latency".as_ptr() };
|
||||
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(), rc, 0);
|
||||
if low_power {
|
||||
let _ = av_opt_set((*ctx).priv_data, c"quality".as_ptr(), c"speed".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 {
|
||||
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"ultrafast".as_ptr(), 0);
|
||||
let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"zerolatency".as_ptr(), 0);
|
||||
(*ctx).thread_count = 0;
|
||||
}
|
||||
|
||||
let ret = avcodec_open2(ctx, c, ptr::null_mut());
|
||||
|
|
|
|||
|
|
@ -95,5 +95,22 @@ mod tests {
|
|||
assert!(parse_hotkey("").is_none());
|
||||
assert!(parse_hotkey("invalid_key_xyz").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_eight_default_scythe_hotkeys() {
|
||||
let defaults = [
|
||||
("Alt+Z", "menu"),
|
||||
("Ctrl+Shift+R", "save_replay"),
|
||||
("Ctrl+Shift+F9", "record_toggle"),
|
||||
("Ctrl+Shift+F8", "pause_record"),
|
||||
("Ctrl+Shift+S", "screenshot"),
|
||||
("Ctrl+Shift+F10", "toggle_cursor"),
|
||||
("Ctrl+Shift+M", "mute_mic"),
|
||||
("Ctrl+Shift+N", "mute_sys"),
|
||||
];
|
||||
for (hk_str, label) in defaults {
|
||||
assert!(parse_hotkey(hk_str).is_some(), "Hotkey '{}' for '{}' must parse successfully", hk_str, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1163,6 +1163,7 @@ pub struct ScytheOverlayApp {
|
|||
pub autostart_power_only: bool,
|
||||
pub screenshot_quality: u32,
|
||||
pub screenshot_preset: String,
|
||||
pub screenshot_resolution: String,
|
||||
pub use_monospace_font: bool,
|
||||
}
|
||||
|
||||
|
|
@ -1257,6 +1258,7 @@ impl ScytheOverlayApp {
|
|||
let autostart_power_only = config.autostart_power_only;
|
||||
let screenshot_quality = config.screenshot_quality;
|
||||
let screenshot_preset = config.screenshot_preset.clone();
|
||||
let screenshot_resolution = config.screenshot_resolution.clone();
|
||||
let use_monospace_font = config.use_monospace_font;
|
||||
|
||||
let update_status = Arc::new(std::sync::Mutex::new(crate::updater::UpdateStatus::Idle));
|
||||
|
|
@ -1339,6 +1341,7 @@ impl ScytheOverlayApp {
|
|||
autostart_power_only,
|
||||
screenshot_quality,
|
||||
screenshot_preset,
|
||||
screenshot_resolution,
|
||||
use_monospace_font,
|
||||
}
|
||||
}
|
||||
|
|
@ -1391,6 +1394,7 @@ impl ScytheOverlayApp {
|
|||
self.config.autostart_power_only = self.autostart_power_only;
|
||||
self.config.screenshot_quality = self.screenshot_quality;
|
||||
self.config.screenshot_preset = self.screenshot_preset.clone();
|
||||
self.config.screenshot_resolution = self.screenshot_resolution.clone();
|
||||
self.config.use_monospace_font = self.use_monospace_font;
|
||||
let _ = self.config.save();
|
||||
crate::hyprland_binds::register_hyprland_binds(&self.config);
|
||||
|
|
@ -1437,6 +1441,7 @@ impl ScytheOverlayApp {
|
|||
self.autostart_power_only = def.autostart_power_only;
|
||||
self.screenshot_quality = def.screenshot_quality;
|
||||
self.screenshot_preset = def.screenshot_preset;
|
||||
self.screenshot_resolution = def.screenshot_resolution;
|
||||
self.use_monospace_font = def.use_monospace_font;
|
||||
self.show_hud_notification("DEFAULTS", "Settings restored to defaults", crate::overlay::ToastIcon::Info);
|
||||
}
|
||||
|
|
@ -1481,6 +1486,24 @@ impl ScytheOverlayApp {
|
|||
.status();
|
||||
}
|
||||
|
||||
let scale_dims = match self.screenshot_resolution.as_str() {
|
||||
"720p" => Some("1280:720"),
|
||||
"1080p" => Some("1920:1080"),
|
||||
"1440p" => Some("2560:1440"),
|
||||
"2160p" => Some("3840:2160"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(scale) = scale_dims
|
||||
&& out_path.exists() {
|
||||
let tmp_path = dir.join(format!("tmp_{}", fname));
|
||||
if std::fs::rename(&out_path, &tmp_path).is_ok() {
|
||||
let _ = std::process::Command::new("ffmpeg")
|
||||
.args(["-v", "error", "-y", "-i", &tmp_path.to_string_lossy(), "-vf", &format!("scale={}:flags=lanczos", scale), &out_path.to_string_lossy()])
|
||||
.status();
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
}
|
||||
}
|
||||
|
||||
self.show_hud_notification("SCREENSHOT", &format!("Saved to {}", fname), crate::overlay::ToastIcon::Screenshot);
|
||||
}
|
||||
|
||||
|
|
@ -2308,14 +2331,6 @@ impl ScytheOverlayApp {
|
|||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
let mut sfg = self.save_folder_by_game;
|
||||
if toggle_switch(ui, &mut sfg, accent).changed() {
|
||||
self.save_folder_by_game = sfg;
|
||||
}
|
||||
ui.label(egui::RichText::new("Save video in a folder based on the games name").size(11.5).color(Color32::WHITE));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
let mut nbg = self.name_by_game;
|
||||
if toggle_switch(ui, &mut nbg, accent).changed() {
|
||||
|
|
@ -2734,20 +2749,19 @@ impl ScytheOverlayApp {
|
|||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new("Resolution preset:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Compression quality:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
|
||||
let mut q = self.screenshot_quality;
|
||||
if ui.add(egui::Slider::new(&mut q, 10..=100).suffix("%")).changed() {
|
||||
self.screenshot_quality = q;
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Compression preset:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
|
||||
for (p_id, label) in [("fast", "Fast"), ("balanced", "Balanced"), ("max", "Maximum Compression")] {
|
||||
if squared_button(ui, label, self.screenshot_preset == p_id, accent) {
|
||||
self.screenshot_preset = p_id.to_string();
|
||||
let presets = [
|
||||
("native", "Native (Source)"),
|
||||
("1080p", "1080p (FHD)"),
|
||||
("1440p", "1440p (QHD)"),
|
||||
("2160p", "2160p (4K UHD)"),
|
||||
("720p", "720p (HD)"),
|
||||
];
|
||||
for (res_id, label) in presets {
|
||||
if squared_button(ui, label, self.screenshot_resolution == res_id, accent) {
|
||||
self.screenshot_resolution = res_id.to_string();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -4432,6 +4446,7 @@ mod tests {
|
|||
app.config.audio_codec = "opus".to_string();
|
||||
app.record_low_power = true;
|
||||
app.show_replay_notifications = false;
|
||||
app.screenshot_resolution = "1080p".to_string();
|
||||
|
||||
app.save_and_apply_settings();
|
||||
|
||||
|
|
@ -4449,6 +4464,7 @@ mod tests {
|
|||
assert_eq!(app.config.audio_codec, "opus");
|
||||
assert!(app.config.record_low_power);
|
||||
assert!(!app.config.show_replay_notifications);
|
||||
assert_eq!(app.config.screenshot_resolution, "1080p");
|
||||
|
||||
app.reset_settings_to_defaults();
|
||||
assert_eq!(app.container_format, "mp4");
|
||||
|
|
@ -4463,5 +4479,6 @@ mod tests {
|
|||
assert_eq!(app.config.audio_codec, "opus");
|
||||
assert!(!app.record_low_power);
|
||||
assert!(app.show_replay_notifications);
|
||||
assert_eq!(app.screenshot_resolution, "native");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ rm -f record_*.mp4 replay_*.mp4
|
|||
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"
|
||||
|
||||
WAS_SERVICE_ACTIVE=0
|
||||
if systemctl --user is-active --quiet scythe-daemon.service 2>/dev/null; then
|
||||
WAS_SERVICE_ACTIVE=1
|
||||
systemctl --user stop scythe-daemon.service 2>/dev/null || true
|
||||
sleep 0.5
|
||||
fi
|
||||
|
||||
echo "[2/6] Starting scythe-daemon with mock capture..."
|
||||
target/release/scythe-daemon --mock > daemon_test.log 2>&1 &
|
||||
DAEMON_PID=$!
|
||||
|
|
@ -25,6 +32,9 @@ cleanup() {
|
|||
target/release/scythe-ui --quit 2>/dev/null || kill -9 $DAEMON_PID 2>/dev/null || true
|
||||
fi
|
||||
rm -f daemon_test.log
|
||||
if [ "$WAS_SERVICE_ACTIVE" -eq 1 ]; then
|
||||
systemctl --user start scythe-daemon.service 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue