Compare commits

..

8 Commits
1.6.2 ... 1.6.4

Author SHA1 Message Date
dec05eba
d0f8b7061f 1.6.4 2025-05-18 01:32:18 +02:00
dec05eba
3a4f03ce27 Use mipmap for almost all icons 2025-05-18 01:30:56 +02:00
dec05eba
e8dc3859fe Improve quality of screenshot and settings icons, especially for smaller resolutions 2025-05-18 01:23:42 +02:00
dec05eba
5fe5830056 Better monitor tracking for capture/notification on wayland 2025-05-17 23:59:59 +02:00
dec05eba
9ac14c963e Properly honor notification settings (when not saving video in game folder). Add pause/unpause notification option 2025-05-16 18:09:39 +02:00
dec05eba
cae1c47643 Update README and TODO 2025-05-16 17:57:44 +02:00
dec05eba
de1ed58f8d 1.6.3 2025-05-15 12:37:12 +02:00
dec05eba
ff564fcb52 Fix replay duration range 2025-05-15 12:36:30 +02:00
14 changed files with 133 additions and 50 deletions

View File

@@ -61,7 +61,9 @@ I'm looking for somebody that can create sound effects for the notifications.
# Known issues
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
* Opening the UI when a game is fullscreened can mess up the game window a bit on Hyprland. I believe this is an issue with Hyprland.
* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland.
* The background of the UI is black when opening the UI while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
* Unable to close the region selection with escape key while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
# FAQ
## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service

14
TODO
View File

@@ -147,8 +147,6 @@ Add systray for recording status.
Add a desktop icon when gsr-ui has a window mode option (which should be the default launch option).
Use /dev/input/eventN (or /dev/hidrawN) instead of /dev/input/jsN for joystick input.
Verify if cursor tracker monitor name is always correct. It uses the wayland monitor name for recording, but gpu screen recorder uses a custom name created from the drm connector name.
Notification with the focused monitor (with CursorTrackerWayland) assumes that the x11 monitor name is the same as the drm monitor name. Same for find_monitor_by_name.
@@ -184,4 +182,14 @@ When gpu screen recorder ui can run as a regular window (and supports tray icon
Add a bug report page that automatically includes system info (make this clear to the user).
Do this by sending the report to a custom server that stores that data.
The server should limit reports per IP to prevent spam.
The server should limit reports per IP to prevent spam.
Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers.
Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland.
Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing.
Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
Support freetype for text rendering. Maybe load freetype as runtime (with dlopen) and use that when available and fallback to stb_freetype if not available.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

View File

@@ -100,6 +100,7 @@ namespace gsr {
bool save_video_in_game_folder = false;
bool show_recording_started_notifications = true;
bool show_video_saved_notifications = true;
bool show_video_paused_notifications = true;
std::string save_directory;
std::string container = "mp4";
ConfigHotkey start_stop_hotkey;

View File

@@ -28,6 +28,7 @@ namespace gsr {
mgl::Texture combobox_arrow_texture;
mgl::Texture settings_texture;
mgl::Texture settings_small_texture;
mgl::Texture settings_extra_small_texture;
mgl::Texture folder_texture;
mgl::Texture up_arrow_texture;
mgl::Texture replay_button_texture;

View File

@@ -186,6 +186,7 @@ namespace gsr {
CheckBox *save_recording_in_game_folder_ptr = nullptr;
CheckBox *show_recording_started_notification_checkbox_ptr = nullptr;
CheckBox *show_video_saved_notification_checkbox_ptr = nullptr;
CheckBox *show_video_paused_notification_checkbox_ptr = nullptr;
CheckBox *show_streaming_started_notification_checkbox_ptr = nullptr;
CheckBox *show_streaming_stopped_notification_checkbox_ptr = nullptr;
Button *save_directory_button_ptr = nullptr;

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.6.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.6.4', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.6.2"
version = "1.6.4"
platforms = ["posix"]
[lang.cpp]

View File

@@ -229,6 +229,7 @@ namespace gsr {
{"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder},
{"record.show_recording_started_notifications", &config.record_config.show_recording_started_notifications},
{"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications},
{"record.show_video_paused_notifications", &config.record_config.show_video_paused_notifications},
{"record.save_directory", &config.record_config.save_directory},
{"record.container", &config.record_config.container},
{"record.start_stop_hotkey", &config.record_config.start_stop_hotkey},

View File

@@ -1090,7 +1090,7 @@ namespace gsr {
button->set_item_icon("save", &get_theme().save_texture);
button->set_item_icon("save_1_min", &get_theme().save_texture);
button->set_item_icon("save_10_min", &get_theme().save_texture);
button->set_item_icon("settings", &get_theme().settings_small_texture);
button->set_item_icon("settings", &get_theme().settings_extra_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack);
@@ -1124,7 +1124,7 @@ namespace gsr {
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("pause", &get_theme().pause_texture);
button->set_item_icon("settings", &get_theme().settings_small_texture);
button->set_item_icon("settings", &get_theme().settings_extra_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack);
@@ -1149,7 +1149,7 @@ namespace gsr {
button->add_item("Start", "start", config.streaming_config.start_stop_hotkey.to_string(false, false));
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("settings", &get_theme().settings_small_texture);
button->set_item_icon("settings", &get_theme().settings_extra_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack);
@@ -1382,10 +1382,12 @@ namespace gsr {
if(paused) {
update_ui_recording_unpaused();
show_notification("Recording has been unpaused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
if(config.record_config.show_video_paused_notifications)
show_notification("Recording has been unpaused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
} else {
update_ui_recording_paused();
show_notification("Recording has been paused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
if(config.record_config.show_video_paused_notifications)
show_notification("Recording has been paused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
}
kill(gpu_screen_recorder_process, SIGUSR2);
@@ -1462,6 +1464,49 @@ namespace gsr {
return strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target);
}
static std::string get_valid_monitor_x11(const std::string &target_monitor_name, const std::vector<Monitor> &monitors) {
std::string target_monitor_name_clean = target_monitor_name;
if(starts_with(target_monitor_name_clean, "HDMI-A"))
target_monitor_name_clean.replace(0, 6, "HDMI");
for(const Monitor &monitor : monitors) {
std::string monitor_name_clean = monitor.name;
if(starts_with(monitor_name_clean, "HDMI-A"))
monitor_name_clean.replace(0, 6, "HDMI");
if(target_monitor_name_clean == monitor_name_clean)
return monitor.name;
}
return "";
}
static std::string get_focused_monitor_by_cursor(CursorTracker *cursor_tracker, const GsrInfo &gsr_info, const std::vector<Monitor> &x11_monitors) {
std::optional<CursorInfo> cursor_info;
if(cursor_tracker) {
cursor_tracker->update();
cursor_info = cursor_tracker->get_latest_cursor_info();
}
std::string focused_monitor_name;
if(cursor_info) {
focused_monitor_name = std::move(cursor_info->monitor_name);
} else {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
Window x11_cursor_window = None;
mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window);
const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display);
const Monitor *focused_monitor = find_monitor_at_position(x11_monitors, monitor_position_query_value);
if(focused_monitor)
focused_monitor_name = focused_monitor->name;
}
return focused_monitor_name;
}
void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) {
char timeout_seconds_str[32];
snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds);
@@ -1480,20 +1525,24 @@ namespace gsr {
notification_args[arg_index++] = notification_type_str;
}
if(capture_target && is_capture_target_monitor(capture_target)) {
notification_args[arg_index++] = "--monitor";
notification_args[arg_index++] = capture_target;
} else {
std::optional<CursorInfo> cursor_info;
if(cursor_tracker) {
cursor_tracker->update();
cursor_info = cursor_tracker->get_latest_cursor_info();
}
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
if(cursor_info) {
notification_args[arg_index++] = "--monitor";
notification_args[arg_index++] = cursor_info->monitor_name.c_str();
}
std::string monitor_name;
const auto monitors = get_monitors(display);
if(capture_target && is_capture_target_monitor(capture_target))
monitor_name = capture_target;
else
monitor_name = get_focused_monitor_by_cursor(cursor_tracker.get(), gsr_info, monitors);
monitor_name = get_valid_monitor_x11(monitor_name, monitors);
if(!monitor_name.empty()) {
notification_args[arg_index++] = "--monitor";
notification_args[arg_index++] = monitor_name.c_str();
} else if(!monitors.empty()) {
notification_args[arg_index++] = "--monitor";
notification_args[arg_index++] = monitors.front().name.c_str();
}
notification_args[arg_index++] = nullptr;
@@ -1684,7 +1733,7 @@ namespace gsr {
replay_save_show_notification = false;
if(config.replay_config.save_video_in_game_folder) {
save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY);
} else {
} else if(config.replay_config.show_replay_saved_notifications) {
char duration[32];
if(replay_save_duration_min > 0)
snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min);
@@ -1814,7 +1863,7 @@ namespace gsr {
if(exit_code == 0) {
if(config.screenshot_config.save_screenshot_in_game_folder) {
save_video_in_current_game_directory(screenshot_filepath.c_str(), NotificationType::SCREENSHOT);
} else {
} else if(config.screenshot_config.show_screenshot_saved_notifications) {
char msg[512];
if(is_capture_target_monitor(screenshot_capture_target.c_str()))
snprintf(msg, sizeof(msg), "Saved a screenshot of this monitor");
@@ -1916,7 +1965,7 @@ namespace gsr {
if(exit_code == 0) {
if(config.record_config.save_video_in_game_folder) {
save_video_in_current_game_directory(video_filepath.c_str(), NotificationType::RECORD);
} else {
} else if(config.record_config.show_video_saved_notifications) {
char msg[512];
if(is_capture_target_monitor(recording_capture_target.c_str()))
snprintf(msg, sizeof(msg), "Saved a recording of this monitor");
@@ -2132,6 +2181,23 @@ namespace gsr {
}
}
static std::string get_valid_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
std::string capture_target_clean = capture_target;
if(starts_with(capture_target_clean, "HDMI-A"))
capture_target_clean.replace(0, 6, "HDMI");
for(const GsrMonitor &monitor : capture_options.monitors) {
std::string monitor_name_clean = monitor.name;
if(starts_with(monitor_name_clean, "HDMI-A"))
monitor_name_clean.replace(0, 6, "HDMI");
if(capture_target_clean == monitor_name_clean)
return monitor.name;
}
return "";
}
std::string Overlay::get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
if(capture_target == "focused_monitor") {
std::optional<CursorInfo> cursor_info;
@@ -2146,17 +2212,10 @@ namespace gsr {
} else {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
Window x11_cursor_window = None;
mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window);
const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display);
auto monitors = get_monitors(display);
const Monitor *focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value);
if(focused_monitor)
focused_monitor_name = focused_monitor->name;
focused_monitor_name = get_focused_monitor_by_cursor(cursor_tracker.get(), gsr_info, get_monitors(display));
}
focused_monitor_name = get_valid_capture_target(focused_monitor_name, capture_options);
if(!focused_monitor_name.empty())
return focused_monitor_name;
else if(!capture_options.monitors.empty())

View File

@@ -63,31 +63,34 @@ namespace gsr {
if(!theme->title_font_file.load((resources_path + "fonts/NotoSans-Bold.ttf").c_str(), mgl::MemoryMappedFile::LoadOptions{true, false}))
goto error;
if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str()))
if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->settings_texture.load_from_file((resources_path + "images/settings.png").c_str()))
goto error;
if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str()))
if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str()))
if(!theme->settings_extra_small_texture.load_from_file((resources_path + "images/settings_extra_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str()))
if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str()))
if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str()))
if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str()))
if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->logo_texture.load_from_file((resources_path + "images/gpu_screen_recorder_logo.png").c_str()))
@@ -99,19 +102,19 @@ namespace gsr {
if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str()))
if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str()))
if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str()))
if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str()))
if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str()))
if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))

View File

@@ -735,7 +735,7 @@ namespace gsr {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto replay_time_entry = std::make_unique<Entry>(&get_theme().body_font, "60", get_theme().body_font.get_character_size() * 3);
replay_time_entry->validate_handler = create_entry_validator_integer_in_range(2, 86400);
replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 86400);
replay_time_entry_ptr = replay_time_entry.get();
list->add_widget(std::move(replay_time_entry));
@@ -940,6 +940,11 @@ namespace gsr {
show_video_saved_notification_checkbox_ptr = show_video_saved_notification_checkbox.get();
checkboxes_list->add_widget(std::move(show_video_saved_notification_checkbox));
auto show_video_paused_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show video paused/unpaused notification");
show_video_paused_notification_checkbox->set_checked(true);
show_video_paused_notification_checkbox_ptr = show_video_paused_notification_checkbox.get();
checkboxes_list->add_widget(std::move(show_video_paused_notification_checkbox));
auto notifications_subsection = std::make_unique<Subsection>("Notifications", std::move(checkboxes_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
Subsection *notifications_subsection_ptr = notifications_subsection.get();
settings_list_ptr->add_widget(std::move(notifications_subsection));
@@ -1239,6 +1244,7 @@ namespace gsr {
save_recording_in_game_folder_ptr->set_checked(config.record_config.save_video_in_game_folder);
show_recording_started_notification_checkbox_ptr->set_checked(config.record_config.show_recording_started_notifications);
show_video_saved_notification_checkbox_ptr->set_checked(config.record_config.show_video_saved_notifications);
show_video_paused_notification_checkbox_ptr->set_checked(config.record_config.show_video_paused_notifications);
save_directory_button_ptr->set_text(config.record_config.save_directory);
container_box_ptr->set_selected_item(config.record_config.container);
}
@@ -1379,6 +1385,7 @@ namespace gsr {
config.record_config.save_video_in_game_folder = save_recording_in_game_folder_ptr->is_checked();
config.record_config.show_recording_started_notifications = show_recording_started_notification_checkbox_ptr->is_checked();
config.record_config.show_video_saved_notifications = show_video_saved_notification_checkbox_ptr->is_checked();
config.record_config.show_video_paused_notifications = show_video_paused_notification_checkbox_ptr->is_checked();
config.record_config.save_directory = save_directory_button_ptr->get_text();
config.record_config.container = container_box_ptr->get_selected_id();
}

View File

@@ -220,7 +220,7 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
return;
/* TODO: What if some key is being pressed down while this is done? will it remain pressed down? */
if(!extra_data->is_non_keyboard_device && (event.type == EV_REL || (event.type == EV_KEY && !keyboard_key))) {
if(!extra_data->is_non_keyboard_device && (event.type == EV_REL || event.type == EV_ABS || (event.type == EV_KEY && !keyboard_key))) {
fprintf(stderr, "Info: device /dev/input/event%d is likely a non-keyboard device as it received a non-keyboard event. This device will be ignored\n", extra_data->dev_input_id);
extra_data->is_non_keyboard_device = true;
if(extra_data->grabbed) {