Compare commits

..

11 Commits
1.8.2 ... 1.9.0

Author SHA1 Message Date
dec05eba
fd5026489c Revert minor 2025-12-25 08:28:17 +01:00
dec05eba
8032cb2cf0 1.9.0 2025-12-25 08:19:42 +01:00
dec05eba
13562d2aa1 Add webcam support 2025-12-25 08:19:07 +01:00
dec05eba
1971d4a288 Die properly when killed with SIGINT 2025-12-23 22:54:27 +01:00
dec05eba
c039b79174 Wayland: only prevent game minimizing if the input focused window is x11. Change screenshot program to a command instead, allows spectacle -E to work 2025-12-22 15:55:13 +01:00
dec05eba
245dcf5730 1.8.3 2025-12-07 18:15:47 +01:00
dec05eba
e68a342b81 Default empty text not kolourpaint 2025-12-04 23:23:57 +01:00
Giovane Perlin
11aa237821 feat: adds an option to run a script after a screenshot 2025-12-04 23:18:01 +01:00
dec05eba
d7be9b38b1 Screenshot: fix image not saved to clipboard if notifications are disabled 2025-12-04 22:43:54 +01:00
dec05eba
4717c64b03 Update flatpak version reference 2025-12-04 20:16:53 +01:00
dec05eba
2c2633ec58 Add exec_program_on_host_daemonized 2025-12-01 19:57:56 +01:00
28 changed files with 689 additions and 120 deletions

10
TODO
View File

@@ -246,4 +246,12 @@ Remove all mgl::Clock usage in Overlay. We only need to get the time once per up
Handle stopping replay/stream when recording is running (show notification that the video is saved and move the video to folder with game name).
Support translations.
Support translations.
Sometimes when opening gpu screen recorder ui gsr-global-hotkeys incorrectly detects that keyboard input is locked.
When running replay for a long time and then stopping it it takes a while. Improve this.
When adding webcamera make replay auto start wait for camera to be available (when /dev/video device exists and can be initialized), just like audio device.
Make it possible to resize webcam box from top left, top right and bottom left as well.

View File

@@ -61,6 +61,14 @@ namespace gsr {
bool record_cursor = true;
bool restore_portal_session = true;
std::string webcam_source = "";
bool webcam_flip_horizontally = false;
std::string webcam_video_format = "auto";
int32_t webcam_x = 0; // A value between 0 and 100 (percentage)
int32_t webcam_y = 0; // A value between 0 and 100 (percentage)
int32_t webcam_width = 30; // A value between 0 and 100 (percentage), 0 = Don't scale it
int32_t webcam_height = 30; // A value between 0 and 100 (percentage), 0 = Don't scale it
bool show_notifications = true;
bool use_led_indicator = false;
};
@@ -146,6 +154,8 @@ namespace gsr {
ConfigHotkey take_screenshot_hotkey;
ConfigHotkey take_screenshot_region_hotkey;
ConfigHotkey take_screenshot_window_hotkey; // Or desktop portal, on wayland
std::string custom_script;
};
struct Config {
@@ -164,4 +174,4 @@ namespace gsr {
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options);
void save_config(Config &config);
}
}

View File

@@ -25,11 +25,22 @@ namespace gsr {
bool png = false;
};
struct SupportedCameraPixelFormats {
bool yuyv = false;
bool mjpeg = false;
};
struct GsrMonitor {
std::string name;
mgl::vec2i size;
};
struct GsrCamera {
std::string path;
mgl::vec2i size;
SupportedCameraPixelFormats supported_pixel_formats;
};
struct GsrVersion {
uint8_t major = 0;
uint8_t minor = 0;
@@ -51,6 +62,7 @@ namespace gsr {
bool focused = false;
bool portal = false;
std::vector<GsrMonitor> monitors;
std::vector<GsrCamera> cameras;
};
enum class DisplayServer {
@@ -103,4 +115,5 @@ namespace gsr {
std::vector<AudioDevice> get_audio_devices();
std::vector<std::string> get_application_audio();
SupportedCaptureOptions get_supported_capture_options(const GsrInfo &gsr_info);
std::vector<GsrCamera> get_v4l2_devices();
}

View File

@@ -117,7 +117,7 @@ namespace gsr {
double get_time_passed_in_replay_buffer_seconds();
void update_notification_process_status();
void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type);
void save_video_in_current_game_directory(std::string &video_filepath, NotificationType notification_type);
void on_replay_saved(const char *replay_saved_filepath);
void process_gsr_output();
void on_gsr_process_error(int exit_code, NotificationType notification_type);
@@ -129,7 +129,7 @@ namespace gsr {
void update_power_supply_status();
void update_system_startup_status();
void on_stop_recording(int exit_code, const std::string &video_filepath);
void on_stop_recording(int exit_code, std::string &video_filepath);
void update_ui_recording_paused();
void update_ui_recording_unpaused();

View File

@@ -13,13 +13,17 @@ namespace gsr {
// Arguments ending with NULL
bool exec_program_daemonized(const char **args, bool debug = true);
// Arguments ending with NULL.
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
// host machine with flatpak-spawn --host.
bool exec_program_on_host_daemonized(const char **args, bool debug = true);
// Arguments ending with NULL. |read_fd| can be NULL
pid_t exec_program(const char **args, int *read_fd, bool debug = true);
// Arguments ending with NULL. Returns the exit status of the program or -1 on error
int exec_program_get_stdout(const char **args, std::string &result, bool debug = true);
// Arguments ending with NULL. Returns the exit status of the program or -1 on error.
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
// host machine with flatpak-spawn --host
// host machine with flatpak-spawn --host.
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug = true);
pid_t pidof(const char *process_name, pid_t ignore_pid);
}

View File

@@ -24,6 +24,7 @@ namespace gsr {
mgl::Font body_font;
mgl::Font title_font;
mgl::Font top_bar_font;
mgl::Font camera_setup_font;
mgl::Texture combobox_arrow_texture;
mgl::Texture settings_texture;

View File

@@ -19,8 +19,8 @@ namespace gsr {
};
std::optional<std::string> get_window_title(Display *dpy, Window window);
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
Window get_focused_window(Display *dpy, WindowCaptureType cap_type, bool fallback_cursor_focused = true);
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type, bool fallback_cursor_focused = true);
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window);
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window);
void set_window_size_not_resizable(Display *dpy, Window window, int width, int height);

View File

@@ -19,6 +19,8 @@ namespace gsr {
void draw(mgl::Window &window, mgl::vec2f offset) override;
void add_item(const std::string &text, const std::string &id);
void clear_items();
// The item can only be selected if it's enabled
void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true);
void set_item_enabled(const std::string &id, bool enabled);

View File

@@ -47,6 +47,9 @@ namespace gsr {
std::unique_ptr<Widget> create_led_indicator();
std::unique_ptr<Widget> create_general_section();
std::unique_ptr<Widget> create_screenshot_indicator_section();
std::unique_ptr<Widget> create_custom_script_screenshot_section();
std::unique_ptr<List> create_custom_script_screenshot_entry();
std::unique_ptr<List> create_custom_script_screenshot();
std::unique_ptr<Widget> create_settings();
void add_widgets();
@@ -75,7 +78,8 @@ namespace gsr {
CheckBox *save_screenshot_to_clipboard_checkbox_ptr = nullptr;
CheckBox *show_notification_checkbox_ptr = nullptr;
CheckBox *led_indicator_checkbox_ptr = nullptr;
Entry *create_custom_script_screenshot_entry_ptr = nullptr;
PageStack *page_stack = nullptr;
};
}
}

View File

@@ -25,6 +25,14 @@ namespace gsr {
INPUT
};
enum class WebcamBoxResizeCorner {
NONE,
//TOP_LEFT,
//TOP_RIGHT,
//BOTTOM_LEFT,
BOTTOM_RIGHT
};
class SettingsPage : public StaticPage {
public:
enum class Type {
@@ -58,6 +66,9 @@ namespace gsr {
std::unique_ptr<List> create_restore_portal_session_section();
std::unique_ptr<Widget> create_change_video_resolution_section();
std::unique_ptr<Widget> create_capture_target_section();
std::unique_ptr<List> create_webcam_sources();
std::unique_ptr<List> create_webcam_video_format();
std::unique_ptr<Widget> create_webcam_section();
std::unique_ptr<ComboBox> create_audio_device_selection_combobox(AudioDeviceType device_type);
std::unique_ptr<Button> create_remove_audio_device_button(List *audio_input_list_ptr, List *audio_device_list_ptr);
std::unique_ptr<List> create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr);
@@ -140,6 +151,8 @@ namespace gsr {
void save_stream();
void view_changed(bool advanced_view);
RecordOptions& get_current_record_options();
private:
Type type;
Config &config;
@@ -197,7 +210,29 @@ namespace gsr {
List *audio_track_section_list_ptr = nullptr;
CheckBox *led_indicator_checkbox_ptr = nullptr;
CheckBox *show_notification_checkbox_ptr = nullptr;
ComboBox *webcam_sources_box_ptr = nullptr;
ComboBox *webcam_video_format_box_ptr = nullptr;
List *webcam_body_list_ptr = nullptr;
CheckBox *flip_camera_horizontally_checkbox_ptr = nullptr;
PageStack *page_stack = nullptr;
mgl::vec2f webcam_box_pos;
mgl::vec2f webcam_box_size;
mgl::vec2f webcam_box_drawn_pos;
mgl::vec2f webcam_box_drawn_size;
mgl::vec2f webcam_box_grab_offset;
mgl::vec2f camera_screen_size;
mgl::vec2f screen_inner_size;
bool moving_webcam_box = false;
WebcamBoxResizeCorner webcam_resize_corner = WebcamBoxResizeCorner::NONE;
mgl::vec2f webcam_resize_start_pos;
mgl::vec2f webcam_box_pos_resize_start;
mgl::vec2f webcam_box_size_resize_start;
std::optional<GsrCamera> selected_camera;
};
}

View File

@@ -9,6 +9,10 @@ namespace mgl {
}
namespace gsr {
mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b);
mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b);
mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max);
// Inner border
void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size);
double get_frame_delta_seconds();

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.8.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.9.0', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
@@ -66,7 +66,7 @@ datadir = get_option('datadir')
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.9.0"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.11.1"', language: ['c', 'cpp'])
executable(
meson.project_name(),

View File

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

View File

@@ -199,6 +199,13 @@ namespace gsr {
{"streaming.record_options.overclock", &config.streaming_config.record_options.overclock},
{"streaming.record_options.record_cursor", &config.streaming_config.record_options.record_cursor},
{"streaming.record_options.restore_portal_session", &config.streaming_config.record_options.restore_portal_session},
{"streaming.record_options.webcam_source", &config.streaming_config.record_options.webcam_source},
{"streaming.record_options.webcam_flip_horizontally", &config.streaming_config.record_options.webcam_flip_horizontally},
{"streaming.record_options.webcam_video_format", &config.streaming_config.record_options.webcam_video_format},
{"streaming.record_options.webcam_x", &config.streaming_config.record_options.webcam_x},
{"streaming.record_options.webcam_y", &config.streaming_config.record_options.webcam_y},
{"streaming.record_options.webcam_width", &config.streaming_config.record_options.webcam_width},
{"streaming.record_options.webcam_height", &config.streaming_config.record_options.webcam_height},
{"streaming.record_options.show_notifications", &config.streaming_config.record_options.show_notifications},
{"streaming.record_options.use_led_indicator", &config.streaming_config.record_options.use_led_indicator},
{"streaming.service", &config.streaming_config.streaming_service},
@@ -231,6 +238,13 @@ namespace gsr {
{"record.record_options.overclock", &config.record_config.record_options.overclock},
{"record.record_options.record_cursor", &config.record_config.record_options.record_cursor},
{"record.record_options.restore_portal_session", &config.record_config.record_options.restore_portal_session},
{"record.record_options.webcam_source", &config.record_config.record_options.webcam_source},
{"record.record_options.webcam_flip_horizontally", &config.record_config.record_options.webcam_flip_horizontally},
{"record.record_options.webcam_video_format", &config.record_config.record_options.webcam_video_format},
{"record.record_options.webcam_x", &config.record_config.record_options.webcam_x},
{"record.record_options.webcam_y", &config.record_config.record_options.webcam_y},
{"record.record_options.webcam_width", &config.record_config.record_options.webcam_width},
{"record.record_options.webcam_height", &config.record_config.record_options.webcam_height},
{"record.record_options.show_notifications", &config.record_config.record_options.show_notifications},
{"record.record_options.use_led_indicator", &config.record_config.record_options.use_led_indicator},
{"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder},
@@ -260,6 +274,13 @@ namespace gsr {
{"replay.record_options.overclock", &config.replay_config.record_options.overclock},
{"replay.record_options.record_cursor", &config.replay_config.record_options.record_cursor},
{"replay.record_options.restore_portal_session", &config.replay_config.record_options.restore_portal_session},
{"replay.record_options.webcam_source", &config.replay_config.record_options.webcam_source},
{"replay.record_options.webcam_flip_horizontally", &config.replay_config.record_options.webcam_flip_horizontally},
{"replay.record_options.webcam_video_format", &config.replay_config.record_options.webcam_video_format},
{"replay.record_options.webcam_x", &config.replay_config.record_options.webcam_x},
{"replay.record_options.webcam_y", &config.replay_config.record_options.webcam_y},
{"replay.record_options.webcam_width", &config.replay_config.record_options.webcam_width},
{"replay.record_options.webcam_height", &config.replay_config.record_options.webcam_height},
{"replay.record_options.show_notifications", &config.replay_config.record_options.show_notifications},
{"replay.record_options.use_led_indicator", &config.replay_config.record_options.use_led_indicator},
{"replay.turn_on_replay_automatically_mode", &config.replay_config.turn_on_replay_automatically_mode},
@@ -289,7 +310,8 @@ namespace gsr {
{"screenshot.save_directory", &config.screenshot_config.save_directory},
{"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey},
{"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_hotkey},
{"screenshot.take_screenshot_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey}
{"screenshot.take_screenshot_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey},
{"screenshot.custom_script", &config.screenshot_config.custom_script},
};
}
@@ -487,4 +509,4 @@ namespace gsr {
fclose(file);
}
}
}

View File

@@ -288,6 +288,56 @@ namespace gsr {
return application_audio;
}
struct KeyValue3 {
std::string_view value1;
std::string_view value2;
std::string_view value3;
};
static std::optional<KeyValue3> parse_3(std::string_view line) {
const size_t space_index1 = line.find('|');
if(space_index1 == std::string_view::npos)
return std::nullopt;
const size_t space_index2 = line.find('|', space_index1 + 1);
if(space_index2 == std::string_view::npos)
return std::nullopt;
return KeyValue3{
line.substr(0, space_index1),
line.substr(space_index1 + 1, space_index2 - (space_index1 + 1)),
line.substr(space_index2 + 1),
};
}
static SupportedCameraPixelFormats parse_supported_camera_pixel_formats(std::string_view line) {
SupportedCameraPixelFormats result;
string_split_char(line, ',', [&](std::string_view column) {
if(column == "yuyv")
result.yuyv = true;
else if(column == "mjpeg")
result.mjpeg = true;
return true;
});
return result;
}
static std::optional<GsrCamera> capture_option_line_to_camera(std::string_view line) {
std::optional<GsrCamera> camera;
const std::optional<KeyValue3> key_value3 = parse_3(line);
if(!key_value3)
return camera;
mgl::vec2i size;
char value_buffer[256];
snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value3->value2.size(), key_value3->value2.data());
if(sscanf(value_buffer, "%dx%d", &size.x, &size.y) != 2)
return camera;
camera = GsrCamera{std::string(key_value3->value1), size, parse_supported_camera_pixel_formats(key_value3->value3)};
return camera;
}
static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) {
std::optional<GsrMonitor> monitor;
const std::optional<KeyValue> key_value = parse_key_value(line);
@@ -305,15 +355,19 @@ namespace gsr {
}
static void parse_capture_options_line(SupportedCaptureOptions &capture_options, std::string_view line) {
if(line == "window")
if(line == "window") {
capture_options.window = true;
else if(line == "region")
} else if(line == "region") {
capture_options.region = true;
else if(line == "focused")
} else if(line == "focused") {
capture_options.focused = true;
else if(line == "portal")
} else if(line == "portal") {
capture_options.portal = true;
else {
} else if(!line.empty() && line[0] == '/') {
std::optional<GsrCamera> camera = capture_option_line_to_camera(line);
if(camera)
capture_options.cameras.push_back(std::move(camera.value()));
} else {
std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line);
if(monitor)
capture_options.monitors.push_back(std::move(monitor.value()));
@@ -348,4 +402,24 @@ namespace gsr {
return capture_options;
}
std::vector<GsrCamera> get_v4l2_devices() {
std::vector<GsrCamera> cameras;
std::string stdout_str;
const char *args[] = { "gpu-screen-recorder", "--list-v4l2-devices", nullptr };
if(exec_program_get_stdout(args, stdout_str) != 0) {
fprintf(stderr, "error: 'gpu-screen-recorder --list-v4l2-devices' failed\n");
return cameras;
}
string_split_char(stdout_str, '\n', [&](std::string_view line) {
std::optional<GsrCamera> camera = capture_option_line_to_camera(line);
if(camera)
cameras.push_back(std::move(camera.value()));
return true;
});
return cameras;
}
}

View File

@@ -17,6 +17,7 @@
#include "../include/CursorTracker/CursorTrackerX11.hpp"
#include "../include/CursorTracker/CursorTrackerWayland.hpp"
#include <iomanip>
#include <string.h>
#include <assert.h>
#include <sys/wait.h>
@@ -1022,7 +1023,10 @@ namespace gsr {
// Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused.
// If the focused window is a wayland application then don't use override redirect and instead create
// a fullscreen window for the ui.
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor)) || is_wlroots || is_hyprland;
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND
|| (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor) && get_focused_window(display, WindowCaptureType::FOCUSED, false) == x11_cursor_window)
|| is_wlroots
|| is_hyprland;
if(prevent_game_minimizing) {
window_pos = focused_monitor->position;
@@ -1909,25 +1913,26 @@ namespace gsr {
return ClipboardFile::FileType::PNG;
}
void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
void Overlay::save_video_in_current_game_directory(std::string &video_filepath, NotificationType notification_type) {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const std::string video_filename = filepath_get_filename(video_filepath);
const std::string video_filename = filepath_get_filename(video_filepath.c_str());
const Window gsr_ui_window = window ? (Window)window->get_system_handle() : None;
std::string focused_window_name = get_window_name_at_cursor_position(display, gsr_ui_window);
if(focused_window_name.empty())
focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED);
focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED, false);
if(focused_window_name.empty())
focused_window_name = "Game";
string_replace_characters(focused_window_name.data(), "/\\", ' ');
std::string video_directory = filepath_get_directory(video_filepath) + "/" + focused_window_name;
std::string video_directory = filepath_get_directory(video_filepath.c_str()) + "/" + focused_window_name;
create_directory_recursive(video_directory.data());
const std::string new_video_filepath = video_directory + "/" + video_filename;
rename(video_filepath, new_video_filepath.c_str());
rename(video_filepath.c_str(), new_video_filepath.c_str());
video_filepath = new_video_filepath;
truncate_string(focused_window_name, 40);
const char *capture_target = nullptr;
@@ -1935,15 +1940,6 @@ namespace gsr {
switch(notification_type) {
case NotificationType::RECORD: {
if(led_indicator) {
if(recording_status == RecordingStatus::REPLAY && !current_recording_config.replay_config.record_options.use_led_indicator)
led_indicator->set_led(false);
else if(recording_status == RecordingStatus::STREAM && !current_recording_config.streaming_config.record_options.use_led_indicator)
led_indicator->set_led(false);
else if(config.record_config.record_options.use_led_indicator)
led_indicator->blink();
}
if(!config.record_config.record_options.show_notifications)
return;
@@ -1955,9 +1951,6 @@ namespace gsr {
break;
}
case NotificationType::REPLAY: {
if(led_indicator && config.replay_config.record_options.use_led_indicator)
led_indicator->blink();
if(!config.replay_config.record_options.show_notifications)
return;
@@ -1969,18 +1962,12 @@ namespace gsr {
break;
}
case NotificationType::SCREENSHOT: {
if(led_indicator && config.screenshot_config.use_led_indicator)
led_indicator->blink();
if(!config.screenshot_config.show_notifications)
return;
snprintf(msg, sizeof(msg), "Saved a screenshot of %s\nto \"%s\"",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str(), focused_window_name.c_str());
capture_target = screenshot_capture_target.c_str();
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(new_video_filepath, filename_to_clipboard_file_type(new_video_filepath));
break;
}
case NotificationType::NONE:
@@ -2003,22 +1990,20 @@ namespace gsr {
void Overlay::on_replay_saved(const char *replay_saved_filepath) {
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);
return;
} else {
if(config.replay_config.record_options.show_notifications) {
const std::string duration_str = to_duration_string(get_time_passed_in_replay_buffer_seconds());
std::string filepath = replay_saved_filepath;
save_video_in_current_game_directory(filepath, NotificationType::REPLAY);
} else if(config.replay_config.record_options.show_notifications) {
const std::string duration_str = to_duration_string(get_time_passed_in_replay_buffer_seconds());
char msg[512];
snprintf(msg, sizeof(msg), "Saved a %s replay of %s",
duration_str.c_str(),
capture_target_get_notification_name(recording_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
}
if(led_indicator && config.replay_config.record_options.use_led_indicator)
led_indicator->blink();
char msg[512];
snprintf(msg, sizeof(msg), "Saved a %s replay of %s",
duration_str.c_str(),
capture_target_get_notification_name(recording_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
}
if(led_indicator && config.replay_config.record_options.use_led_indicator)
led_indicator->blink();
}
void Overlay::process_gsr_output() {
@@ -2045,7 +2030,8 @@ namespace gsr {
const std::string video_filepath = filepath_get_filename(line);
if(starts_with(video_filepath, "Video_")) {
on_stop_recording(0, line);
record_filepath = line;
on_stop_recording(0, record_filepath);
return;
}
@@ -2182,20 +2168,25 @@ 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 {
if(config.screenshot_config.show_notifications) {
char msg[512];
snprintf(msg, sizeof(msg), "Saved a screenshot of %s",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
save_video_in_current_game_directory(screenshot_filepath, NotificationType::SCREENSHOT);
} else if(config.screenshot_config.show_notifications) {
char msg[512];
snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
}
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(screenshot_filepath, filename_to_clipboard_file_type(screenshot_filepath));
}
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(screenshot_filepath, filename_to_clipboard_file_type(screenshot_filepath));
if(led_indicator && config.screenshot_config.use_led_indicator)
led_indicator->blink();
if(led_indicator && config.screenshot_config.use_led_indicator)
led_indicator->blink();
if(!strip(config.screenshot_config.custom_script).empty()) {
std::stringstream ss;
ss << config.screenshot_config.custom_script << " " << std::quoted(screenshot_filepath);
const std::string command = ss.str();
const char *args[] = { "/bin/sh", "-c", command.c_str(), nullptr };
exec_program_on_host_daemonized(args);
}
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code);
@@ -2229,6 +2220,18 @@ namespace gsr {
return true;
}
static bool is_webcam_available_to_capture(const RecordOptions &record_options) {
if(record_options.webcam_source.empty())
return true;
const auto cameras = get_v4l2_devices();
for(const GsrCamera &camera : cameras) {
if(camera.path == record_options.webcam_source)
return true;
}
return false;
}
void Overlay::replay_status_update_status() {
if(replay_status_update_clock.get_elapsed_time_seconds() < replay_status_update_check_timeout_seconds)
return;
@@ -2246,7 +2249,7 @@ namespace gsr {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED);
const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED, false);
if(window && focused_window == (Window)window->get_system_handle())
return;
@@ -2254,7 +2257,7 @@ namespace gsr {
focused_window_is_fullscreen = focused_window != 0 && window_is_fullscreen(display, focused_window);
if(focused_window_is_fullscreen != prev_focused_window_is_fullscreen) {
if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) {
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list))
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list) && is_webcam_available_to_capture(config.replay_config.record_options))
on_press_start_replay(false, false);
} else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) {
on_press_start_replay(true, false);
@@ -2271,7 +2274,7 @@ namespace gsr {
power_supply_connected = power_supply_online_filepath.empty() || power_supply_is_connected(power_supply_online_filepath.c_str());
if(power_supply_connected != prev_power_supply_status) {
if(recording_status == RecordingStatus::NONE && power_supply_connected) {
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list))
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list) && is_webcam_available_to_capture(config.replay_config.record_options))
on_press_start_replay(false, false);
} else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) {
on_press_start_replay(false, false);
@@ -2283,33 +2286,31 @@ namespace gsr {
if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP || recording_status != RecordingStatus::NONE || !try_replay_startup)
return;
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list))
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list) && is_webcam_available_to_capture(config.replay_config.record_options))
on_press_start_replay(true, false);
}
void Overlay::on_stop_recording(int exit_code, const std::string &video_filepath) {
void Overlay::on_stop_recording(int exit_code, std::string &video_filepath) {
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 {
if(config.record_config.record_options.show_notifications) {
const std::string duration_str = to_duration_string(recording_duration_clock.get_elapsed_time_seconds() - paused_total_time_seconds - (paused ? paused_clock.get_elapsed_time_seconds() : 0.0));
save_video_in_current_game_directory(video_filepath, NotificationType::RECORD);
} else if(config.record_config.record_options.show_notifications) {
const std::string duration_str = to_duration_string(recording_duration_clock.get_elapsed_time_seconds() - paused_total_time_seconds - (paused ? paused_clock.get_elapsed_time_seconds() : 0.0));
char msg[512];
snprintf(msg, sizeof(msg), "Saved a %s recording of %s",
duration_str.c_str(),
capture_target_get_notification_name(recording_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
}
char msg[512];
snprintf(msg, sizeof(msg), "Saved a %s recording of %s",
duration_str.c_str(),
capture_target_get_notification_name(recording_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
}
if(led_indicator) {
if(recording_status == RecordingStatus::REPLAY && !current_recording_config.replay_config.record_options.use_led_indicator)
led_indicator->set_led(false);
else if(recording_status == RecordingStatus::STREAM && !current_recording_config.streaming_config.record_options.use_led_indicator)
led_indicator->set_led(false);
else if(config.record_config.record_options.use_led_indicator)
led_indicator->blink();
}
if(led_indicator) {
if(recording_status == RecordingStatus::REPLAY && !current_recording_config.replay_config.record_options.use_led_indicator)
led_indicator->set_led(false);
else if(recording_status == RecordingStatus::STREAM && !current_recording_config.streaming_config.record_options.use_led_indicator)
led_indicator->set_led(false);
else if(config.record_config.record_options.use_led_indicator)
led_indicator->blink();
}
} else {
on_gsr_process_error(exit_code, NotificationType::RECORD);
@@ -2682,6 +2683,66 @@ namespace gsr {
return framerate_mode;
}
struct CameraAlignment {
std::string halign;
std::string valign;
mgl::vec2i pos;
};
static CameraAlignment position_to_alignment(mgl::vec2i pos, mgl::vec2i size) {
const mgl::vec2i pos_overflow = mgl::vec2i(100, 100) - (pos + size);
if(pos_overflow.x < 0)
pos.x += pos_overflow.x;
if(pos_overflow.y < 0)
pos.y += pos_overflow.y;
if(pos.x < 0)
pos.x = 0;
if(pos.y < 0)
pos.y = 0;
CameraAlignment camera_alignment;
const mgl::vec2i center = pos + size/2;
if(center.x < 50) {
camera_alignment.halign = "start";
camera_alignment.pos.x = pos.x;
} else {
camera_alignment.halign = "end";
camera_alignment.pos.x = -(100 - (pos.x + size.x));
}
if(center.y < 50) {
camera_alignment.valign = "start";
camera_alignment.pos.y = pos.y;
} else {
camera_alignment.valign = "end";
camera_alignment.pos.y = -(100 - (pos.y + size.y));
}
return camera_alignment;
}
static std::string compose_capture_source_arg(const std::string &capture_target, const RecordOptions &record_options) {
std::string capture_source_arg = capture_target;
if(!record_options.webcam_source.empty()) {
const mgl::vec2i webcam_size(record_options.webcam_width, record_options.webcam_height);
const CameraAlignment camera_alignment = position_to_alignment(mgl::vec2i(record_options.webcam_x, record_options.webcam_y), webcam_size);
capture_source_arg += "|" + record_options.webcam_source;
capture_source_arg += ";halign=" + camera_alignment.halign;
capture_source_arg += ";valign=" + camera_alignment.valign;
capture_source_arg += ";x=" + std::to_string(camera_alignment.pos.x) + "%";
capture_source_arg += ";y=" + std::to_string(camera_alignment.pos.y) + "%";
capture_source_arg += ";width=" + std::to_string(webcam_size.x) + "%";
capture_source_arg += ";height=" + std::to_string(webcam_size.y) + "%";
capture_source_arg += ";pixfmt=" + record_options.webcam_video_format;
if(record_options.webcam_flip_horizontally)
capture_source_arg += ";hflip=true";
}
return capture_source_arg;
}
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
if(region_selector.is_started() || window_selector.is_started())
return false;
@@ -2771,8 +2832,10 @@ namespace gsr {
if(config.replay_config.record_options.record_area_option != "focused" && config.replay_config.record_options.change_video_resolution)
snprintf(size, sizeof(size), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height);
const std::string capture_source_arg = compose_capture_source_arg(recording_capture_target, config.replay_config.record_options);
std::vector<const char*> args = {
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
"gpu-screen-recorder", "-w", capture_source_arg.c_str(),
"-c", container,
"-ac", config.replay_config.record_options.audio_codec.c_str(),
"-cursor", config.replay_config.record_options.record_cursor ? "yes" : "no",
@@ -2993,8 +3056,10 @@ namespace gsr {
if(config.record_config.record_options.record_area_option != "focused" && config.record_config.record_options.change_video_resolution)
snprintf(size, sizeof(size), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height);
const std::string capture_source_arg = compose_capture_source_arg(recording_capture_target, config.record_config.record_options);
std::vector<const char*> args = {
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
"gpu-screen-recorder", "-w", capture_source_arg.c_str(),
"-c", container,
"-ac", config.record_config.record_options.audio_codec.c_str(),
"-cursor", config.record_config.record_options.record_cursor ? "yes" : "no",
@@ -3181,8 +3246,10 @@ namespace gsr {
if(config.streaming_config.record_options.record_area_option != "focused" && config.streaming_config.record_options.change_video_resolution)
snprintf(size, sizeof(size), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height);
const std::string capture_source_arg = compose_capture_source_arg(recording_capture_target, config.streaming_config.record_options);
std::vector<const char*> args = {
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
"gpu-screen-recorder", "-w", capture_source_arg.c_str(),
"-c", container,
"-ac", config.streaming_config.record_options.audio_codec.c_str(),
"-cursor", config.streaming_config.record_options.record_cursor ? "yes" : "no",
@@ -3312,7 +3379,7 @@ namespace gsr {
args.push_back("yes");
}
const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gpu-screen-recorder/gsr-ui-window-capture-token";
const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gsr-ui-window-capture-token";
if(record_area_option == "portal") {
hide_ui = true;
if(force_type == ScreenshotForceType::WINDOW) {

View File

@@ -73,6 +73,28 @@ namespace gsr {
return true;
}
bool exec_program_on_host_daemonized(const char **args, bool debug) {
if(count_num_args(args) > 64 - 3) {
fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]);
return -1;
}
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
if(inside_flatpak) {
// Assumes programs wont need more than 64 - 3 args
const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
for(int i = 3; i < 64; ++i) {
const char *arg = args[i - 3];
modified_args[i] = arg;
if(!arg)
break;
}
return exec_program_daemonized(modified_args, debug);
} else {
return exec_program_daemonized(args, debug);
}
}
pid_t exec_program(const char **args, int *read_fd, bool debug) {
if(read_fd)
*read_fd = -1;
@@ -164,11 +186,9 @@ namespace gsr {
const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
for(int i = 3; i < 64; ++i) {
const char *arg = args[i - 3];
if(!arg) {
modified_args[i] = nullptr;
break;
}
modified_args[i] = arg;
if(!arg)
break;
}
return exec_program_get_stdout(modified_args, result, debug);
} else {

View File

@@ -48,6 +48,9 @@ namespace gsr {
if(!theme->body_font.load_from_file(theme->body_font_file, std::max(13.0f, window_size.y * 0.015f)))
return false;
if(!theme->camera_setup_font.load_from_file(theme->body_font_file, 24))
return false;
return true;
}

View File

@@ -121,7 +121,7 @@ namespace gsr {
return root_pos;
}
Window get_focused_window(Display *dpy, WindowCaptureType cap_type) {
Window get_focused_window(Display *dpy, WindowCaptureType cap_type, bool fallback_cursor_focused) {
//const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
Window focused_window = None;
@@ -146,6 +146,9 @@ namespace gsr {
XGetInputFocus(dpy, &focused_window, &revert_to);
if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
return focused_window;
if(!fallback_cursor_focused)
return None;
}
get_cursor_position(dpy, &focused_window);
@@ -213,9 +216,9 @@ namespace gsr {
return result;
}
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type, bool fallback_cursor_focused) {
std::string result;
const Window focused_window = get_focused_window(dpy, window_capture_type);
const Window focused_window = get_focused_window(dpy, window_capture_type, fallback_cursor_focused);
if(focused_window == None)
return result;

View File

@@ -90,6 +90,13 @@ namespace gsr {
dirty = true;
}
void ComboBox::clear_items() {
items.clear();
selected_item = 0;
show_dropdown = false;
dirty = true;
}
void ComboBox::set_selected_item(const std::string &id, bool trigger_event, bool trigger_event_even_if_selection_not_changed) {
for(size_t i = 0; i < items.size(); ++i) {
auto &item = items[i];

View File

@@ -1,4 +1,5 @@
#include "../../include/gui/CustomRendererWidget.hpp"
#include "../../include/gui/Utils.hpp"
#include <mglpp/window/Window.hpp>
@@ -17,11 +18,14 @@ namespace gsr {
const mgl::vec2f draw_pos = position + offset;
const mgl::Scissor prev_scissor = window.get_scissor();
window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()});
const mgl::Scissor parent_scissor = window.get_scissor();
const mgl::Scissor scissor = scissor_get_sub_area(parent_scissor, {draw_pos.to_vec2i(), size.to_vec2i()});
window.set_scissor(scissor);
if(draw_handler)
draw_handler(window, draw_pos, size);
window.set_scissor(prev_scissor);
window.set_scissor(parent_scissor);
}
mgl::vec2f CustomRendererWidget::get_size() {

View File

@@ -54,7 +54,7 @@ namespace gsr {
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_area() {
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture source:", get_color_theme().text_color));
record_area_list->add_widget(create_record_area_box());
return record_area_list;
}
@@ -139,7 +139,7 @@ namespace gsr {
return list;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_cursor_section() {
auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record cursor");
record_cursor_checkbox->set_checked(true);
@@ -163,7 +163,7 @@ namespace gsr {
auto select_directory_page = std::make_unique<GsrPage>("File", "Settings");
select_directory_page->add_button("Save", "save", get_color_theme().tint_color);
select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color);
auto file_chooser = std::make_unique<FileChooser>(save_directory_button_ptr->get_text().c_str(), select_directory_page->get_inner_size());
FileChooser *file_chooser_ptr = file_chooser.get();
select_directory_page->add_widget(std::move(file_chooser));
@@ -249,6 +249,27 @@ namespace gsr {
return std::make_unique<Subsection>("Screenshot indicator", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<List> ScreenshotSettingsPage::create_custom_script_screenshot_entry() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL, List::Alignment::CENTER);
auto create_custom_script_screenshot_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
create_custom_script_screenshot_entry_ptr = create_custom_script_screenshot_entry.get();
list->add_widget(std::move(create_custom_script_screenshot_entry));
return list;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_custom_script_screenshot() {
auto custom_script_screenshot_list = std::make_unique<List>(List::Orientation::VERTICAL);
custom_script_screenshot_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Command to open the screenshot with:", get_color_theme().text_color));
custom_script_screenshot_list->add_widget(create_custom_script_screenshot_entry());
return custom_script_screenshot_list;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_custom_script_screenshot_section() {
return std::make_unique<Subsection>("Script", create_custom_script_screenshot(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() {
auto page_list = std::make_unique<List>(List::Orientation::VERTICAL);
page_list->set_spacing(0.018f);
@@ -263,6 +284,7 @@ namespace gsr {
settings_list->add_widget(create_file_info_section());
settings_list->add_widget(create_general_section());
settings_list->add_widget(create_screenshot_indicator_section());
settings_list->add_widget(create_custom_script_screenshot_section());
settings_scrollable_page_ptr->add_widget(std::move(settings_list));
return page_list;
}
@@ -321,6 +343,8 @@ namespace gsr {
if(config.screenshot_config.image_height < 32)
config.screenshot_config.image_height = 32;
image_height_entry_ptr->set_text(std::to_string(config.screenshot_config.image_height));
create_custom_script_screenshot_entry_ptr->set_text(config.screenshot_config.custom_script);
}
void ScreenshotSettingsPage::save() {
@@ -337,6 +361,7 @@ namespace gsr {
config.screenshot_config.save_screenshot_to_clipboard = save_screenshot_to_clipboard_checkbox_ptr->is_checked();
config.screenshot_config.show_notifications = show_notification_checkbox_ptr->is_checked();
config.screenshot_config.use_led_indicator = led_indicator_checkbox_ptr->is_checked();
config.screenshot_config.custom_script = create_custom_script_screenshot_entry_ptr->get_text();
if(config.screenshot_config.image_width == 0)
config.screenshot_config.image_width = 1920;
@@ -356,4 +381,4 @@ namespace gsr {
save_config(config);
}
}
}

View File

@@ -50,7 +50,9 @@ namespace gsr {
return false;
}
if(event.type == mgl::Event::MouseButtonPressed || event.type == mgl::Event::MouseButtonReleased) {
// Pass release to children even if outside area, because we want to be able to release mouse when moved outside,
// for example in Entry when selecting text
if(event.type == mgl::Event::MouseButtonPressed/* || event.type == mgl::Event::MouseButtonReleased*/) {
if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_button.x, event.mouse_button.y}))
return true;
} else if(event.type == mgl::Event::MouseMoved) {

View File

@@ -4,11 +4,16 @@
#include "../../include/gui/PageStack.hpp"
#include "../../include/gui/FileChooser.hpp"
#include "../../include/gui/Subsection.hpp"
#include "../../include/gui/Image.hpp"
#include "../../include/gui/CustomRendererWidget.hpp"
#include "../../include/gui/Utils.hpp"
#include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
#include "mglpp/window/Window.hpp"
#include "mglpp/window/Event.hpp"
#include <algorithm>
#include <cmath>
#include <string.h>
namespace gsr {
@@ -87,7 +92,7 @@ namespace gsr {
std::unique_ptr<Widget> SettingsPage::create_record_area() {
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture source:", get_color_theme().text_color));
record_area_list->add_widget(create_record_area_box());
return record_area_list;
}
@@ -187,6 +192,214 @@ namespace gsr {
return std::make_unique<Subsection>("Capture", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<List> SettingsPage::create_webcam_sources() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
ll->add_widget(std::make_unique<Label>(&get_theme().body_font, "Webcam source:", get_color_theme().text_color));
auto combobox = std::make_unique<ComboBox>(&get_theme().body_font);
combobox->add_item("None", "");
for(const GsrCamera &camera : capture_options.cameras) {
combobox->add_item(camera.path, camera.path);
}
webcam_sources_box_ptr = combobox.get();
webcam_sources_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
selected_camera = std::nullopt;
webcam_video_format_box_ptr->clear_items();
if(id == "") {
webcam_body_list_ptr->set_visible(false);
return;
}
auto it = std::find_if(capture_options.cameras.begin(), capture_options.cameras.end(), [&](const GsrCamera &camera) {
return camera.path == id;
});
if(it == capture_options.cameras.end())
return;
webcam_body_list_ptr->set_visible(true);
webcam_video_format_box_ptr->add_item("Auto (recommended)", "auto");
if(it->supported_pixel_formats.yuyv)
webcam_video_format_box_ptr->add_item("YUYV", "yuyv");
if(it->supported_pixel_formats.mjpeg)
webcam_video_format_box_ptr->add_item("Motion-JPEG", "mjpeg");
webcam_video_format_box_ptr->set_selected_item(get_current_record_options().webcam_video_format);
selected_camera = *it;
};
ll->add_widget(std::move(combobox));
return ll;
}
std::unique_ptr<List> SettingsPage::create_webcam_video_format() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
ll->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video format:", get_color_theme().text_color));
auto combobox = std::make_unique<ComboBox>(&get_theme().body_font);
webcam_video_format_box_ptr = combobox.get();
webcam_video_format_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
get_current_record_options().webcam_video_format = id;
};
ll->add_widget(std::move(combobox));
return ll;
}
std::unique_ptr<Widget> SettingsPage::create_webcam_section() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
ll->add_widget(create_webcam_sources());
auto body_list = std::make_unique<List>(List::Orientation::VERTICAL);
body_list->set_visible(false);
webcam_body_list_ptr = body_list.get();
{
const float camera_screen_width = std::min(400.0f, (float)settings_scrollable_page_ptr->get_inner_size().x * 0.90f);
camera_screen_size = mgl::vec2f(camera_screen_width, camera_screen_width * 0.5625);
const float screen_border = 2.0f;
const mgl::vec2f screen_border_size(screen_border, screen_border);
screen_inner_size = mgl::vec2f(camera_screen_size - screen_border_size*2.0f);
const mgl::vec2f bounding_box_size(30.0f, 30.0f);
auto camera_location_widget = std::make_unique<CustomRendererWidget>(camera_screen_size);
camera_location_widget->draw_handler = [this, screen_border_size, screen_border](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) {
if(!selected_camera.has_value())
return;
pos = pos.floor();
size = size.floor();
const mgl::vec2i mouse_pos = window.get_mouse_position();
const mgl::vec2f webcam_box_min_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), screen_inner_size * 0.2f);
if(moving_webcam_box) {
webcam_box_pos = mouse_pos.to_vec2f() - screen_border_size - webcam_box_grab_offset - pos;
} else if(webcam_resize_corner == WebcamBoxResizeCorner::BOTTOM_RIGHT) {
const mgl::vec2f mouse_diff = mouse_pos.to_vec2f() - webcam_resize_start_pos;
webcam_box_size = webcam_box_size_resize_start + mouse_diff;
}
webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
if(webcam_box_pos.x < 0.0f)
webcam_box_pos.x = 0.0f;
else if(webcam_box_pos.x + webcam_box_size.x > screen_inner_size.x)
webcam_box_pos.x = screen_inner_size.x - webcam_box_size.x;
if(webcam_box_pos.y < 0.0f)
webcam_box_pos.y = 0.0f;
else if(webcam_box_pos.y + webcam_box_size.y > screen_inner_size.y)
webcam_box_pos.y = screen_inner_size.y - webcam_box_size.y;
if(webcam_box_size.x < webcam_box_min_size.x)
webcam_box_size.x = webcam_box_min_size.x;
else if(webcam_box_pos.x + webcam_box_size.x > screen_inner_size.x)
webcam_box_size.x = screen_inner_size.x - webcam_box_pos.x;
//webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
if(webcam_box_size.y < webcam_box_min_size.y)
webcam_box_size.y = webcam_box_min_size.y;
else if(webcam_box_pos.y + webcam_box_size.y > screen_inner_size.y)
webcam_box_size.y = screen_inner_size.y - webcam_box_pos.y;
webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
{
draw_rectangle_outline(window, pos, size, mgl::Color(255, 0, 0, 255), screen_border);
mgl::Text screen_text("Screen", get_theme().camera_setup_font);
screen_text.set_position((pos + size * 0.5f - screen_text.get_bounds().size * 0.5f).floor());
window.draw(screen_text);
}
{
webcam_box_drawn_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
webcam_box_drawn_pos = (pos + screen_border_size + webcam_box_pos).floor();
draw_rectangle_outline(window, webcam_box_drawn_pos, webcam_box_drawn_size, mgl::Color(0, 255, 0, 255), screen_border);
// mgl::Rectangle resize_area(webcam_box_drawn_pos + webcam_box_drawn_size - bounding_box_size*0.5f - screen_border_size*0.5f, bounding_box_size);
// resize_area.set_color(mgl::Color(0, 0, 255, 255));
// window.draw(resize_area);
mgl::Text webcam_text("Webcam", get_theme().camera_setup_font);
webcam_text.set_position((webcam_box_drawn_pos + webcam_box_drawn_size * 0.5f - webcam_text.get_bounds().size * 0.5f).floor());
window.draw(webcam_text);
}
};
camera_location_widget->event_handler = [this, screen_border_size, bounding_box_size](mgl::Event &event, mgl::Window&, mgl::vec2f, mgl::vec2f) {
switch(event.type) {
case mgl::Event::MouseButtonPressed: {
if(event.mouse_button.button == mgl::Mouse::Left && webcam_resize_corner == WebcamBoxResizeCorner::NONE) {
const mgl::vec2f mouse_button_pos(event.mouse_button.x, event.mouse_button.y);
if(mgl::FloatRect(webcam_box_drawn_pos, webcam_box_drawn_size).contains(mouse_button_pos)) {
moving_webcam_box = true;
webcam_box_grab_offset = mouse_button_pos - webcam_box_drawn_pos;
} else {
moving_webcam_box = false;
}
} else if(event.mouse_button.button == mgl::Mouse::Right && !moving_webcam_box) {
const mgl::vec2f mouse_button_pos(event.mouse_button.x, event.mouse_button.y);
webcam_resize_start_pos = mouse_button_pos;
webcam_box_pos_resize_start = webcam_box_pos;
webcam_box_size_resize_start = webcam_box_size;
webcam_box_grab_offset = mouse_button_pos - webcam_box_drawn_pos;
/*if(mgl::FloatRect(webcam_box_drawn_pos - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
webcam_resize_corner = WebcamBoxResizeCorner::TOP_LEFT;
fprintf(stderr, "top left\n");
} else if(mgl::FloatRect(webcam_box_drawn_pos + mgl::vec2f(webcam_box_drawn_size.x, 0.0f) - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
webcam_resize_corner = WebcamBoxResizeCorner::TOP_RIGHT;
fprintf(stderr, "top right\n");
} else if(mgl::FloatRect(webcam_box_drawn_pos + mgl::vec2f(0.0f, webcam_box_drawn_size.y) - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
webcam_resize_corner = WebcamBoxResizeCorner::BOTTOM_LEFT;
fprintf(stderr, "bottom left\n");
} else */if(mgl::FloatRect(webcam_box_drawn_pos + webcam_box_drawn_size - bounding_box_size*0.5f - screen_border_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
webcam_resize_corner = WebcamBoxResizeCorner::BOTTOM_RIGHT;
} else {
webcam_resize_corner = WebcamBoxResizeCorner::NONE;
}
}
break;
}
case mgl::Event::MouseButtonReleased: {
if(event.mouse_button.button == mgl::Mouse::Left && webcam_resize_corner == WebcamBoxResizeCorner::NONE) {
moving_webcam_box = false;
} else if(event.mouse_button.button == mgl::Mouse::Right && !moving_webcam_box) {
webcam_resize_corner = WebcamBoxResizeCorner::NONE;
}
break;
}
default: {
break;
}
}
return true;
};
body_list->add_widget(std::move(camera_location_widget));
}
body_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "* Right click in the bottom right corner to resize the webcam", get_color_theme().text_color));
{
auto flip_camera_horizontally_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Flip camera horizontally");
flip_camera_horizontally_checkbox_ptr = flip_camera_horizontally_checkbox.get();
body_list->add_widget(std::move(flip_camera_horizontally_checkbox));
}
body_list->add_widget(create_webcam_video_format());
ll->add_widget(std::move(body_list));
return std::make_unique<Subsection>("Webcam", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
static bool audio_device_is_output(const std::string &audio_device_id) {
return audio_device_id == "default_output" || ends_with(audio_device_id, ".monitor");
}
@@ -618,6 +831,7 @@ namespace gsr {
auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
settings_list->set_spacing(0.018f);
settings_list->add_widget(create_capture_target_section());
settings_list->add_widget(create_webcam_section());
settings_list->add_widget(create_audio_section());
settings_list->add_widget(create_video_section());
settings_list_ptr = settings_list.get();
@@ -830,6 +1044,16 @@ namespace gsr {
settings_scrollable_page_ptr->reset_scroll();
}
RecordOptions& SettingsPage::get_current_record_options() {
switch(type) {
default:
assert(false);
case Type::REPLAY: return config.replay_config.record_options;
case Type::RECORD: return config.record_config.record_options;
case Type::STREAM: return config.streaming_config.record_options;
}
}
std::unique_ptr<CheckBox> SettingsPage::create_led_indicator(const char *type) {
char label_str[256];
snprintf(label_str, sizeof(label_str), "Show %s status with scroll lock led", type);
@@ -1200,6 +1424,17 @@ namespace gsr {
show_notification_checkbox_ptr->set_checked(record_options.show_notifications);
led_indicator_checkbox_ptr->set_checked(record_options.use_led_indicator);
webcam_sources_box_ptr->set_selected_item(record_options.webcam_source);
flip_camera_horizontally_checkbox_ptr->set_checked(record_options.webcam_flip_horizontally);
webcam_video_format_box_ptr->set_selected_item(record_options.webcam_video_format);
webcam_box_pos.x = ((float)record_options.webcam_x / 100.0f) * screen_inner_size.x;
webcam_box_pos.y = ((float)record_options.webcam_y / 100.0f) * screen_inner_size.y;
webcam_box_size.x = ((float)record_options.webcam_width / 100.0f * screen_inner_size.x);
webcam_box_size.y = ((float)record_options.webcam_height / 100.0f * screen_inner_size.y);
if(selected_camera.has_value())
webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
if(record_options.record_area_width == 0)
record_options.record_area_width = 1920;
@@ -1332,6 +1567,17 @@ namespace gsr {
record_options.show_notifications = show_notification_checkbox_ptr->is_checked();
record_options.use_led_indicator = led_indicator_checkbox_ptr->is_checked();
if(selected_camera.has_value())
webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
record_options.webcam_source = webcam_sources_box_ptr->get_selected_id();
record_options.webcam_flip_horizontally = flip_camera_horizontally_checkbox_ptr->is_checked();
record_options.webcam_video_format = webcam_video_format_box_ptr->get_selected_id();
record_options.webcam_x = std::round((webcam_box_pos.x / screen_inner_size.x) * 100.0f);
record_options.webcam_y = std::round((webcam_box_pos.y / screen_inner_size.y) * 100.0f);
record_options.webcam_width = std::round((webcam_box_size.x / screen_inner_size.x) * 100.0f);
record_options.webcam_height = std::round((webcam_box_size.y / screen_inner_size.y) * 100.0f);
if(record_options.record_area_width == 0)
record_options.record_area_width = 1920;

View File

@@ -5,15 +5,15 @@
namespace gsr {
static double frame_delta_seconds = 1.0;
static mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
return { std::min(a.x, b.x), std::min(a.y, b.y) };
}
static mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b) {
mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b) {
return { std::max(a.x, b.x), std::max(a.y, b.y) };
}
static mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
return min_vec2i(max, max_vec2i(value, min));
}
@@ -91,7 +91,7 @@ namespace gsr {
const mgl::vec2i pos = clamp_vec2i(child.position, parent.position, parent.position + parent.size);
return mgl::Scissor{
pos,
min_vec2i(child.size, parent.position + parent.size - pos)
max_vec2i(mgl::vec2i(0, 0), min_vec2i(child.position + child.size - pos, parent.position + parent.size - pos))
};
}
}

View File

@@ -23,8 +23,10 @@ extern "C" {
}
static sig_atomic_t running = 1;
static sig_atomic_t killed = 0;
static void sigint_handler(int signal) {
(void)signal;
killed = 1;
running = 0;
}
@@ -331,6 +333,8 @@ int main(int argc, char **argv) {
}
}
const bool connected_to_display_server = mgl_is_connected_to_display_server();
fprintf(stderr, "Info: shutting down!\n");
rpc.reset();
overlay.reset();
@@ -344,5 +348,11 @@ int main(int argc, char **argv) {
return 0;
}
return mgl_is_connected_to_display_server() ? 0 : 1;
if(killed)
return 0;
if(connected_to_display_server)
return 1;
return 0;
}

View File

@@ -200,6 +200,9 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
return;
}
if(extra_data->is_non_keyboard_device)
return;
if(event.type == EV_SYN && event.code == SYN_DROPPED) {
/* TODO: Don't do this on every SYN_DROPPED to prevent spamming this, instead wait until the next event or wait for timeout */
keyboard_event_fetch_update_key_states(self, extra_data, fd);
@@ -210,6 +213,8 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
//fprintf(stderr, "fd: %d, type: %d, pressed %d, value: %d\n", fd, event.type, event.code, event.value);
//}
const bool prev_grabbed = extra_data->grabbed;
const bool keyboard_key = is_keyboard_key(event.code);
if(event.type == EV_KEY && keyboard_key) {
keyboard_event_process_key_state_change(self, &event, extra_data, fd);
@@ -228,7 +233,7 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
}
if(extra_data->grabbed) {
if(!self->check_grab_lock && (event.type == EV_KEY || event.type == EV_MSC)) {
if(prev_grabbed && !self->check_grab_lock && (event.type == EV_KEY || event.type == EV_MSC)) {
self->uinput_written_time_seconds = clock_get_monotonic_seconds();
self->check_grab_lock = true;
}