diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Config.cpp | 11 | ||||
-rw-r--r-- | src/CursorTracker/CursorTrackerWayland.cpp (renamed from src/CursorTrackerWayland.cpp) | 106 | ||||
-rw-r--r-- | src/CursorTracker/CursorTrackerX11.cpp (renamed from src/CursorTrackerX11.cpp) | 4 | ||||
-rw-r--r-- | src/GlobalHotkeys/GlobalHotkeysJoystick.cpp (renamed from src/GlobalHotkeysJoystick.cpp) | 79 | ||||
-rw-r--r-- | src/GlobalHotkeys/GlobalHotkeysLinux.cpp (renamed from src/GlobalHotkeysLinux.cpp) | 2 | ||||
-rw-r--r-- | src/GlobalHotkeys/GlobalHotkeysX11.cpp (renamed from src/GlobalHotkeysX11.cpp) | 2 | ||||
-rw-r--r-- | src/Overlay.cpp | 552 | ||||
-rw-r--r-- | src/Process.cpp | 23 | ||||
-rw-r--r-- | src/RegionSelector.cpp | 6 | ||||
-rw-r--r-- | src/Rpc.cpp | 7 | ||||
-rw-r--r-- | src/Theme.cpp | 27 | ||||
-rw-r--r-- | src/Utils.cpp | 27 | ||||
-rw-r--r-- | src/WindowSelector.cpp | 229 | ||||
-rw-r--r-- | src/WindowUtils.cpp | 25 | ||||
-rw-r--r-- | src/gui/ComboBox.cpp | 2 | ||||
-rw-r--r-- | src/gui/GlobalSettingsPage.cpp | 5 | ||||
-rw-r--r-- | src/gui/RadioButton.cpp | 11 | ||||
-rw-r--r-- | src/gui/ScreenshotSettingsPage.cpp | 18 | ||||
-rw-r--r-- | src/gui/SettingsPage.cpp | 137 | ||||
-rw-r--r-- | src/main.cpp | 63 |
20 files changed, 1015 insertions, 321 deletions
diff --git a/src/Config.cpp b/src/Config.cpp index e920bf0..313cd38 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -1,7 +1,7 @@ #include "../include/Config.hpp" #include "../include/Utils.hpp" #include "../include/GsrInfo.hpp" -#include "../include/GlobalHotkeys.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeys.hpp" #include <variant> #include <limits.h> #include <inttypes.h> @@ -119,16 +119,16 @@ namespace gsr { streaming_config.record_options.video_quality = "custom"; streaming_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); - streaming_config.record_options.video_bitrate = 15000; + streaming_config.record_options.video_bitrate = 8000; record_config.save_directory = default_videos_save_directory; record_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); - record_config.record_options.video_bitrate = 45000; + record_config.record_options.video_bitrate = 40000; replay_config.record_options.video_quality = "custom"; replay_config.save_directory = default_videos_save_directory; replay_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); - replay_config.record_options.video_bitrate = 45000; + replay_config.record_options.video_bitrate = 40000; screenshot_config.save_directory = default_pictures_save_directory; @@ -201,6 +201,7 @@ namespace gsr { {"streaming.service", &config.streaming_config.streaming_service}, {"streaming.youtube.key", &config.streaming_config.youtube.stream_key}, {"streaming.twitch.key", &config.streaming_config.twitch.stream_key}, + {"streaming.rumble.key", &config.streaming_config.rumble.stream_key}, {"streaming.custom.url", &config.streaming_config.custom.url}, {"streaming.custom.container", &config.streaming_config.custom.container}, {"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey}, @@ -229,6 +230,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}, @@ -264,6 +266,7 @@ namespace gsr { {"replay.save_directory", &config.replay_config.save_directory}, {"replay.container", &config.replay_config.container}, {"replay.time", &config.replay_config.replay_time}, + {"replay.replay_storage", &config.replay_config.replay_storage}, {"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey}, {"replay.save_hotkey", &config.replay_config.save_hotkey}, {"replay.save_1_min_hotkey", &config.replay_config.save_1_min_hotkey}, diff --git a/src/CursorTrackerWayland.cpp b/src/CursorTracker/CursorTrackerWayland.cpp index 5f37d0a..7af86b4 100644 --- a/src/CursorTrackerWayland.cpp +++ b/src/CursorTracker/CursorTrackerWayland.cpp @@ -1,4 +1,4 @@ -#include "../include/CursorTrackerWayland.hpp" +#include "../../include/CursorTracker/CursorTrackerWayland.hpp" #include <string.h> #include <unistd.h> #include <fcntl.h> @@ -9,14 +9,8 @@ namespace gsr { static const int MAX_CONNECTORS = 32; - static const int CONNECTOR_TYPE_COUNTS = 32; static const uint32_t plane_property_all = 0xF; - typedef struct { - int type; - int count; - } drm_connector_type_count; - typedef enum { PLANE_PROPERTY_CRTC_X = 1 << 0, PLANE_PROPERTY_CRTC_Y = 1 << 1, @@ -27,19 +21,20 @@ namespace gsr { typedef struct { uint64_t crtc_id; mgl::vec2i size; + bool vrr_enabled; } drm_connector; typedef struct { drm_connector connectors[MAX_CONNECTORS]; int num_connectors; + bool has_any_crtc_with_vrr_enabled; } drm_connectors; /* Returns plane_property_mask */ - static uint32_t plane_get_properties(int drm_fd, uint32_t plane_id, int *crtc_x, int *crtc_y, int *crtc_id, bool *is_cursor) { + static uint32_t plane_get_properties(int drm_fd, uint32_t plane_id, int *crtc_x, int *crtc_y, int *crtc_id) { *crtc_x = 0; *crtc_y = 0; *crtc_id = 0; - *is_cursor = false; uint32_t property_mask = 0; @@ -80,8 +75,8 @@ namespace gsr { return property_mask; } - static bool connector_get_property_by_name(int drm_fd, drmModeConnectorPtr props, const char *name, uint64_t *result) { - for(int i = 0; i < props->count_props; ++i) { + static bool get_drm_property_by_name(int drm_fd, drmModeObjectPropertiesPtr props, const char *name, uint64_t *result) { + for(uint32_t i = 0; i < props->count_props; ++i) { drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]); if(!prop) continue; @@ -96,20 +91,12 @@ namespace gsr { return false; } - static drm_connector_type_count* drm_connector_types_get_index(drm_connector_type_count *type_counts, int *num_type_counts, int connector_type) { - for(int i = 0; i < *num_type_counts; ++i) { - if(type_counts[i].type == connector_type) - return &type_counts[i]; - } - - if(*num_type_counts == CONNECTOR_TYPE_COUNTS) - return NULL; - - const int index = *num_type_counts; - type_counts[index].type = connector_type; - type_counts[index].count = 0; - ++*num_type_counts; - return &type_counts[index]; + static bool connector_get_property_by_name(int drm_fd, drmModeConnectorPtr props, const char *name, uint64_t *result) { + drmModeObjectProperties properties; + properties.count_props = (uint32_t)props->count_props; + properties.props = props->props; + properties.prop_values = props->prop_values; + return get_drm_property_by_name(drm_fd, &properties, name, result); } // Note: this monitor name logic is kept in sync with gpu screen recorder @@ -119,27 +106,23 @@ namespace gsr { if(!resources) return result; - drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS]; - int num_type_counts = 0; - for(int i = 0; i < resources->count_connectors; ++i) { uint64_t connector_crtc_id = 0; drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]); if(!connector) continue; - drm_connector_type_count *connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector->connector_type); const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type); - if(connector_type) - ++connector_type->count; + if(!connection_name) + goto next; if(connector->connection != DRM_MODE_CONNECTED) goto next; - if(connector_type && connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) { + if(connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) { result = connection_name; result += "-"; - result += std::to_string(connector_type->count); + result += std::to_string(connector->connector_type_id); drmModeFreeConnector(connector); break; } @@ -325,7 +308,7 @@ namespace gsr { }; /* Returns nullptr if not found */ - static const drm_connector* get_drm_connector_by_crtc_id(const drm_connectors *connectors, uint32_t crtc_id) { + static drm_connector* get_drm_connector_by_crtc_id(drm_connectors *connectors, uint32_t crtc_id) { for(int i = 0; i < connectors->num_connectors; ++i) { if(connectors->connectors[i].crtc_id == crtc_id) return &connectors->connectors[i]; @@ -335,6 +318,8 @@ namespace gsr { static void get_drm_connectors(int drm_fd, drm_connectors *drm_connectors) { drm_connectors->num_connectors = 0; + drm_connectors->has_any_crtc_with_vrr_enabled = false; + drmModeResPtr resources = drmModeGetResources(drm_fd); if(!resources) return; @@ -350,23 +335,59 @@ namespace gsr { uint64_t crtc_id = 0; connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &crtc_id); if(crtc_id == 0) - goto next; + goto next_connector; crtc = drmModeGetCrtc(drm_fd, crtc_id); if(!crtc) - goto next; + goto next_connector; drm_connectors->connectors[drm_connectors->num_connectors].crtc_id = crtc_id; drm_connectors->connectors[drm_connectors->num_connectors].size = mgl::vec2i{(int)crtc->width, (int)crtc->height}; + drm_connectors->connectors[drm_connectors->num_connectors].vrr_enabled = false; ++drm_connectors->num_connectors; - next: + next_connector: if(crtc) drmModeFreeCrtc(crtc); if(connector) drmModeFreeConnector(connector); } + + for(int i = 0; i < resources->count_crtcs; ++i) { + drmModeCrtcPtr crtc = nullptr; + drmModeObjectPropertiesPtr properties = nullptr; + uint64_t vrr_enabled = 0; + drm_connector *connector = nullptr; + + crtc = drmModeGetCrtc(drm_fd, resources->crtcs[i]); + if(!crtc) + continue; + + properties = drmModeObjectGetProperties(drm_fd, crtc->crtc_id, DRM_MODE_OBJECT_CRTC); + if(!properties) + goto next_crtc; + + if(!get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled)) + goto next_crtc; + + connector = get_drm_connector_by_crtc_id(drm_connectors, crtc->crtc_id); + if(!connector) + goto next_crtc; + + if(vrr_enabled) { + connector->vrr_enabled = true; + drm_connectors->has_any_crtc_with_vrr_enabled = true; + } + + next_crtc: + if(properties) + drmModeFreeObjectProperties(properties); + + if(crtc) + drmModeFreeCrtc(crtc); + } + drmModeFreeResources(resources); } @@ -392,19 +413,20 @@ namespace gsr { drm_connectors connectors; connectors.num_connectors = 0; + connectors.has_any_crtc_with_vrr_enabled = false; get_drm_connectors(drm_fd, &connectors); drmModePlaneResPtr planes = drmModeGetPlaneResources(drm_fd); if(!planes) return; + bool found_cursor = false; for(uint32_t i = 0; i < planes->count_planes; ++i) { drmModePlanePtr plane = nullptr; const drm_connector *connector = nullptr; int crtc_x = 0; int crtc_y = 0; int crtc_id = 0; - bool is_cursor = false; uint32_t property_mask = 0; plane = drmModeGetPlane(drm_fd, planes->planes[i]); @@ -414,7 +436,7 @@ namespace gsr { if(!plane->fb_id) goto next; - property_mask = plane_get_properties(drm_fd, planes->planes[i], &crtc_x, &crtc_y, &crtc_id, &is_cursor); + property_mask = plane_get_properties(drm_fd, planes->planes[i], &crtc_x, &crtc_y, &crtc_id); if(property_mask != plane_property_all || crtc_id <= 0) goto next; @@ -426,6 +448,7 @@ namespace gsr { latest_cursor_position.x = crtc_x; latest_cursor_position.y = crtc_y; latest_crtc_id = crtc_id; + found_cursor = true; drmModeFreePlane(plane); break; } @@ -434,6 +457,11 @@ namespace gsr { drmModeFreePlane(plane); } + // On kde plasma wayland (and possibly other wayland compositors) it uses a software cursor only for the monitors with vrr enabled. + // In that case we cant know the cursor location and we instead want to fallback to getting focused monitor by using the hack of creating a window and getting the position. + if(!found_cursor && latest_crtc_id > 0 && connectors.has_any_crtc_with_vrr_enabled) + latest_crtc_id = -1; + drmModeFreePlaneResources(planes); } diff --git a/src/CursorTrackerX11.cpp b/src/CursorTracker/CursorTrackerX11.cpp index 7c40cea..7c98f4d 100644 --- a/src/CursorTrackerX11.cpp +++ b/src/CursorTracker/CursorTrackerX11.cpp @@ -1,5 +1,5 @@ -#include "../include/CursorTrackerX11.hpp" -#include "../include/WindowUtils.hpp" +#include "../../include/CursorTracker/CursorTrackerX11.hpp" +#include "../../include/WindowUtils.hpp" namespace gsr { CursorTrackerX11::CursorTrackerX11(Display *dpy) : dpy(dpy) { diff --git a/src/GlobalHotkeysJoystick.cpp b/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp index 822a73a..5969438 100644 --- a/src/GlobalHotkeysJoystick.cpp +++ b/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp @@ -1,4 +1,4 @@ -#include "../include/GlobalHotkeysJoystick.hpp" +#include "../../include/GlobalHotkeys/GlobalHotkeysJoystick.hpp" #include <string.h> #include <errno.h> #include <fcntl.h> @@ -11,9 +11,73 @@ namespace gsr { static constexpr int triangle_button = 2; static constexpr int options_button = 9; static constexpr int playstation_button = 10; + static constexpr int l3_button = 11; + static constexpr int r3_button = 12; static constexpr int axis_up_down = 7; static constexpr int axis_left_right = 6; + struct DeviceId { + uint16_t vendor; + uint16_t product; + }; + + static bool read_file_hex_number(const char *path, unsigned int *value) { + *value = 0; + FILE *f = fopen(path, "rb"); + if(!f) + return false; + + fscanf(f, "%x", value); + fclose(f); + return true; + } + + static DeviceId joystick_get_device_id(const char *path) { + DeviceId device_id; + device_id.vendor = 0; + device_id.product = 0; + + const char *js_path_id = nullptr; + const int len = strlen(path); + for(int i = len - 1; i >= 0; --i) { + if(path[i] == '/') { + js_path_id = path + i + 1; + break; + } + } + + if(!js_path_id) + return device_id; + + unsigned int vendor = 0; + unsigned int product = 0; + char path_buf[1024]; + + snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id); + if(!read_file_hex_number(path_buf, &vendor)) + return device_id; + + snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id); + if(!read_file_hex_number(path_buf, &product)) + return device_id; + + device_id.vendor = vendor; + device_id.product = product; + return device_id; + } + + static bool is_ps4_controller(DeviceId device_id) { + return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4); + } + + static bool is_ps5_controller(DeviceId device_id) { + return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6); + } + + static bool is_stadia_controller(DeviceId device_id) { + return device_id.vendor == 0x18D1 && (device_id.product == 0x9400); + } + // Returns -1 on error static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) { if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0) @@ -204,7 +268,8 @@ namespace gsr { if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) { switch(event.number) { case playstation_button: { - playstation_button_pressed = event.value == button_pressed; + // Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time + playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed; break; } case options_button: { @@ -222,6 +287,14 @@ namespace gsr { save_10_min_replay = true; break; } + case l3_button: { + l3_button_pressed = event.value == button_pressed; + break; + } + case r3_button: { + r3_button_pressed = event.value == button_pressed; + break; + } } } else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) { const int trigger_threshold = 16383; @@ -276,6 +349,8 @@ namespace gsr { dev_input_id }; + //const DeviceId device_id = joystick_get_device_id(dev_input_filepath); + ++num_poll_fd; fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath); return true; diff --git a/src/GlobalHotkeysLinux.cpp b/src/GlobalHotkeys/GlobalHotkeysLinux.cpp index d780916..a56bbc6 100644 --- a/src/GlobalHotkeysLinux.cpp +++ b/src/GlobalHotkeys/GlobalHotkeysLinux.cpp @@ -1,4 +1,4 @@ -#include "../include/GlobalHotkeysLinux.hpp" +#include "../../include/GlobalHotkeys/GlobalHotkeysLinux.hpp" #include <sys/wait.h> #include <fcntl.h> #include <limits.h> diff --git a/src/GlobalHotkeysX11.cpp b/src/GlobalHotkeys/GlobalHotkeysX11.cpp index 9af2607..bc79ce8 100644 --- a/src/GlobalHotkeysX11.cpp +++ b/src/GlobalHotkeys/GlobalHotkeysX11.cpp @@ -1,4 +1,4 @@ -#include "../include/GlobalHotkeysX11.hpp" +#include "../../include/GlobalHotkeys/GlobalHotkeysX11.hpp" #include <X11/keysym.h> #include <mglpp/window/Event.hpp> #include <assert.h> diff --git a/src/Overlay.cpp b/src/Overlay.cpp index 91f20db..c698cff 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -12,10 +12,10 @@ #include "../include/gui/Utils.hpp" #include "../include/gui/PageStack.hpp" #include "../include/WindowUtils.hpp" -#include "../include/GlobalHotkeys.hpp" -#include "../include/GlobalHotkeysLinux.hpp" -#include "../include/CursorTrackerX11.hpp" -#include "../include/CursorTrackerWayland.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeys.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeysLinux.hpp" +#include "../include/CursorTracker/CursorTrackerX11.hpp" +#include "../include/CursorTracker/CursorTrackerWayland.hpp" #include <string.h> #include <assert.h> @@ -26,6 +26,7 @@ #include <malloc.h> #include <stdexcept> #include <algorithm> +#include <inttypes.h> #include <X11/Xlib.h> #include <X11/Xutil.h> @@ -37,6 +38,7 @@ #include <X11/Xcursor/Xcursor.h> #include <mglpp/system/Rect.hpp> #include <mglpp/window/Event.hpp> +#include <mglpp/system/Utf8.hpp> extern "C" { #include <mgl/mgl.h> @@ -207,24 +209,20 @@ namespace gsr { return false; }*/ - // Returns the first monitor if not found. Assumes there is at least one monitor connected. static const Monitor* find_monitor_at_position(const std::vector<Monitor> &monitors, mgl::vec2i pos) { - assert(!monitors.empty()); for(const Monitor &monitor : monitors) { if(mgl::IntRect(monitor.position, monitor.size).contains(pos)) return &monitor; } - return &monitors.front(); + return nullptr; } - // Returns the first monitor if not found. Assumes there is at least one monitor connected. static const Monitor* find_monitor_by_name(const std::vector<Monitor> &monitors, const std::string &name) { - assert(!monitors.empty()); for(const Monitor &monitor : monitors) { if(monitor.name == name) return &monitor; } - return &monitors.front(); + return nullptr; } static std::string get_power_supply_online_filepath() { @@ -275,6 +273,33 @@ namespace gsr { return true; } + static bool is_hyprland_waybar_running_as_dock() { + const char *args[] = { "hyprctl", "layers", nullptr }; + std::string stdout_str; + if(exec_program_on_host_get_stdout(args, stdout_str) != 0) + return false; + + int waybar_layer_level = -1; + int current_layer_level = 0; + string_split_char(stdout_str, '\n', [&](const std::string_view line) { + if(line.find("Layer level 0") != std::string_view::npos) + current_layer_level = 0; + else if(line.find("Layer level 1") != std::string_view::npos) + current_layer_level = 1; + else if(line.find("Layer level 2") != std::string_view::npos) + current_layer_level = 2; + else if(line.find("Layer level 3") != std::string_view::npos) + current_layer_level = 3; + else if(line.find("namespace: waybar") != std::string_view::npos) { + waybar_layer_level = current_layer_level; + return false; + } + return true; + }); + + return waybar_layer_level >= 0 && waybar_layer_level <= 1; + } + static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) { return { (uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key), @@ -681,6 +706,22 @@ namespace gsr { on_region_selected = nullptr; } + window_selector.poll_events(); + if(window_selector.take_canceled()) { + on_window_selected = nullptr; + } else if(window_selector.take_selection() && on_window_selected) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + const Window selected_window = window_selector.get_selection(); + if(selected_window && selected_window != DefaultRootWindow(display)) { + on_window_selected(); + } else { + show_notification("No window selected", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } + on_window_selected = nullptr; + } + if(!visible || !window) return; @@ -717,6 +758,12 @@ namespace gsr { update_gsr_screenshot_process_status(); replay_status_update_status(); + if(hide_ui) { + hide_ui = false; + hide(); + return false; + } + if(start_region_capture) { start_region_capture = false; hide(); @@ -726,7 +773,16 @@ namespace gsr { } } - if(region_selector.is_started()) { + if(start_window_capture) { + start_window_capture = false; + hide(); + if(!window_selector.start(get_color_theme().tint_color)) { + show_notification("Failed to start window capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + on_window_selected = nullptr; + } + } + + if(region_selector.is_started() || window_selector.is_started()) { usleep(5 * 1000); // 5 ms return true; } @@ -860,7 +916,7 @@ namespace gsr { if(visible) return; - if(region_selector.is_started()) + if(region_selector.is_started() || window_selector.is_started()) return; drawn_first_frame = false; @@ -881,6 +937,8 @@ namespace gsr { const std::string wm_name = get_window_manager_name(display); const bool is_kwin = wm_name == "KWin"; const bool is_wlroots = wm_name.find("wlroots") != std::string::npos; + const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos; + const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock(); std::optional<CursorInfo> cursor_info; if(cursor_tracker) { @@ -894,16 +952,21 @@ namespace gsr { const Monitor *focused_monitor = nullptr; if(cursor_info) { focused_monitor = find_monitor_by_name(monitors, cursor_info->monitor_name); + if(!focused_monitor) + focused_monitor = &monitors.front(); cursor_position = cursor_info->position; } else { 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); focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value); + if(!focused_monitor) + focused_monitor = &monitors.front(); } // 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_wlroots; + // TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor)) + const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland; if(prevent_game_minimizing) { window_pos = focused_monitor->position; @@ -933,8 +996,11 @@ namespace gsr { // when a compositor isn't running. window_create_params.graphics_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_GRAPHICS_API_GLX : MGL_GRAPHICS_API_EGL; - if(!window->create("gsr ui", window_create_params)) + if(!window->create("gsr ui", window_create_params)) { fprintf(stderr, "error: failed to create window\n"); + window.reset(); + return; + } //window->set_low_latency(true); @@ -1004,7 +1070,7 @@ namespace gsr { // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events. xi_grab_all_mouse_devices(xi_display); - if(!is_wlroots) + if(!is_wlroots && !hyprland_waybar_is_dock) window->set_fullscreen(true); visible = true; @@ -1087,7 +1153,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); @@ -1121,7 +1187,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); @@ -1146,7 +1212,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); @@ -1278,6 +1344,8 @@ namespace gsr { if(!visible) return; + hide_ui = false; + mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; @@ -1308,6 +1376,7 @@ namespace gsr { visible = false; drawn_first_frame = false; start_region_capture = false; + start_window_capture = false; if(xi_input_xev) { free(xi_input_xev); @@ -1379,10 +1448,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); @@ -1428,6 +1499,27 @@ namespace gsr { return nullptr; } + static void truncate_string(std::string &str, int max_length) { + int index = 0; + size_t byte_index = 0; + + while(index < max_length && byte_index < str.size()) { + uint32_t codepoint = 0; + size_t codepoint_length = 0; + mgl::utf8_decode((const unsigned char*)str.c_str() + byte_index, str.size() - byte_index, &codepoint, &codepoint_length); + if(codepoint_length == 0) + codepoint_length = 1; + + index += 1; + byte_index += codepoint_length; + } + + if(byte_index < str.size()) { + str.erase(byte_index); + str += "..."; + } + } + static bool is_hex_num(char c) { return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); } @@ -1455,8 +1547,87 @@ namespace gsr { return is_hex && !hex_start; } + static bool is_number(const char *str) { + const char *p = str; + while(*p) { + char c = *p; + if(c < '0' || c > '9') + return false; + ++p; + } + return true; + } + static bool is_capture_target_monitor(const char *capture_target) { - return strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target); + return strcmp(capture_target, "window") != 0 && strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target); + } + + static std::string capture_target_get_notification_name(const char *capture_target) { + std::string result; + if(is_capture_target_monitor(capture_target)) { + result = "this monitor"; + } else if(is_number(capture_target)) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + int64_t window_id = None; + sscanf(capture_target, "%" PRIi64, &window_id); + + const std::optional<std::string> window_title = get_window_title(display, window_id); + if(window_title) { + result = strip(window_title.value()); + truncate_string(result, 20); + result = "window \"" + result + "\""; + } else { + result = std::string("window ") + capture_target; + } + } else { + result = capture_target; + } + return result; + } + + 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) { @@ -1477,20 +1648,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; @@ -1588,11 +1763,6 @@ namespace gsr { return result; } - static void truncate_string(std::string &str, int max_length) { - if((int)str.size() > max_length) - str.replace(str.begin() + max_length, str.end(), "..."); - } - void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) { mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; @@ -1622,11 +1792,7 @@ namespace gsr { if(!config.record_config.show_video_saved_notifications) return; - if(is_capture_target_monitor(recording_capture_target.c_str())) - snprintf(msg, sizeof(msg), "Saved a recording of this monitor to \"%s\"", focused_window_name.c_str()); - else - snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", recording_capture_target.c_str(), focused_window_name.c_str()); - + snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str()); capture_target = recording_capture_target.c_str(); break; } @@ -1640,11 +1806,7 @@ namespace gsr { else snprintf(duration, sizeof(duration), " "); - if(is_capture_target_monitor(recording_capture_target.c_str())) - snprintf(msg, sizeof(msg), "Saved a%sreplay of this monitor to \"%s\"", duration, focused_window_name.c_str()); - else - snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, recording_capture_target.c_str(), focused_window_name.c_str()); - + snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str()); capture_target = recording_capture_target.c_str(); break; } @@ -1652,11 +1814,7 @@ namespace gsr { if(!config.screenshot_config.show_screenshot_saved_notifications) return; - if(is_capture_target_monitor(screenshot_capture_target.c_str())) - snprintf(msg, sizeof(msg), "Saved a screenshot of this monitor to \"%s\"", focused_window_name.c_str()); - else - snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", screenshot_capture_target.c_str(), focused_window_name.c_str()); - + snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str(), focused_window_name.c_str()); capture_target = screenshot_capture_target.c_str(); break; } @@ -1681,7 +1839,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); @@ -1689,10 +1847,7 @@ namespace gsr { snprintf(duration, sizeof(duration), " "); char msg[512]; - if(is_capture_target_monitor(recording_capture_target.c_str())) - snprintf(msg, sizeof(msg), "Saved a%sreplay of this monitor", duration); - else - snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, recording_capture_target.c_str()); + snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, capture_target_get_notification_name(recording_capture_target.c_str()).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()); } } @@ -1741,6 +1896,35 @@ namespace gsr { } } + void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) { + fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); + if(exit_code == 50) { + show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } else if(exit_code == 60) { + show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } else { + const char *prefix = ""; + switch(notification_type) { + case NotificationType::NONE: + case NotificationType::SCREENSHOT: + break; + case NotificationType::RECORD: + prefix = "Failed to start/save recording"; + break; + case NotificationType::REPLAY: + prefix = "Replay stopped because of an error"; + break; + case NotificationType::STREAM: + prefix = "Streaming stopped because of an error"; + break; + } + + char msg[256]; + snprintf(msg, sizeof(msg), "%s. Verify if settings are correct", prefix); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } + } + void Overlay::update_gsr_process_status() { if(gpu_screen_recorder_process <= 0) return; @@ -1767,8 +1951,7 @@ namespace gsr { if(config.replay_config.show_replay_stopped_notifications) show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); } else { - fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Replay stopped because of an error. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); + on_gsr_process_error(exit_code, NotificationType::REPLAY); } break; } @@ -1783,8 +1966,7 @@ namespace gsr { if(config.streaming_config.show_streaming_stopped_notifications) show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); } else { - fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Streaming stopped because of an error. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + on_gsr_process_error(exit_code, NotificationType::STREAM); } break; } @@ -1811,12 +1993,9 @@ 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"); - else - snprintf(msg, sizeof(msg), "Saved a screenshot of %s", screenshot_capture_target.c_str()); + snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str()).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()); } } else { @@ -1913,17 +2092,13 @@ 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"); - else - snprintf(msg, sizeof(msg), "Saved a recording of %s", recording_capture_target.c_str()); + snprintf(msg, sizeof(msg), "Saved a recording of %s", capture_target_get_notification_name(recording_capture_target.c_str()).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()); } } else { - fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Failed to start/save recording. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + on_gsr_process_error(exit_code, NotificationType::RECORD); } update_ui_recording_stopped(); replay_recording = false; @@ -2047,6 +2222,8 @@ namespace gsr { for(const AudioTrack &audio_track : audio_tracks) { std::string audio_track_merged; + int num_app_audio = 0; + for(const std::string &audio_input_name : audio_track.audio_inputs) { std::string new_audio_input_name = audio_input_name; const bool is_app_audio = starts_with(new_audio_input_name, "app:"); @@ -2056,12 +2233,22 @@ namespace gsr { if(is_app_audio && audio_track.application_audio_invert) new_audio_input_name.replace(0, 4, "app-inverse:"); + if(is_app_audio) + ++num_app_audio; + if(!audio_track_merged.empty()) audio_track_merged += "|"; audio_track_merged += new_audio_input_name; } + if(num_app_audio == 0 && audio_track.application_audio_invert) { + if(!audio_track_merged.empty()) + audio_track_merged += "|"; + + audio_track_merged += "app-inverse:"; + } + if(!audio_track_merged.empty()) result.push_back(std::move(audio_track_merged)); } @@ -2111,11 +2298,12 @@ namespace gsr { } static bool validate_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) { - // TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number) - if(capture_target == "region") { - return capture_options.region; + if(capture_target == "window") { + return capture_options.window; } else if(capture_target == "focused") { return capture_options.focused; + } else if(capture_target == "region") { + return capture_options.region; } else if(capture_target == "portal") { return capture_options.portal; } else if(capture_target == "focused_monitor") { @@ -2129,16 +2317,45 @@ 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") { + if(capture_target == "window") { + return std::to_string(window_selector.get_selection()); + } else if(capture_target == "focused_monitor") { std::optional<CursorInfo> cursor_info; if(cursor_tracker) { cursor_tracker->update(); cursor_info = cursor_tracker->get_latest_cursor_info(); } - if(cursor_info) - return cursor_info->monitor_name; + 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; + 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()) return capture_options.monitors.front().name; else @@ -2189,8 +2406,50 @@ namespace gsr { kill(gpu_screen_recorder_process, SIGRTMIN+5); } - bool Overlay::on_press_start_replay(bool disable_notification, bool finished_region_selection) { - if(region_selector.is_started()) + static const char* switch_video_codec_to_usable_hardware_encoder(const GsrInfo &gsr_info) { + if(gsr_info.supported_video_codecs.h264) + return "h264"; + else if(gsr_info.supported_video_codecs.hevc) + return "hevc"; + else if(gsr_info.supported_video_codecs.av1) + return "av1"; + else if(gsr_info.supported_video_codecs.vp8) + return "vp8"; + else if(gsr_info.supported_video_codecs.vp9) + return "vp9"; + return nullptr; + } + + static const char* change_container_if_codec_not_supported(const char *video_codec, const char *container) { + if(strcmp(video_codec, "vp8") == 0 || strcmp(video_codec, "vp9") == 0) { + if(strcmp(container, "webm") != 0 && strcmp(container, "matroska") != 0) { + fprintf(stderr, "Warning: container '%s' is not compatible with video codec '%s', using webm container instead\n", container, video_codec); + return "webm"; + } + } else if(strcmp(container, "webm") == 0) { + fprintf(stderr, "Warning: container webm is not compatible with video codec '%s', using mp4 container instead\n", video_codec); + return "mp4"; + } + return container; + } + + static void choose_video_codec_and_container_with_fallback(const GsrInfo &gsr_info, const char **video_codec, const char **container, const char **encoder) { + *encoder = "gpu"; + if(strcmp(*video_codec, "h264_software") == 0) { + *video_codec = "h264"; + *encoder = "cpu"; + } else if(strcmp(*video_codec, "auto") == 0) { + *video_codec = switch_video_codec_to_usable_hardware_encoder(gsr_info); + if(!*video_codec) { + *video_codec = "h264"; + *encoder = "cpu"; + } + } + *container = change_container_if_codec_not_supported(*video_codec, *container); + } + + bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) return false; switch(recording_status) { @@ -2233,14 +2492,14 @@ namespace gsr { const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); recording_capture_target = get_capture_target(config.replay_config.record_options.record_area_option, capture_options); - if(!validate_capture_target(recording_capture_target, capture_options)) { + if(!validate_capture_target(config.replay_config.record_options.record_area_option, capture_options)) { char err_msg[256]; - snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid. Please change capture target in settings", recording_capture_target.c_str()); + snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); return false; } - if(config.replay_config.record_options.record_area_option == "region" && !finished_region_selection) { + if(config.replay_config.record_options.record_area_option == "region" && !finished_selection) { start_region_capture = true; on_region_selected = [disable_notification, this]() { on_press_start_replay(disable_notification, true); @@ -2248,6 +2507,14 @@ namespace gsr { return false; } + if(config.replay_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [disable_notification, this]() { + on_press_start_replay(disable_notification, true); + }; + return false; + } + // TODO: Validate input, fallback to valid values const std::string fps = std::to_string(config.replay_config.record_options.fps); const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate); @@ -2255,12 +2522,10 @@ namespace gsr { const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info); const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode; const std::string replay_time = std::to_string(config.replay_config.replay_time); + const char *container = config.replay_config.container.c_str(); const char *video_codec = config.replay_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); char size[64]; size[0] = '\0'; @@ -2272,7 +2537,7 @@ namespace gsr { std::vector<const char*> args = { "gpu-screen-recorder", "-w", recording_capture_target.c_str(), - "-c", config.replay_config.container.c_str(), + "-c", container, "-ac", config.replay_config.record_options.audio_codec.c_str(), "-cursor", config.replay_config.record_options.record_cursor ? "yes" : "no", "-cr", config.replay_config.record_options.color_range.c_str(), @@ -2290,6 +2555,11 @@ namespace gsr { args.push_back("yes"); } + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 5, 0}) { + args.push_back("-replay-storage"); + args.push_back(config.replay_config.replay_storage.c_str()); + } + char region_str[128]; add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector); @@ -2322,18 +2592,18 @@ namespace gsr { // to see when the program has exit. if(!disable_notification && config.replay_config.show_replay_started_notifications) { char msg[256]; - if(is_capture_target_monitor(recording_capture_target.c_str())) - snprintf(msg, sizeof(msg), "Started replaying this monitor"); - else - snprintf(msg, sizeof(msg), "Started replaying %s", recording_capture_target.c_str()); + snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str()); } + if(config.replay_config.record_options.record_area_option == "portal") + hide_ui = true; + return true; } - void Overlay::on_press_start_record(bool finished_region_selection) { - if(region_selector.is_started()) + void Overlay::on_press_start_record(bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) return; switch(recording_status) { @@ -2404,12 +2674,12 @@ namespace gsr { recording_capture_target = get_capture_target(config.record_config.record_options.record_area_option, capture_options); if(!validate_capture_target(config.record_config.record_options.record_area_option, capture_options)) { char err_msg[256]; - snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid. Please change capture target in settings", recording_capture_target.c_str()); + snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); return; } - if(config.record_config.record_options.record_area_option == "region" && !finished_region_selection) { + if(config.record_config.record_options.record_area_option == "region" && !finished_selection) { start_region_capture = true; on_region_selected = [this]() { on_press_start_record(true); @@ -2417,6 +2687,14 @@ namespace gsr { return; } + if(config.record_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this]() { + on_press_start_record(true); + }; + return; + } + record_filepath.clear(); // TODO: Validate input, fallback to valid values @@ -2425,12 +2703,10 @@ namespace gsr { const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str()); const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info); const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode; + const char *container = config.record_config.container.c_str(); const char *video_codec = config.record_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); char size[64]; size[0] = '\0'; @@ -2442,7 +2718,7 @@ namespace gsr { std::vector<const char*> args = { "gpu-screen-recorder", "-w", recording_capture_target.c_str(), - "-c", config.record_config.container.c_str(), + "-c", container, "-ac", config.record_config.record_options.audio_codec.c_str(), "-cursor", config.record_config.record_options.record_cursor ? "yes" : "no", "-cr", config.record_config.record_options.color_range.c_str(), @@ -2479,12 +2755,12 @@ namespace gsr { // 1... if(config.record_config.show_recording_started_notifications) { char msg[256]; - if(is_capture_target_monitor(recording_capture_target.c_str())) - snprintf(msg, sizeof(msg), "Started recording this monitor"); - else - snprintf(msg, sizeof(msg), "Started recording %s", recording_capture_target.c_str()); + snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str()); } + + if(config.record_config.record_options.record_area_option == "portal") + hide_ui = true; } static std::string streaming_get_url(const Config &config) { @@ -2495,6 +2771,9 @@ namespace gsr { } else if(config.streaming_config.streaming_service == "youtube") { url += "rtmp://a.rtmp.youtube.com/live2/"; url += config.streaming_config.youtube.stream_key; + } else if(config.streaming_config.streaming_service == "rumble") { + url += "rtmp://rtmp.rumble.com/live/"; + url += config.streaming_config.rumble.stream_key; } else if(config.streaming_config.streaming_service == "custom") { url = config.streaming_config.custom.url; if(url.size() >= 7 && strncmp(url.c_str(), "rtmp://", 7) == 0) @@ -2519,8 +2798,8 @@ namespace gsr { return url; } - void Overlay::on_press_start_stream(bool finished_region_selection) { - if(region_selector.is_started()) + void Overlay::on_press_start_stream(bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) return; switch(recording_status) { @@ -2561,12 +2840,12 @@ namespace gsr { recording_capture_target = get_capture_target(config.streaming_config.record_options.record_area_option, capture_options); if(!validate_capture_target(config.streaming_config.record_options.record_area_option, capture_options)) { char err_msg[256]; - snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid. Please change capture target in settings", recording_capture_target.c_str()); + snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); return; } - if(config.streaming_config.record_options.record_area_option == "region" && !finished_region_selection) { + if(config.streaming_config.record_options.record_area_option == "region" && !finished_selection) { start_region_capture = true; on_region_selected = [this]() { on_press_start_stream(true); @@ -2574,6 +2853,14 @@ namespace gsr { return; } + if(config.streaming_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this]() { + on_press_start_stream(true); + }; + return; + } + // TODO: Validate input, fallback to valid values const std::string fps = std::to_string(config.streaming_config.record_options.fps); const std::string video_bitrate = std::to_string(config.streaming_config.record_options.video_bitrate); @@ -2583,16 +2870,12 @@ namespace gsr { if(audio_tracks.size() > 1) audio_tracks.resize(1); const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode; + const char *container = "flv"; + if(config.streaming_config.streaming_service == "custom") + container = config.streaming_config.custom.container.c_str(); const char *video_codec = config.streaming_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } - - std::string container = "flv"; - if(config.streaming_config.streaming_service == "custom") - container = config.streaming_config.custom.container; + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); const std::string url = streaming_get_url(config); @@ -2606,7 +2889,7 @@ namespace gsr { std::vector<const char*> args = { "gpu-screen-recorder", "-w", recording_capture_target.c_str(), - "-c", container.c_str(), + "-c", container, "-ac", config.streaming_config.record_options.audio_codec.c_str(), "-cursor", config.streaming_config.record_options.record_cursor ? "yes" : "no", "-cr", config.streaming_config.record_options.color_range.c_str(), @@ -2649,16 +2932,16 @@ namespace gsr { // to see when the program has exit. if(config.streaming_config.show_streaming_started_notifications) { char msg[256]; - if(is_capture_target_monitor(recording_capture_target.c_str())) - snprintf(msg, sizeof(msg), "Started streaming this monitor"); - else - snprintf(msg, sizeof(msg), "Started streaming %s", recording_capture_target.c_str()); + snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str()); } + + if(config.streaming_config.record_options.record_area_option == "portal") + hide_ui = true; } - void Overlay::on_press_take_screenshot(bool finished_region_selection, bool force_region_capture) { - if(region_selector.is_started()) + void Overlay::on_press_take_screenshot(bool finished_selection, bool force_region_capture) { + if(region_selector.is_started() || window_selector.is_started()) return; if(gpu_screen_recorder_screenshot_process > 0) { @@ -2672,12 +2955,12 @@ namespace gsr { screenshot_capture_target = get_capture_target(record_area_option, capture_options); if(!validate_capture_target(record_area_option, capture_options)) { char err_msg[256]; - snprintf(err_msg, sizeof(err_msg), "Failed to take a screenshot, capture target \"%s\" is invalid. Please change capture target in settings", screenshot_capture_target.c_str()); + snprintf(err_msg, sizeof(err_msg), "Failed to take a screenshot, capture target \"%s\" is invalid.\nPlease change capture target in settings", screenshot_capture_target.c_str()); show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT); return; } - if(region_capture && !finished_region_selection) { + if(region_capture && !finished_selection) { start_region_capture = true; on_region_selected = [this, force_region_capture]() { usleep(200 * 1000); // Hack: wait 0.2 seconds before taking a screenshot to allow user to move cursor away. TODO: Remove this @@ -2686,6 +2969,14 @@ namespace gsr { return; } + if(config.screenshot_config.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this, force_region_capture]() { + on_press_take_screenshot(true, force_region_capture); + }; + return; + } + // TODO: Validate input, fallback to valid values const std::string output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format @@ -2721,6 +3012,9 @@ namespace gsr { if(gpu_screen_recorder_screenshot_process == -1) { show_notification("Failed to launch gpu-screen-recorder to take a screenshot", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT); } + + if(config.screenshot_config.record_area_option == "portal") + hide_ui = true; } bool Overlay::update_compositor_texture(const Monitor &monitor) { diff --git a/src/Process.cpp b/src/Process.cpp index 0a62986..c02753a 100644 --- a/src/Process.cpp +++ b/src/Process.cpp @@ -130,8 +130,6 @@ namespace gsr { exit_status = -1; break; } - - buffer[bytes_read] = '\0'; result.append(buffer, bytes_read); } @@ -178,11 +176,21 @@ namespace gsr { } } + static const char *get_basename(const char *path, int size) { + for(int i = size - 1; i >= 0; --i) { + if(path[i] == '/') + return path + i + 1; + } + return path; + } + // |output_buffer| should be at least PATH_MAX in size bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) { output_buffer[0] = '\0'; + const char *arg0_start = NULL; const char *arg0_end = NULL; + int arg0_size = 0; int fd = open(filepath, O_RDONLY); if(fd == -1) return false; @@ -192,13 +200,16 @@ namespace gsr { if(bytes_read == -1) goto err; - arg0_end = (const char*)memchr(buffer, '\0', bytes_read); + arg0_start = buffer; + arg0_end = (const char*)memchr(arg0_start, '\0', bytes_read); if(!arg0_end) goto err; - if((arg0_end - buffer) + 1 <= output_buffer_size) { - memcpy(output_buffer, buffer, arg0_end - buffer); - output_buffer[arg0_end - buffer] = '\0'; + arg0_start = get_basename(arg0_start, arg0_end - arg0_start); + arg0_size = arg0_end - arg0_start; + if(arg0_size + 1 <= output_buffer_size) { + memcpy(output_buffer, arg0_start, arg0_size); + output_buffer[arg0_size] = '\0'; close(fd); return true; } diff --git a/src/RegionSelector.cpp b/src/RegionSelector.cpp index 5b7243b..89a0209 100644 --- a/src/RegionSelector.cpp +++ b/src/RegionSelector.cpp @@ -208,7 +208,7 @@ namespace gsr { window_attr.background_pixel = is_wayland ? 0 : border_color_x11; window_attr.border_pixel = 0; window_attr.override_redirect = true; - window_attr.event_mask = StructureNotifyMask | PointerMotionMask; + window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask; window_attr.colormap = region_window_colormap; Screen *screen = XDefaultScreenOfDisplay(dpy); @@ -366,10 +366,6 @@ namespace gsr { return true; } - bool RegionSelector::is_selected() const { - return selected; - } - bool RegionSelector::take_selection() { const bool result = selected; selected = false; diff --git a/src/Rpc.cpp b/src/Rpc.cpp index 3eec98d..803a4dc 100644 --- a/src/Rpc.cpp +++ b/src/Rpc.cpp @@ -32,7 +32,7 @@ namespace gsr { fclose(file); if(!fifo_filepath.empty()) - remove(fifo_filepath.c_str()); + unlink(fifo_filepath.c_str()); } bool Rpc::create(const char *name) { @@ -44,15 +44,16 @@ namespace gsr { char fifo_filepath_tmp[PATH_MAX]; get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name); fifo_filepath = fifo_filepath_tmp; - remove(fifo_filepath.c_str()); + unlink(fifo_filepath.c_str()); if(mkfifo(fifo_filepath.c_str(), 0600) != 0) { fprintf(stderr, "Error: mkfifo failed, error: %s, %s\n", strerror(errno), fifo_filepath.c_str()); + fifo_filepath.clear(); return false; } if(!open_filepath(fifo_filepath.c_str())) { - remove(fifo_filepath.c_str()); + unlink(fifo_filepath.c_str()); fifo_filepath.clear(); return false; } diff --git a/src/Theme.cpp b/src/Theme.cpp index 6c384e3..2bef3c8 100644 --- a/src/Theme.cpp +++ b/src/Theme.cpp @@ -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})) diff --git a/src/Utils.cpp b/src/Utils.cpp index bc7b1f2..c36a64a 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -27,6 +27,33 @@ namespace gsr { return str.size() >= len && memcmp(str.data(), substr, len) == 0; } + bool ends_with(std::string_view str, const char *substr) { + size_t len = strlen(substr); + return str.size() >= len && memcmp(str.data() + str.size() - len, substr, len) == 0; + } + + std::string strip(const std::string &str) { + int start_index = 0; + int str_len = str.size(); + + for(int i = 0; i < str_len; ++i) { + if(str[i] != ' ') { + start_index += i; + str_len -= i; + break; + } + } + + for(int i = str_len - 1; i >= 0; --i) { + if(str[i] != ' ') { + str_len = i + 1; + break; + } + } + + return str.substr(start_index, str_len); + } + std::string get_home_dir() { const char *home_dir = getenv("HOME"); if(!home_dir) { diff --git a/src/WindowSelector.cpp b/src/WindowSelector.cpp new file mode 100644 index 0000000..f04d600 --- /dev/null +++ b/src/WindowSelector.cpp @@ -0,0 +1,229 @@ +#include "../include/WindowSelector.hpp" +#include "../include/WindowUtils.hpp" + +#include <stdio.h> +#include <string.h> + +#include <X11/extensions/shape.h> +#include <X11/cursorfont.h> +#include <X11/keysym.h> + +namespace gsr { + static const int rectangle_border_size = 2; + + static int max_int(int a, int b) { + return a >= b ? a : b; + } + + static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) { + if(width < 0) { + x += width; + width = abs(width); + } + + if(height < 0) { + y += height; + height = abs(height); + } + + XRectangle rectangles[] = { + { + (short)max_int(0, x), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Left + { + (short)max_int(0, x + width - border_size), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Right + { + (short)max_int(0, x + border_size), (short)max_int(0, y), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Top + { + (short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Bottom + }; + XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted); + XFlush(dpy); + } + + static unsigned long mgl_color_to_x11_color(mgl::Color color) { + if(color.a == 0) + return 0; + return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF); + } + + static Window get_cursor_window(Display *dpy) { + Window root_window = None; + Window window = None; + int dummy_i; + unsigned int dummy_u; + mgl::vec2i root_pos; + XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u); + return window; + } + + static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) { + Window root_window; + int x = 0; + int y = 0; + unsigned int w = 0; + unsigned int h = 0; + unsigned int dummy_border, dummy_depth; + XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth); + pos.x = x; + pos.y = y; + size.x = w; + size.y = h; + } + + WindowSelector::WindowSelector() { + + } + + WindowSelector::~WindowSelector() { + stop(); + } + + bool WindowSelector::start(mgl::Color border_color) { + if(dpy) + return false; + + const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color); + dpy = XOpenDisplay(nullptr); + if(!dpy) { + fprintf(stderr, "Error: WindowSelector::start: failed to connect to the X11 server\n"); + return false; + } + + const Window cursor_window = get_cursor_window(dpy); + mgl::vec2i cursor_window_pos, cursor_window_size; + get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size); + + XVisualInfo vinfo; + memset(&vinfo, 0, sizeof(vinfo)); + XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo); + border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone); + + XSetWindowAttributes window_attr; + window_attr.background_pixel = border_color_x11; + window_attr.border_pixel = 0; + window_attr.override_redirect = true; + window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask; + window_attr.colormap = border_window_colormap; + + Screen *screen = XDefaultScreenOfDisplay(dpy); + border_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0, + vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr); + if(!border_window) { + fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n"); + stop(); + return false; + } + set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen)); + if(cursor_window && cursor_window != DefaultRootWindow(dpy)) + set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size); + else + set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0); + make_window_click_through(dpy, border_window); + XMapWindow(dpy, border_window); + + crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair); + XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime); + XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime); + XFlush(dpy); + + selected = false; + canceled = false; + selected_window = None; + return true; + } + + void WindowSelector::stop() { + if(!dpy) + return; + + XUngrabPointer(dpy, CurrentTime); + XUngrabKeyboard(dpy, CurrentTime); + + if(border_window_colormap) { + XFreeColormap(dpy, border_window_colormap); + border_window_colormap = 0; + } + + if(border_window) { + XDestroyWindow(dpy, border_window); + border_window = 0; + } + + if(crosshair_cursor) { + XFreeCursor(dpy, crosshair_cursor); + crosshair_cursor = None; + } + + XFlush(dpy); + XCloseDisplay(dpy); + dpy = nullptr; + } + + bool WindowSelector::is_started() const { + return dpy != nullptr; + } + + bool WindowSelector::failed() const { + return !dpy; + } + + bool WindowSelector::poll_events() { + if(!dpy || selected) + return false; + + XEvent xev; + while(XPending(dpy)) { + XNextEvent(dpy, &xev); + + if(xev.type == MotionNotify) { + const Window motion_window = xev.xmotion.subwindow; + mgl::vec2i motion_window_pos, motion_window_size; + get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size); + if(motion_window && motion_window != DefaultRootWindow(dpy)) + set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size); + else + set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0); + XFlush(dpy); + } else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) { + selected_window = xev.xbutton.subwindow; + const Window clicked_window_real = window_get_target_window_child(dpy, selected_window); + if(clicked_window_real) + selected_window = clicked_window_real; + selected = true; + + stop(); + break; + } else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) { + canceled = true; + selected = false; + stop(); + break; + } + } + return true; + } + + bool WindowSelector::take_selection() { + const bool result = selected; + selected = false; + return result; + } + + bool WindowSelector::take_canceled() { + const bool result = canceled; + canceled = false; + return result; + } + + Window WindowSelector::get_selection() const { + return selected_window; + } +}
\ No newline at end of file diff --git a/src/WindowUtils.cpp b/src/WindowUtils.cpp index eb6080f..c6b278b 100644 --- a/src/WindowUtils.cpp +++ b/src/WindowUtils.cpp @@ -1,4 +1,5 @@ #include "../include/WindowUtils.hpp" +#include "../include/Utils.hpp" #include <X11/Xatom.h> #include <X11/Xutil.h> @@ -62,7 +63,7 @@ namespace gsr { return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom); } - static Window window_get_target_window_child(Display *display, Window window) { + Window window_get_target_window_child(Display *display, Window window) { if(window == None) return None; @@ -212,28 +213,6 @@ namespace gsr { return result; } - static std::string strip(const std::string &str) { - int start_index = 0; - int str_len = str.size(); - - for(int i = 0; i < str_len; ++i) { - if(str[i] != ' ') { - start_index += i; - str_len -= i; - break; - } - } - - for(int i = str_len - 1; i >= 0; --i) { - if(str[i] != ' ') { - str_len = i + 1; - break; - } - } - - return str.substr(start_index, str_len); - } - std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) { std::string result; const Window focused_window = get_focused_window(dpy, window_capture_type); diff --git a/src/gui/ComboBox.cpp b/src/gui/ComboBox.cpp index dbe9aa0..4287a53 100644 --- a/src/gui/ComboBox.cpp +++ b/src/gui/ComboBox.cpp @@ -85,7 +85,7 @@ namespace gsr { void ComboBox::add_item(const std::string &text, const std::string &id) { items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}}); - items.back().text.set_max_width(font->get_character_size() * 22); // TODO: Make a proper solution + items.back().text.set_max_width(font->get_character_size() * 20); // TODO: Make a proper solution //items.back().text.set_max_rows(1); dirty = true; } diff --git a/src/gui/GlobalSettingsPage.cpp b/src/gui/GlobalSettingsPage.cpp index ccebb92..6650c69 100644 --- a/src/gui/GlobalSettingsPage.cpp +++ b/src/gui/GlobalSettingsPage.cpp @@ -1,7 +1,6 @@ #include "../../include/gui/GlobalSettingsPage.hpp" #include "../../include/Overlay.hpp" -#include "../../include/GlobalHotkeys.hpp" #include "../../include/Theme.hpp" #include "../../include/Process.hpp" #include "../../include/gui/GsrPage.hpp" @@ -420,10 +419,10 @@ namespace gsr { list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_options_texture, get_theme().body_font.get_character_size(), "to show/hide the UI")); list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_up_texture, get_theme().body_font.get_character_size(), "to take a screenshot")); list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_down_texture, get_theme().body_font.get_character_size(), "to save a replay")); - list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_cross_texture, get_theme().body_font.get_character_size(), "to save a 1 minute replay")); - list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_triangle_texture, get_theme().body_font.get_character_size(), "to save a 10 minute replay")); list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_left_texture, get_theme().body_font.get_character_size(), "to start/stop recording")); list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_right_texture, get_theme().body_font.get_character_size(), "to turn replay on/off")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_cross_texture, get_theme().body_font.get_character_size(), "to save a 1 minute replay")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_triangle_texture, get_theme().body_font.get_character_size(), "to save a 10 minute replay")); return subsection; } diff --git a/src/gui/RadioButton.cpp b/src/gui/RadioButton.cpp index a6ef96a..bbb958a 100644 --- a/src/gui/RadioButton.cpp +++ b/src/gui/RadioButton.cpp @@ -169,7 +169,7 @@ namespace gsr { } } - const std::string RadioButton::get_selected_id() const { + const std::string& RadioButton::get_selected_id() const { if(items.empty()) { static std::string dummy; return dummy; @@ -177,4 +177,13 @@ namespace gsr { return items[selected_item].id; } } + + const std::string& RadioButton::get_selected_text() const { + if(items.empty()) { + static std::string dummy; + return dummy; + } else { + return items[selected_item].text.get_string(); + } + } }
\ No newline at end of file diff --git a/src/gui/ScreenshotSettingsPage.cpp b/src/gui/ScreenshotSettingsPage.cpp index 5b8efbd..c3560ad 100644 --- a/src/gui/ScreenshotSettingsPage.cpp +++ b/src/gui/ScreenshotSettingsPage.cpp @@ -35,9 +35,8 @@ namespace gsr { std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_record_area_box() { auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font); // TODO: Show options not supported but disable them - // TODO: Enable this - //if(capture_options.window) - // record_area_box->add_item("Window", "window"); + if(capture_options.window) + record_area_box->add_item("Window", "window"); if(capture_options.region) record_area_box->add_item("Region", "region"); if(!capture_options.monitors.empty()) @@ -60,14 +59,6 @@ namespace gsr { return record_area_list; } - std::unique_ptr<List> ScreenshotSettingsPage::create_select_window() { - auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL); - select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color)); - select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120))); - select_window_list_ptr = select_window_list.get(); - return select_window_list; - } - std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_width_entry() { auto image_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3); image_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15); @@ -124,13 +115,12 @@ namespace gsr { auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); capture_target_list->add_widget(create_record_area()); - capture_target_list->add_widget(create_select_window()); capture_target_list->add_widget(create_image_resolution_section()); capture_target_list->add_widget(create_restore_portal_session_section()); ll->add_widget(std::move(capture_target_list)); ll->add_widget(create_change_image_resolution_section()); - return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + 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> ScreenshotSettingsPage::create_image_quality_section() { @@ -258,9 +248,7 @@ namespace gsr { content_page_ptr->add_widget(create_settings()); record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { - const bool window_selected = id == "window"; const bool portal_selected = id == "portal"; - select_window_list_ptr->set_visible(window_selected); image_resolution_list_ptr->set_visible(change_image_resolution_checkbox_ptr->is_checked()); restore_portal_session_list_ptr->set_visible(portal_selected); return true; diff --git a/src/gui/SettingsPage.cpp b/src/gui/SettingsPage.cpp index 9890d17..405ba28 100644 --- a/src/gui/SettingsPage.cpp +++ b/src/gui/SettingsPage.cpp @@ -65,13 +65,12 @@ namespace gsr { std::unique_ptr<ComboBox> SettingsPage::create_record_area_box() { auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font); // TODO: Show options not supported but disable them - // TODO: Enable this - //if(capture_options.window) - // record_area_box->add_item("Window", "window"); - if(capture_options.region) - record_area_box->add_item("Region", "region"); + if(capture_options.window) + record_area_box->add_item("Window", "window"); if(capture_options.focused) record_area_box->add_item("Follow focused window", "focused"); + if(capture_options.region) + record_area_box->add_item("Region", "region"); if(!capture_options.monitors.empty()) record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor"); for(const auto &monitor : capture_options.monitors) { @@ -92,14 +91,6 @@ namespace gsr { return record_area_list; } - std::unique_ptr<List> SettingsPage::create_select_window() { - auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL); - select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color)); - select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120))); - select_window_list_ptr = select_window_list.get(); - return select_window_list; - } - std::unique_ptr<Entry> SettingsPage::create_area_width_entry() { auto area_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3); area_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15); @@ -186,20 +177,29 @@ namespace gsr { auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); capture_target_list->add_widget(create_record_area()); - capture_target_list->add_widget(create_select_window()); capture_target_list->add_widget(create_area_size_section()); capture_target_list->add_widget(create_video_resolution_section()); capture_target_list->add_widget(create_restore_portal_session_section()); ll->add_widget(std::move(capture_target_list)); ll->add_widget(create_change_video_resolution_section()); - return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + return std::make_unique<Subsection>("Capture", 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"); } - std::unique_ptr<ComboBox> SettingsPage::create_audio_device_selection_combobox() { + std::unique_ptr<ComboBox> SettingsPage::create_audio_device_selection_combobox(AudioDeviceType device_type) { auto audio_device_box = std::make_unique<ComboBox>(&get_theme().body_font); for(const auto &audio_device : audio_devices) { - audio_device_box->add_item(audio_device.description, audio_device.name); + const bool device_is_output = audio_device_is_output(audio_device.name); + if((device_type == AudioDeviceType::OUTPUT && device_is_output) || (device_type == AudioDeviceType::INPUT && !device_is_output)) { + std::string description = audio_device.description; + if(starts_with(description, "Monitor of ")) + description.erase(0, 11); + audio_device_box->add_item(description, audio_device.name); + } } return audio_device_box; } @@ -211,7 +211,7 @@ namespace gsr { List *audio_track_items_list = dynamic_cast<List*>(audio_track_subsection->get_inner_widget()); List *buttons_list = dynamic_cast<List*>(audio_track_items_list->get_child_widget_by_index(1)); - Button *add_application_audio_button = dynamic_cast<Button*>(buttons_list->get_child_widget_by_index(1)); + Button *add_application_audio_button = dynamic_cast<Button*>(buttons_list->get_child_widget_by_index(2)); add_application_audio_button->set_visible(visible); CheckBox *invert_app_audio_checkbox = dynamic_cast<CheckBox*>(audio_track_items_list->get_child_widget_by_index(3)); @@ -236,11 +236,11 @@ namespace gsr { return remove_audio_track_button; } - std::unique_ptr<List> SettingsPage::create_audio_device(List *audio_input_list_ptr) { + std::unique_ptr<List> SettingsPage::create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr) { auto audio_device_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); audio_device_list->userdata = (void*)(uintptr_t)AudioTrackType::DEVICE; - audio_device_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Device:", get_color_theme().text_color)); - audio_device_list->add_widget(create_audio_device_selection_combobox()); + audio_device_list->add_widget(std::make_unique<Label>(&get_theme().body_font, device_type == AudioDeviceType::OUTPUT ? "Output device:" : "Input device: ", get_color_theme().text_color)); + audio_device_list->add_widget(create_audio_device_selection_combobox(device_type)); audio_device_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, audio_device_list.get())); return audio_device_list; } @@ -254,13 +254,22 @@ namespace gsr { return button; } - std::unique_ptr<Button> SettingsPage::create_add_audio_device_button(List *audio_input_list_ptr) { - auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add audio device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - add_audio_track_button->on_click = [this, audio_input_list_ptr]() { + std::unique_ptr<Button> SettingsPage::create_add_audio_output_device_button(List *audio_input_list_ptr) { + auto button = std::make_unique<Button>(&get_theme().body_font, "Add output device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + button->on_click = [this, audio_input_list_ptr]() { audio_devices = get_audio_devices(); - audio_input_list_ptr->add_widget(create_audio_device(audio_input_list_ptr)); + audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::OUTPUT, audio_input_list_ptr)); }; - return add_audio_track_button; + return button; + } + + std::unique_ptr<Button> SettingsPage::create_add_audio_input_device_button(List *audio_input_list_ptr) { + auto button = std::make_unique<Button>(&get_theme().body_font, "Add input device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + button->on_click = [this, audio_input_list_ptr]() { + audio_devices = get_audio_devices(); + audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::INPUT, audio_input_list_ptr)); + }; + return button; } std::unique_ptr<ComboBox> SettingsPage::create_application_audio_selection_combobox(List *application_audio_row) { @@ -285,7 +294,7 @@ namespace gsr { std::unique_ptr<List> SettingsPage::create_application_audio(List *audio_input_list_ptr) { auto application_audio_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); application_audio_list->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION; - application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "App: ", get_color_theme().text_color)); + application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Application: ", get_color_theme().text_color)); application_audio_list->add_widget(create_application_audio_selection_combobox(application_audio_list.get())); application_audio_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, application_audio_list.get())); return application_audio_list; @@ -294,7 +303,7 @@ namespace gsr { std::unique_ptr<List> SettingsPage::create_custom_application_audio(List *audio_input_list_ptr) { auto application_audio_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); application_audio_list->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION_CUSTOM; - application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "App: ", get_color_theme().text_color)); + application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Application: ", get_color_theme().text_color)); application_audio_list->add_widget(std::make_unique<Entry>(&get_theme().body_font, "", (int)(get_theme().body_font.get_character_size() * 10.0f))); application_audio_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, application_audio_list.get())); return application_audio_list; @@ -314,7 +323,8 @@ namespace gsr { std::unique_ptr<List> SettingsPage::create_add_audio_buttons(List *audio_input_list_ptr) { auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); - list->add_widget(create_add_audio_device_button(audio_input_list_ptr)); + list->add_widget(create_add_audio_output_device_button(audio_input_list_ptr)); + list->add_widget(create_add_audio_input_device_button(audio_input_list_ptr)); list->add_widget(create_add_application_audio_button(audio_input_list_ptr)); return list; } @@ -431,13 +441,13 @@ namespace gsr { std::unique_ptr<List> SettingsPage::create_video_bitrate_entry() { auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); - auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "15000", (int)(get_theme().body_font.get_character_size() * 4.0f)); + auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "8000", (int)(get_theme().body_font.get_character_size() * 4.0f)); video_bitrate_entry->validate_handler = create_entry_validator_integer_in_range(1, 500000); video_bitrate_entry_ptr = video_bitrate_entry.get(); list->add_widget(std::move(video_bitrate_entry)); if(type == Type::STREAM) { - auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "1.64MB", get_color_theme().text_color); + auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "", get_color_theme().text_color); Label *size_mb_label_ptr = size_mb_label.get(); list->add_widget(std::move(size_mb_label)); @@ -614,10 +624,8 @@ namespace gsr { content_page_ptr->add_widget(create_settings()); record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { - const bool window_selected = id == "window"; const bool focused_selected = id == "focused"; const bool portal_selected = id == "portal"; - select_window_list_ptr->set_visible(window_selected); area_size_list_ptr->set_visible(focused_selected); video_resolution_list_ptr->set_visible(!focused_selected && change_video_resolution_checkbox_ptr->is_checked()); change_video_resolution_checkbox_ptr->set_visible(!focused_selected); @@ -715,7 +723,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(1, 10800); + 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)); @@ -733,6 +741,24 @@ namespace gsr { return replay_time_list; } + std::unique_ptr<List> SettingsPage::create_replay_storage() { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Where should temporary replay data be stored?", get_color_theme().text_color)); + auto replay_storage_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + replay_storage_button_ptr = replay_storage_button.get(); + replay_storage_button->add_item("RAM", "ram"); + replay_storage_button->add_item("Disk (Not recommended on SSDs)", "disk"); + + replay_storage_button->on_selection_changed = [this](const std::string&, const std::string &id) { + update_estimated_replay_file_size(id); + return true; + }; + + list->add_widget(std::move(replay_storage_button)); + list->set_visible(gsr_info->system_info.gsr_version >= GsrVersion{5, 5, 0}); + return list; + } + std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() { char fullscreen_text[256]; snprintf(fullscreen_text, sizeof(fullscreen_text), "Turn on replay when starting a fullscreen application%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)"); @@ -766,13 +792,13 @@ namespace gsr { return label; } - void SettingsPage::update_estimated_replay_file_size() { + void SettingsPage::update_estimated_replay_file_size(const std::string &replay_storage_type) { const int64_t replay_time_seconds = atoi(replay_time_entry_ptr->get_text().c_str()); const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL; const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024; char buffer[256]; - snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB.\nChange video bitrate or replay duration to change file size.", video_filesize_mb); + snprintf(buffer, sizeof(buffer), "Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.", replay_storage_type == "ram" ? "in RAM" : "on disk", video_filesize_mb); estimated_file_size_ptr->set_text(buffer); } @@ -811,12 +837,14 @@ namespace gsr { settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); auto general_list = std::make_unique<List>(List::Orientation::VERTICAL); - general_list->add_widget(create_start_replay_automatically()); + general_list->add_widget(create_replay_storage()); general_list->add_widget(create_save_replay_in_game_folder()); if(gsr_info->system_info.gsr_version >= GsrVersion{5, 0, 3}) general_list->add_widget(create_restart_replay_on_save()); settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); + settings_list_ptr->add_widget(std::make_unique<Subsection>("Autostart", create_start_replay_automatically(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); + auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL); auto show_replay_started_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show replay started notification"); @@ -845,12 +873,12 @@ namespace gsr { view_radio_button_ptr->on_selection_changed("Simple", "simple"); replay_time_entry_ptr->on_changed = [this](const std::string&) { - update_estimated_replay_file_size(); + update_estimated_replay_file_size(replay_storage_button_ptr->get_selected_id()); update_replay_time_text(); }; video_bitrate_entry_ptr->on_changed = [this](const std::string&) { - update_estimated_replay_file_size(); + update_estimated_replay_file_size(replay_storage_button_ptr->get_selected_id()); }; } @@ -900,6 +928,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)); @@ -919,6 +952,7 @@ namespace gsr { auto streaming_service_box = std::make_unique<ComboBox>(&get_theme().body_font); streaming_service_box->add_item("Twitch", "twitch"); streaming_service_box->add_item("YouTube", "youtube"); + streaming_service_box->add_item("Rumble", "rumble"); streaming_service_box->add_item("Custom", "custom"); streaming_service_box_ptr = streaming_service_box.get(); return streaming_service_box; @@ -943,6 +977,10 @@ namespace gsr { youtube_stream_key_entry_ptr = youtube_stream_key_entry.get(); stream_key_list->add_widget(std::move(youtube_stream_key_entry)); + auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20); + rumble_stream_key_entry_ptr = rumble_stream_key_entry.get(); + stream_key_list->add_widget(std::move(rumble_stream_key_entry)); + stream_key_list_ptr = stream_key_list.get(); return stream_key_list; } @@ -1004,12 +1042,14 @@ namespace gsr { streaming_service_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { const bool twitch_option = id == "twitch"; const bool youtube_option = id == "youtube"; + const bool rumble_option = id == "rumble"; const bool custom_option = id == "custom"; stream_key_list_ptr->set_visible(!custom_option); stream_url_list_ptr->set_visible(custom_option); container_list_ptr->set_visible(custom_option); twitch_stream_key_entry_ptr->set_visible(twitch_option); youtube_stream_key_entry_ptr->set_visible(youtube_option); + rumble_stream_key_entry_ptr->set_visible(rumble_option); return true; }; streaming_service_box_ptr->on_selection_changed("Twitch", "twitch"); @@ -1095,12 +1135,15 @@ namespace gsr { audio_input_list_ptr->add_widget(std::move(application_audio_widget)); } } else if(starts_with(audio_input, "device:")) { - std::unique_ptr<List> audio_track_widget = create_audio_device(audio_input_list_ptr); + const std::string device_name = audio_input.substr(7); + const AudioDeviceType audio_device_type = audio_device_is_output(device_name) ? AudioDeviceType::OUTPUT : AudioDeviceType::INPUT; + std::unique_ptr<List> audio_track_widget = create_audio_device(audio_device_type, audio_input_list_ptr); ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); - audio_device_box->set_selected_item(audio_input.substr(7)); + audio_device_box->set_selected_item(device_name); audio_input_list_ptr->add_widget(std::move(audio_track_widget)); } else { - std::unique_ptr<List> audio_track_widget = create_audio_device(audio_input_list_ptr); + const AudioDeviceType audio_device_type = audio_device_is_output(audio_input) ? AudioDeviceType::OUTPUT : AudioDeviceType::INPUT; + std::unique_ptr<List> audio_track_widget = create_audio_device(audio_device_type, audio_input_list_ptr); ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); audio_device_box->set_selected_item(audio_input); audio_input_list_ptr->add_widget(std::move(audio_track_widget)); @@ -1173,6 +1216,7 @@ namespace gsr { void SettingsPage::load_replay() { load_common(config.replay_config.record_options); + replay_storage_button_ptr->set_selected_item(config.replay_config.replay_storage); turn_on_replay_automatically_mode_ptr->set_selected_item(config.replay_config.turn_on_replay_automatically_mode); save_replay_in_game_folder_ptr->set_checked(config.replay_config.save_video_in_game_folder); if(restart_replay_on_save) @@ -1185,8 +1229,8 @@ namespace gsr { if(config.replay_config.replay_time < 2) config.replay_config.replay_time = 2; - if(config.replay_config.replay_time > 10800) - config.replay_config.replay_time = 10800; + if(config.replay_config.replay_time > 86400) + config.replay_config.replay_time = 86400; replay_time_entry_ptr->set_text(std::to_string(config.replay_config.replay_time)); } @@ -1195,6 +1239,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); } @@ -1206,6 +1251,7 @@ namespace gsr { streaming_service_box_ptr->set_selected_item(config.streaming_config.streaming_service); youtube_stream_key_entry_ptr->set_text(config.streaming_config.youtube.stream_key); twitch_stream_key_entry_ptr->set_text(config.streaming_config.twitch.stream_key); + rumble_stream_key_entry_ptr->set_text(config.streaming_config.rumble.stream_key); stream_url_entry_ptr->set_text(config.streaming_config.custom.url); container_box_ptr->set_selected_item(config.streaming_config.custom.container); } @@ -1322,6 +1368,7 @@ namespace gsr { config.replay_config.save_directory = save_directory_button_ptr->get_text(); config.replay_config.container = container_box_ptr->get_selected_id(); config.replay_config.replay_time = atoi(replay_time_entry_ptr->get_text().c_str()); + config.replay_config.replay_storage = replay_storage_button_ptr->get_selected_id(); if(config.replay_config.replay_time < 5) { config.replay_config.replay_time = 5; @@ -1334,6 +1381,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(); } @@ -1345,6 +1393,7 @@ namespace gsr { config.streaming_config.streaming_service = streaming_service_box_ptr->get_selected_id(); config.streaming_config.youtube.stream_key = youtube_stream_key_entry_ptr->get_text(); config.streaming_config.twitch.stream_key = twitch_stream_key_entry_ptr->get_text(); + config.streaming_config.rumble.stream_key = rumble_stream_key_entry_ptr->get_text(); config.streaming_config.custom.url = stream_url_entry_ptr->get_text(); config.streaming_config.custom.container = container_box_ptr->get_selected_id(); } diff --git a/src/main.cpp b/src/main.cpp index 19a23c7..192e84e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,7 +4,6 @@ #include "../include/Process.hpp" #include "../include/Rpc.hpp" -#include <unistd.h> #include <signal.h> #include <string.h> #include <limits.h> @@ -14,7 +13,6 @@ #include <mglpp/system/Clock.hpp> // TODO: Make keyboard/controller controllable for steam deck (and other controllers). -// TODO: Keep track of gpu screen recorder run by other programs to not allow recording at the same time, or something. // TODO: Add systray by using org.kde.StatusNotifierWatcher/etc dbus directly. // TODO: Make sure the overlay always stays on top. Test with starting the overlay and then opening youtube in fullscreen. // This is done in Overlay::force_window_on_top, but it's not called right now. It cant be used because the overlay will be on top of @@ -159,6 +157,21 @@ static bool is_flatpak() { return getenv("FLATPAK_ID") != nullptr; } +static void set_display_server_environment_variables() { + // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) + const char *display = getenv("DISPLAY"); + if(!display) { + display = ":0"; + setenv("DISPLAY", display, true); + } + + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if(!wayland_display) { + wayland_display = "wayland-1"; + setenv("WAYLAND_DISPLAY", wayland_display, true); + } +} + static void usage() { printf("usage: gsr-ui [action]\n"); printf("OPTIONS:\n"); @@ -203,30 +216,19 @@ int main(int argc, char **argv) { usage(); } - // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) - const char *display = getenv("DISPLAY"); - if(!display) { - display = ":0"; - setenv("DISPLAY", display, true); - } + set_display_server_environment_variables(); - const char *wayland_display = getenv("WAYLAND_DISPLAY"); - if(!wayland_display) { - wayland_display = "wayland-1"; - setenv("WAYLAND_DISPLAY", wayland_display, true); - } + auto rpc = std::make_unique<gsr::Rpc>(); + const bool rpc_created = rpc->create("gsr-ui"); + if(!rpc_created) + fprintf(stderr, "Error: Failed to create rpc\n"); - // TODO: This is a shitty method to detect if multiple instances of gsr-ui is running but this will work properly even in flatpak - // that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform. - // TODO: This method doesn't work when disabling hotkeys and the method below with pidof gsr-ui doesn't work in flatpak. - // What do? creating a pid file doesn't work in flatpak either. - // TODO: This doesn't work in flatpak when disabling hotkeys. - if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) { + if(is_gsr_ui_virtual_keyboard_running() || !rpc_created) { if(launch_action == LaunchAction::LAUNCH_DAEMON) return 1; - gsr::Rpc rpc; - if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) { + rpc = std::make_unique<gsr::Rpc>(); + if(rpc->open("gsr-ui") && rpc->write("show_ui\n", 8)) { fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n"); } else { fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n"); @@ -236,6 +238,16 @@ int main(int argc, char **argv) { return 1; } + if(gsr::pidof("gpu-screen-recorder", -1) != -1) { + const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr }; + gsr::exec_program_daemonized(args); + } + + if(mgl_init(MGL_WINDOW_SYSTEM_X11) != 0) { + fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n"); + return 1; + } + if(is_flatpak()) install_flatpak_systemd_service(); else @@ -279,11 +291,6 @@ int main(int argc, char **argv) { disable_prime_run(); } - if(mgl_init(MGL_WINDOW_SYSTEM_X11) != 0) { - fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n"); - exit(1); - } - gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info); std::string resources_path; @@ -316,10 +323,6 @@ int main(int argc, char **argv) { if(launch_action == LaunchAction::LAUNCH_SHOW) overlay->show(); - auto rpc = std::make_unique<gsr::Rpc>(); - if(!rpc->create("gsr-ui")) - fprintf(stderr, "Error: Failed to create rpc, commands won't be received\n"); - rpc_add_commands(rpc.get(), overlay.get()); // TODO: Add hotkeys in Overlay when using x11 global hotkeys. The hotkeys in Overlay should duplicate each key that is used for x11 global hotkeys. |