diff options
Diffstat (limited to 'src/Overlay.cpp')
-rw-r--r-- | src/Overlay.cpp | 1184 |
1 files changed, 967 insertions, 217 deletions
diff --git a/src/Overlay.cpp b/src/Overlay.cpp index 2475a77..919117d 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -7,20 +7,29 @@ #include "../include/gui/DropdownButton.hpp" #include "../include/gui/CustomRendererWidget.hpp" #include "../include/gui/SettingsPage.hpp" +#include "../include/gui/GlobalSettingsPage.hpp" #include "../include/gui/Utils.hpp" #include "../include/gui/PageStack.hpp" +#include "../include/gui/GsrPage.hpp" +#include "../include/WindowUtils.hpp" +#include "../include/GlobalHotkeys.hpp" #include <string.h> #include <assert.h> #include <sys/wait.h> #include <limits.h> #include <fcntl.h> +#include <poll.h> #include <stdexcept> #include <X11/Xlib.h> #include <X11/Xutil.h> #include <X11/Xatom.h> #include <X11/cursorfont.h> +#include <X11/extensions/Xfixes.h> +#include <X11/extensions/XInput2.h> +#include <X11/extensions/shape.h> +#include <X11/Xcursor/Xcursor.h> #include <mglpp/system/Rect.hpp> #include <mglpp/window/Event.hpp> @@ -32,65 +41,7 @@ namespace gsr { static const mgl::Color bg_color(0, 0, 0, 100); static const double force_window_on_top_timeout_seconds = 1.0; static const double replay_status_update_check_timeout_seconds = 1.0; - - static bool window_has_atom(Display *dpy, Window window, Atom atom) { - Atom type; - unsigned long len, bytes_left; - int format; - unsigned char *properties = NULL; - if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) - return false; - - if(properties) - XFree(properties); - - return type != None; - } - - static bool window_is_user_program(Display *dpy, Window window) { - const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); - const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False); - return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom); - } - - static Window get_window_at_cursor_position(Display *dpy) { - Window root_window = None; - Window window = None; - int dummy_i; - unsigned int dummy_u; - int cursor_pos_x = 0; - int cursor_pos_y = 0; - XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u); - return window; - } - - static Window get_focused_window(Display *dpy) { - const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); - Window focused_window = None; - - // Atom type = None; - // int format = 0; - // unsigned long num_items = 0; - // unsigned long bytes_left = 0; - // unsigned char *data = NULL; - // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data); - - // fprintf(stderr, "focused window: %p\n", (void*)data); - - // if(type == XA_WINDOW && num_items == 1 && data) - // return *(Window*)data; - - int revert_to = 0; - XGetInputFocus(dpy, &focused_window, &revert_to); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) - return focused_window; - - focused_window = get_window_at_cursor_position(dpy); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) - return focused_window; - - return None; - } + static const double replay_saving_notification_timeout_seconds = 0.5; static mgl::Texture texture_from_ximage(XImage *img) { uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3); @@ -117,6 +68,53 @@ namespace gsr { return texture; } + static bool texture_from_x11_cursor(XcursorImage *x11_cursor_image, bool *visible, mgl::vec2i *hotspot, mgl::Texture &texture) { + uint8_t *cursor_data = NULL; + uint8_t *out = NULL; + const unsigned int *pixels = NULL; + *visible = false; + + if(!x11_cursor_image) + return false; + + if(!x11_cursor_image->pixels) + return false; + + hotspot->x = x11_cursor_image->xhot; + hotspot->y = x11_cursor_image->yhot; + + pixels = x11_cursor_image->pixels; + cursor_data = (uint8_t*)malloc((int)x11_cursor_image->width * (int)x11_cursor_image->height * 4); + if(!cursor_data) + return false; + + out = cursor_data; + /* Un-premultiply alpha */ + for(uint32_t y = 0; y < x11_cursor_image->height; ++y) { + for(uint32_t x = 0; x < x11_cursor_image->width; ++x) { + uint32_t pixel = *pixels++; + uint8_t *in = (uint8_t*)&pixel; + uint8_t alpha = in[3]; + if(alpha == 0) { + alpha = 1; + } else { + *visible = true; + } + + out[0] = (float)in[2] * 255.0/(float)alpha; + out[1] = (float)in[1] * 255.0/(float)alpha; + out[2] = (float)in[0] * 255.0/(float)alpha; + out[3] = in[3]; + out += 4; + in += 4; + } + } + + texture.load_from_memory(cursor_data, x11_cursor_image->width, x11_cursor_image->height, MGL_IMAGE_FORMAT_RGBA); + free(cursor_data); + return true; + } + static char hex_value_to_str(uint8_t v) { if(v <= 9) return '0' + v; @@ -167,7 +165,7 @@ namespace gsr { return std::abs(a - b) <= difference; } - static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitor) { + static bool is_window_fullscreen_on_monitor(Display *display, Window window, const Monitor &monitor) { if(!window) return false; @@ -176,8 +174,8 @@ namespace gsr { return false; const int margin = 2; - return diff_int(geometry.x, monitor->pos.x, margin) && diff_int(geometry.y, monitor->pos.y, margin) - && diff_int(geometry.width, monitor->size.x, margin) && diff_int(geometry.height, monitor->size.y, margin); + return diff_int(geometry.x, monitor.position.x, margin) && diff_int(geometry.y, monitor.position.y, margin) + && diff_int(geometry.width, monitor.size.x, margin) && diff_int(geometry.height, monitor.size.y, margin); } /*static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitors, int num_monitors) { @@ -231,15 +229,6 @@ namespace gsr { return is_fullscreen; } - static void set_focused_window(Display *dpy, Window window) { - XSetInputFocus(dpy, window, RevertToPointerRoot, CurrentTime); - - const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); - XChangeProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, XA_WINDOW, 32, PropModeReplace, (const unsigned char*)&window, 1); - - XFlush(dpy); - } - #define _NET_WM_STATE_REMOVE 0 #define _NET_WM_STATE_ADD 1 #define _NET_WM_STATE_TOGGLE 2 @@ -265,6 +254,14 @@ namespace gsr { return True; } + static void make_window_click_through(Display *display, Window window) { + XRectangle rect; + memset(&rect, 0, sizeof(rect)); + XserverRegion region = XFixesCreateRegion(display, &rect, 1); + XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region); + XFixesDestroyRegion(display, region); + } + static Bool make_window_sticky(Display *dpy, Window window) { return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False)); } @@ -274,22 +271,13 @@ namespace gsr { } // Returns the first monitor if not found. Assumes there is at least one monitor connected. - static const mgl_monitor* find_monitor_by_cursor_position(mgl::Window &window) { - const mgl_window *win = window.internal_window(); - assert(win->num_monitors > 0); - for(int i = 0; i < win->num_monitors; ++i) { - const mgl_monitor *mon = &win->monitors[i]; - if(mgl::IntRect({ mon->pos.x, mon->pos.y }, { mon->size.x, mon->size.y }).contains({ win->cursor_position.x, win->cursor_position.y })) - return mon; + 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 &win->monitors[0]; - } - - static bool is_compositor_running(Display *dpy, int screen) { - char prop_name[20]; - snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen); - Atom prop_atom = XInternAtom(dpy, prop_name, False); - return XGetSelectionOwner(dpy, prop_atom) != None; + return &monitors.front(); } static std::string get_power_supply_online_filepath() { @@ -320,17 +308,108 @@ namespace gsr { return is_connected; } - Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, egl_functions egl_funcs) : + static bool xinput_is_supported(Display *dpy, int *xi_opcode) { + *xi_opcode = 0; + int query_event = 0; + int query_error = 0; + if(!XQueryExtension(dpy, "XInputExtension", xi_opcode, &query_event, &query_error)) { + fprintf(stderr, "gsr-ui error: X Input extension not available\n"); + return false; + } + + int major = 2; + int minor = 1; + int retval = XIQueryVersion(dpy, &major, &minor); + if (retval != Success) { + fprintf(stderr, "gsr-ui error: XInput 2.1 is not supported\n"); + return false; + } + + return true; + } + + static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) { + return { + (uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key), + config_hotkey.modifiers + }; + } + + static void bind_linux_hotkeys(GlobalHotkeysLinux *global_hotkeys, Overlay *overlay) { + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().main_config.show_hide_hotkey), + "show_hide", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_show(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_hotkey), + "record", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_record(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().record_config.pause_unpause_hotkey), + "pause", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_pause(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().streaming_config.start_stop_hotkey), + "stream", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_stream(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.start_stop_hotkey), + "replay_start", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_replay(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.save_hotkey), + "replay_save", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + } + + static std::unique_ptr<GlobalHotkeysLinux> register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) { + auto global_hotkeys = std::make_unique<GlobalHotkeysLinux>(grab_type); + if(!global_hotkeys->start()) + fprintf(stderr, "error: failed to start global hotkeys\n"); + + bind_linux_hotkeys(global_hotkeys.get(), overlay); + return global_hotkeys; + } + + static std::unique_ptr<GlobalHotkeysJoystick> register_joystick_hotkeys(Overlay *overlay) { + auto global_hotkeys_js = std::make_unique<GlobalHotkeysJoystick>(); + if(!global_hotkeys_js->start()) + fprintf(stderr, "Warning: failed to start joystick hotkeys\n"); + + global_hotkeys_js->bind_action("save_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + + return global_hotkeys_js; + } + + Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) : resources_path(std::move(resources_path)), - gsr_info(gsr_info), + gsr_info(std::move(gsr_info)), egl_funcs(egl_funcs), + config(capture_options), bg_screenshot_overlay({0.0f, 0.0f}), top_bar_background({0.0f, 0.0f}), - close_button_widget({0.0f, 0.0f}), - config(gsr_info) + close_button_widget({0.0f, 0.0f}) { - memset(&window_texture, 0, sizeof(window_texture)); - key_bindings[0].key_event.code = mgl::Keyboard::Escape; key_bindings[0].key_event.alt = false; key_bindings[0].key_event.control = false; @@ -340,19 +419,32 @@ namespace gsr { page_stack.pop(); }; - std::optional<Config> new_config = read_config(gsr_info); + memset(&window_texture, 0, sizeof(window_texture)); + + std::optional<Config> new_config = read_config(capture_options); if(new_config) config = std::move(new_config.value()); - init_color_theme(gsr_info); - // These environment variable are used by files in scripts/ folder - const std::string notify_bg_color_str = color_to_hex_str(get_color_theme().tint_color); - setenv("GSR_NOTIFY_BG_COLOR", notify_bg_color_str.c_str(), true); + init_color_theme(config, this->gsr_info); power_supply_online_filepath = get_power_supply_online_filepath(); if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup") on_press_start_replay(true); + + if(config.main_config.hotkeys_enable_option == "enable_hotkeys") + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); + else if(config.main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices") + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL); + + if(config.main_config.joystick_hotkeys_enable_option == "enable_hotkeys") + global_hotkeys_js = register_joystick_hotkeys(this); + + x11_mapping_display = XOpenDisplay(nullptr); + if(x11_mapping_display) + XKeysymToKeycode(x11_mapping_display, XK_F1); // If we dont call we will never get a MappingNotify + else + fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n"); } Overlay::~Overlay() { @@ -377,6 +469,134 @@ namespace gsr { } gpu_screen_recorder_process = -1; } + + close_gpu_screen_recorder_output(); + deinit_color_theme(); + + if(x11_mapping_display) + XCloseDisplay(x11_mapping_display); + } + + void Overlay::xi_setup() { + xi_display = XOpenDisplay(nullptr); + if(!xi_display) { + fprintf(stderr, "gsr-ui error: failed to setup XI connection\n"); + return; + } + + if(!xinput_is_supported(xi_display, &xi_opcode)) + goto error; + + xi_input_xev = (XEvent*)calloc(1, sizeof(XEvent)); + if(!xi_input_xev) + throw std::runtime_error("gsr-ui error: failed to allocate XEvent data"); + + xi_output_xev = (XEvent*)calloc(1, sizeof(XEvent)); + if(!xi_output_xev) + throw std::runtime_error("gsr-ui error: failed to allocate XEvent data"); + + unsigned char mask[XIMaskLen(XI_LASTEVENT)]; + memset(mask, 0, sizeof(mask)); + XISetMask(mask, XI_Motion); + //XISetMask(mask, XI_RawMotion); + XISetMask(mask, XI_ButtonPress); + XISetMask(mask, XI_ButtonRelease); + XISetMask(mask, XI_KeyPress); + XISetMask(mask, XI_KeyRelease); + + XIEventMask xi_masks; + xi_masks.deviceid = XIAllMasterDevices; + xi_masks.mask_len = sizeof(mask); + xi_masks.mask = mask; + if(XISelectEvents(xi_display, DefaultRootWindow(xi_display), &xi_masks, 1) != Success) { + fprintf(stderr, "gsr-ui error: XISelectEvents failed\n"); + goto error; + } + + XFlush(xi_display); + return; + + error: + free(xi_input_xev); + xi_input_xev = nullptr; + free(xi_output_xev); + xi_output_xev = nullptr; + if(xi_display) { + XCloseDisplay(xi_display); + xi_display = nullptr; + } + } + + void Overlay::close_gpu_screen_recorder_output() { + if(gpu_screen_recorder_process_output_file) { + fclose(gpu_screen_recorder_process_output_file); + gpu_screen_recorder_process_output_file = nullptr; + } + + if(gpu_screen_recorder_process_output_fd > 0) { + close(gpu_screen_recorder_process_output_fd); + gpu_screen_recorder_process_output_fd = -1; + } + } + + void Overlay::handle_xi_events() { + if(!xi_display) + return; + + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + while(XPending(xi_display)) { + XNextEvent(xi_display, xi_input_xev); + XGenericEventCookie *cookie = &xi_input_xev->xcookie; + if(cookie->type == GenericEvent && cookie->extension == xi_opcode && XGetEventData(xi_display, cookie)) { + const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data; + if(cookie->evtype == XI_Motion) { + memset(xi_output_xev, 0, sizeof(*xi_output_xev)); + xi_output_xev->type = MotionNotify; + xi_output_xev->xmotion.display = display; + xi_output_xev->xmotion.window = window->get_system_handle(); + xi_output_xev->xmotion.subwindow = window->get_system_handle(); + xi_output_xev->xmotion.x = de->root_x - window_pos.x; + xi_output_xev->xmotion.y = de->root_y - window_pos.y; + xi_output_xev->xmotion.x_root = de->root_x; + xi_output_xev->xmotion.y_root = de->root_y; + //xi_output_xev->xmotion.state = // modifiers // TODO: + if(window->inject_x11_event(xi_output_xev, event)) + on_event(event); + } else if(cookie->evtype == XI_ButtonPress || cookie->evtype == XI_ButtonRelease) { + memset(xi_output_xev, 0, sizeof(*xi_output_xev)); + xi_output_xev->type = cookie->evtype == XI_ButtonPress ? ButtonPress : ButtonRelease; + xi_output_xev->xbutton.display = display; + xi_output_xev->xbutton.window = window->get_system_handle(); + xi_output_xev->xbutton.subwindow = window->get_system_handle(); + xi_output_xev->xbutton.x = de->root_x - window_pos.x; + xi_output_xev->xbutton.y = de->root_y - window_pos.y; + xi_output_xev->xbutton.x_root = de->root_x; + xi_output_xev->xbutton.y_root = de->root_y; + //xi_output_xev->xbutton.state = // modifiers // TODO: + xi_output_xev->xbutton.button = de->detail; + if(window->inject_x11_event(xi_output_xev, event)) + on_event(event); + } else if(cookie->evtype == XI_KeyPress || cookie->evtype == XI_KeyRelease) { + memset(xi_output_xev, 0, sizeof(*xi_output_xev)); + xi_output_xev->type = cookie->evtype == XI_KeyPress ? KeyPress : KeyRelease; + xi_output_xev->xkey.display = display; + xi_output_xev->xkey.window = window->get_system_handle(); + xi_output_xev->xkey.subwindow = window->get_system_handle(); + xi_output_xev->xkey.x = de->root_x - window_pos.x; + xi_output_xev->xkey.y = de->root_y - window_pos.y; + xi_output_xev->xkey.x_root = de->root_x; + xi_output_xev->xkey.y_root = de->root_y; + xi_output_xev->xkey.state = de->mods.effective; + xi_output_xev->xkey.keycode = de->detail; + if(window->inject_x11_event(xi_output_xev, event)) + on_event(event); + } + //fprintf(stderr, "got xi event: %d\n", cookie->evtype); + XFreeEventData(xi_display, cookie); + } + } } static uint32_t key_event_to_bitmask(mgl::Event::KeyEvent key_event) { @@ -397,11 +617,42 @@ namespace gsr { } } + void Overlay::handle_keyboard_mapping_event() { + if(!x11_mapping_display) + return; + + bool mapping_updated = false; + while(XPending(x11_mapping_display)) { + XNextEvent(x11_mapping_display, &x11_mapping_xev); + if(x11_mapping_xev.type == MappingNotify) { + XRefreshKeyboardMapping(&x11_mapping_xev.xmapping); + mapping_updated = true; + } + } + + if(mapping_updated) + rebind_all_keyboard_hotkeys(); + } + void Overlay::handle_events() { + if(global_hotkeys) + global_hotkeys->poll_events(); + + if(global_hotkeys_js) + global_hotkeys_js->poll_events(); + + handle_keyboard_mapping_event(); + if(!visible || !window) return; + handle_xi_events(); + while(window->poll_event(event)) { + if(global_hotkeys) { + if(!global_hotkeys->on_event(event)) + continue; + } on_event(event); } } @@ -410,7 +661,9 @@ namespace gsr { if(!visible || !window) return; - close_button_widget.on_event(event, *window, mgl::vec2f(0.0f, 0.0f)); + if(!close_button_widget.on_event(event, *window, mgl::vec2f(0.0f, 0.0f))) + return; + if(!page_stack.on_event(event, *window, mgl::vec2f(0.0f, 0.0f))) return; @@ -419,6 +672,7 @@ namespace gsr { bool Overlay::draw() { update_notification_process_status(); + update_gsr_replay_save(); update_gsr_process_status(); replay_status_update_status(); @@ -433,55 +687,229 @@ namespace gsr { if(!window) return false; + grab_mouse_and_keyboard(); + //force_window_on_top(); - window->clear(bg_color); + const bool draw_ui = show_overlay_clock.get_elapsed_time_seconds() >= show_overlay_timeout_seconds; - if(window_texture_sprite.get_texture() && window_texture.texture_id) { - window->draw(window_texture_sprite); - window->draw(bg_screenshot_overlay); - } else if(screenshot_texture.is_valid()) { - window->draw(screenshot_sprite); - window->draw(bg_screenshot_overlay); - } + window->clear(draw_ui ? bg_color : mgl::Color(0, 0, 0, 0)); + + if(draw_ui) { + if(window_texture_sprite.get_texture() && window_texture.texture_id) { + window->draw(window_texture_sprite); + window->draw(bg_screenshot_overlay); + } else if(screenshot_texture.is_valid()) { + window->draw(screenshot_sprite); + window->draw(bg_screenshot_overlay); + } + + window->draw(top_bar_background); + window->draw(top_bar_text); + window->draw(logo_sprite); - window->draw(top_bar_background); - window->draw(top_bar_text); - window->draw(logo_sprite); + close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f)); + page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f)); - close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f)); - page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f)); + if(cursor_texture.is_valid()) { + cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f()); + window->draw(cursor_sprite); + } + + if(!drawn_first_frame) { + drawn_first_frame = true; + mgl::Event event; + event.type = mgl::Event::MouseMoved; + event.mouse_move.x = window->get_mouse_position().x; + event.mouse_move.y = window->get_mouse_position().y; + on_event(event); + } + } window->display(); return true; } + void Overlay::grab_mouse_and_keyboard() { + // TODO: Remove these grabs when debugging with a debugger, or your X11 session will appear frozen. + // There should be a debug mode to not use these + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + XGrabPointer(display, window->get_system_handle(), True, + ButtonPressMask | ButtonReleaseMask | PointerMotionMask | + Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask | + ButtonMotionMask, + GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime); + // TODO: This breaks global hotkeys (when using x11 global hotkeys) + XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime); + XFlush(display); + } + + void Overlay::xi_setup_fake_cursor() { + if(!xi_display) + return; + + XFixesHideCursor(xi_display, DefaultRootWindow(xi_display)); + XFlush(xi_display); + + // TODO: XCURSOR_SIZE and XCURSOR_THEME environment variables + const char *cursor_theme = XcursorGetTheme(xi_display); + if(!cursor_theme) { + //fprintf(stderr, "Warning: failed to get cursor theme, using \"default\" theme instead\n"); + cursor_theme = "default"; + } + + int cursor_size = XcursorGetDefaultSize(xi_display); + if(cursor_size <= 1) + cursor_size = 24; + + XcursorImage *cursor_image = nullptr; + for(int cursor_size_test : {cursor_size, 24}) { + for(const char *cursor_theme_test : {cursor_theme, "default", "Adwaita"}) { + for(unsigned int shape : {XC_left_ptr, XC_arrow}) { + cursor_image = XcursorShapeLoadImage(shape, cursor_theme_test, cursor_size_test); + if(cursor_image) + goto done; + } + } + } + + done: + if(!cursor_image) { + fprintf(stderr, "Error: failed to get cursor, loading bundled default cursor instead\n"); + const std::string default_cursor_path = resources_path + "images/default.cur"; + for(int cursor_size_test : {cursor_size, 24}) { + cursor_image = XcursorFilenameLoadImage(default_cursor_path.c_str(), cursor_size_test); + if(cursor_image) + break; + } + } + + if(!cursor_image) { + fprintf(stderr, "Error: failed to get cursor\n"); + XFixesShowCursor(xi_display, DefaultRootWindow(xi_display)); + XFlush(xi_display); + return; + } + + bool cursor_visible = false; + texture_from_x11_cursor(cursor_image, &cursor_visible, &cursor_hotspot, cursor_texture); + if(cursor_texture.is_valid()) + cursor_sprite.set_texture(&cursor_texture); + + XcursorImageDestroy(cursor_image); + } + + static bool device_is_mouse(const XIDeviceInfo *dev) { + for(int i = 0; i < dev->num_classes; ++i) { + if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer) + return true; + } + return false; + } + + void Overlay::xi_grab_all_mouse_devices() { + if(!xi_display) + return; + + int num_devices = 0; + XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices); + if(!info) + return; + + unsigned char mask[XIMaskLen(XI_LASTEVENT)]; + memset(mask, 0, sizeof(mask)); + XISetMask(mask, XI_Motion); + //XISetMask(mask, XI_RawMotion); + XISetMask(mask, XI_ButtonPress); + XISetMask(mask, XI_ButtonRelease); + XISetMask(mask, XI_KeyPress); + XISetMask(mask, XI_KeyRelease); + + for (int i = 0; i < num_devices; ++i) { + const XIDeviceInfo *dev = &info[i]; + if(!device_is_mouse(dev)) + continue; + + XIEventMask xi_masks; + xi_masks.deviceid = dev->deviceid; + xi_masks.mask_len = sizeof(mask); + xi_masks.mask = mask; + XIGrabDevice(xi_display, dev->deviceid, window->get_system_handle(), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, &xi_masks); + } + + XFlush(xi_display); + XIFreeDeviceInfo(info); + } + void Overlay::show() { + if(visible) + return; + + drawn_first_frame = false; window.reset(); window = std::make_unique<mgl::Window>(); deinit_theme(); - mgl::vec2i window_size = { 1280, 720 }; - mgl::vec2i window_pos = { 0, 0 }; + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + const std::vector<Monitor> monitors = get_monitors(display); + if(monitors.empty()) { + fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n"); + window.reset(); + return; + } + + 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; + + // The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it + Window x11_cursor_window = None; + const 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(monitors, monitor_position_query_value); + + // 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; + + if(prevent_game_minimizing) { + window_pos = focused_monitor->position; + window_size = focused_monitor->size; + } else { + window_pos = {0, 0}; + window_size = focused_monitor->size / 2; + } mgl::Window::CreateParams window_create_params; window_create_params.size = window_size; - window_create_params.min_size = window_size; - window_create_params.max_size = window_size; - window_create_params.position = window_pos; - window_create_params.hidden = true; - window_create_params.override_redirect = true; - window_create_params.background_color = bg_color; + if(prevent_game_minimizing) { + window_create_params.min_size = window_size; + window_create_params.max_size = window_size; + } + window_create_params.position = focused_monitor->position + focused_monitor->size / 2 - window_size / 2; + window_create_params.hidden = prevent_game_minimizing; + window_create_params.override_redirect = prevent_game_minimizing; + window_create_params.background_color = mgl::Color(0, 0, 0, 0); window_create_params.support_alpha = true; - window_create_params.window_type = MGL_WINDOW_TYPE_NOTIFICATION; - window_create_params.render_api = MGL_RENDER_API_EGL; + window_create_params.hide_decorations = true; + // MGL_WINDOW_TYPE_DIALOG is needed for kde plasma wayland in some cases, otherwise the window will pop up on another activity + // or may not be visible at all + window_create_params.window_type = (is_kwin && gsr_info.system_info.display_server == DisplayServer::WAYLAND) ? MGL_WINDOW_TYPE_DIALOG : MGL_WINDOW_TYPE_NORMAL; + // Nvidia + Wayland + Egl doesn't work on some systems properly and it instead falls back to software rendering. + // Use Glx on Wayland to workaround this issue. This is fine since Egl is only needed for x11 to reliably get the texture of the fullscreen window on Nvidia + // when a compositor isn't running. + window_create_params.render_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_RENDER_API_GLX : MGL_RENDER_API_EGL; if(!window->create("gsr ui", window_create_params)) fprintf(stderr, "error: failed to create window\n"); - mgl_context *context = mgl_get_context(); - Display *display = (Display*)context->connection; + //window->set_low_latency(true); unsigned char data = 2; // Prefer being composed to allow transparency XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); @@ -489,30 +917,27 @@ namespace gsr { data = 1; XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); + const auto original_window_size = window_size; + window_pos = focused_monitor->position; + window_size = focused_monitor->size; if(!init_theme(resources_path)) { fprintf(stderr, "Error: failed to load theme\n"); - exit(1); - } - - mgl_window *win = window->internal_window(); - if(win->num_monitors == 0) { - fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n"); + window.reset(); return; } - - const mgl_monitor *focused_monitor = find_monitor_by_cursor_position(*window); - window_pos = {focused_monitor->pos.x, focused_monitor->pos.y}; - window_size = {focused_monitor->size.x, focused_monitor->size.y}; get_theme().set_window_size(window_size); - window->set_size(window_size); - window->set_size_limits(window_size, window_size); - window->set_position(window_pos); + if(prevent_game_minimizing) { + window->set_size(window_size); + window->set_size_limits(window_size, window_size); + } + window->set_position(focused_monitor->position + focused_monitor->size / 2 - original_window_size / 2); - update_compositor_texture(focused_monitor); + mgl_window *win = window->internal_window(); + win->cursor_position.x = cursor_position.x - window_pos.x; + win->cursor_position.y = cursor_position.y - window_pos.y; - top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font); - logo_sprite = mgl::Sprite(&get_theme().logo_texture); + update_compositor_texture(*focused_monitor); bg_screenshot_overlay = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height)); top_bar_background = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height*0.06f).floor()); @@ -545,6 +970,7 @@ namespace gsr { const int button_width = button_height; auto main_buttons_list = std::make_unique<List>(List::Orientation::HORIZONTAL); + List * main_buttons_list_ptr = main_buttons_list.get(); main_buttons_list->set_spacing(0.0f); { auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Instant Replay", "Off", &get_theme().replay_button_texture, @@ -555,9 +981,14 @@ namespace gsr { button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); button->set_item_icon("save", &get_theme().save_texture); + button->set_item_icon("settings", &get_theme().settings_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); + auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack); + replay_settings_page->on_config_changed = [this]() { + if(recording_status == RecordingStatus::REPLAY) + show_notification("Replay settings have been modified.\nYou may need to restart replay to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + }; page_stack.push(std::move(replay_settings_page)); } else if(id == "save") { on_press_save_replay(); @@ -576,9 +1007,14 @@ 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->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); + auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack); + record_settings_page->on_config_changed = [this]() { + if(recording_status == RecordingStatus::RECORD) + show_notification("Recording settings have been modified.\nYou may need to restart recording to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + }; page_stack.push(std::move(record_settings_page)); } else if(id == "pause") { toggle_pause(); @@ -595,9 +1031,14 @@ namespace gsr { button->add_item("Start", "start", "Alt+F8"); 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->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); + auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack); + stream_settings_page->on_config_changed = [this]() { + if(recording_status == RecordingStatus::STREAM) + show_notification("Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + }; page_stack.push(std::move(stream_settings_page)); } else if(id == "start") { on_press_start_stream(); @@ -610,6 +1051,59 @@ namespace gsr { main_buttons_list->set_position((mgl::vec2f(window_size.x * 0.5f, window_size.y * 0.25f) - main_buttons_list_size * 0.5f).floor()); front_page_ptr->add_widget(std::move(main_buttons_list)); + { + const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size(); + const int settings_button_size = main_buttons_size.y * 0.2f; + auto button = std::make_unique<Button>(&get_theme().title_font, "", mgl::vec2f(settings_button_size, settings_button_size), mgl::Color(0, 0, 0, 180)); + button->set_position((main_buttons_list_ptr->get_position() + main_buttons_size - mgl::vec2f(0.0f, settings_button_size) + mgl::vec2f(settings_button_size * 0.333f, 0.0f)).floor()); + button->set_bg_hover_color(mgl::Color(0, 0, 0, 255)); + button->set_icon(&get_theme().settings_small_texture); + button->on_click = [&]() { + auto settings_page = std::make_unique<GlobalSettingsPage>(this, &gsr_info, config, &page_stack); + + settings_page->on_startup_changed = [&](bool enable, int exit_status) { + if(exit_status == 0) + return; + + if(exit_status == 127) { + if(enable) + show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } else { + if(enable) + show_notification("Failed to add GPU Screen Recorder to system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + else + show_notification("Failed to remove GPU Screen Recorder from system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } + }; + + settings_page->on_click_exit_program_button = [this](const char *reason) { + do_exit = true; + exit_reason = reason; + }; + + settings_page->on_keyboard_hotkey_changed = [this](const char *hotkey_option) { + global_hotkeys.reset(); + if(strcmp(hotkey_option, "enable_hotkeys") == 0) + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); + else if(strcmp(hotkey_option, "enable_hotkeys_virtual_devices") == 0) + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL); + else if(strcmp(hotkey_option, "disable_hotkeys") == 0) + global_hotkeys.reset(); + }; + + settings_page->on_joystick_hotkey_changed = [this](const char *hotkey_option) { + global_hotkeys_js.reset(); + if(strcmp(hotkey_option, "enable_hotkeys") == 0) + global_hotkeys_js = register_joystick_hotkeys(this); + else if(strcmp(hotkey_option, "disable_hotkeys") == 0) + global_hotkeys_js.reset(); + }; + + page_stack.push(std::move(settings_page)); + }; + front_page_ptr->add_widget(std::move(button)); + } + close_button_widget.draw_handler = [&](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) { const int border_size = std::max(1.0f, 0.0015f * get_theme().window_height); const float padding_size = std::max(1.0f, 0.003f * get_theme().window_height); @@ -637,8 +1131,18 @@ namespace gsr { return true; }; - window->set_fullscreen(true); + // The focused application can be an xwayland application but the cursor can hover over a wayland application. + // This is even the case when hovering over the titlebar of the xwayland application. + const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing; + if(fake_cursor) + xi_setup(); + + //window->set_fullscreen(true); + if(gsr_info.system_info.display_server == DisplayServer::X11) + make_window_click_through(display, window->get_system_handle()); + window->set_visible(true); + make_window_sticky(display, window->get_system_handle()); hide_window_from_taskbar(display, window->get_system_handle()); @@ -646,33 +1150,23 @@ namespace gsr { XFreeCursor(display, default_cursor); default_cursor = 0; } - default_cursor = XCreateFontCursor(display, XC_arrow); + default_cursor = XCreateFontCursor(display, XC_left_ptr); + XFlush(display); - // TODO: Retry if these fail. - // TODO: Hmm, these dont work in owlboy. Maybe owlboy uses xi2 and that breaks this (does it?). - // Remove these grabs when debugging with a debugger, or your X11 session will appear frozen + grab_mouse_and_keyboard(); - // XGrabPointer(display, window->get_system_handle(), True, - // ButtonPressMask | ButtonReleaseMask | PointerMotionMask | - // Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask | - // ButtonMotionMask, - // GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime); - // TODO: This breaks global hotkeys - //XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime); + // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed + xi_setup_fake_cursor(); - set_focused_window(display, window->get_system_handle()); - XFlush(display); + // We want to grab all devices to prevent any other application below the UI from receiving events. + // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events. + xi_grab_all_mouse_devices(); - //window->set_fullscreen(true); + if(!is_wlroots) + window->set_fullscreen(true); visible = true; - mgl::Event event; - event.type = mgl::Event::MouseMoved; - event.mouse_move.x = window->get_mouse_position().x; - event.mouse_move.y = window->get_mouse_position().y; - on_event(event); - if(gpu_screen_recorder_process > 0) { switch(recording_status) { case RecordingStatus::NONE: @@ -691,9 +1185,18 @@ namespace gsr { if(paused) update_ui_recording_paused(); + + // Wayland compositors have retarded fullscreen animations that we cant disable in a proper way + // without messing up window position. + show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15; + show_overlay_clock.restart(); + draw(); } void Overlay::hide() { + if(!visible) + return; + mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; @@ -710,13 +1213,57 @@ namespace gsr { XUngrabPointer(display, CurrentTime); XFlush(display); + if(xi_display) { + cursor_texture.clear(); + cursor_sprite.set_texture(nullptr); + } + window_texture_deinit(&window_texture); window_texture_sprite.set_texture(nullptr); screenshot_texture.clear(); screenshot_sprite.set_texture(nullptr); visible = false; + drawn_first_frame = false; + + if(xi_input_xev) { + free(xi_input_xev); + xi_input_xev = nullptr; + } + + if(xi_output_xev) { + free(xi_output_xev); + xi_output_xev = nullptr; + } + + if(xi_display) { + XCloseDisplay(xi_display); + xi_display = nullptr; + + if(window) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position(); + XWarpPointer(display, DefaultRootWindow(display), DefaultRootWindow(display), 0, 0, 0, 0, new_cursor_position.x, new_cursor_position.y); + XFlush(display); + + XFixesShowCursor(display, DefaultRootWindow(display)); + XFlush(display); + } + } + if(window) { + if(show_overlay_timeout_seconds > 0.0001) { + window->clear(mgl::Color(0, 0, 0, 0)); + window->display(); + + mgl_context *context = mgl_get_context(); + context->gl.glFlush(); + context->gl.glFinish(); + usleep(50 * 1000); // EGL doesn't do an immediate flush for some reason + } + window->set_visible(false); window.reset(); } @@ -725,10 +1272,16 @@ namespace gsr { } void Overlay::toggle_show() { - if(visible) - hide(); - else + if(visible) { + //hide(); + // We dont want to hide immediately because hide is called in mgl event callback, in which it destroys the mgl window. + // Instead remove all pages and wait until next iteration to close the UI (which happens when there are no pages to render). + while(!page_stack.empty()) { + page_stack.pop(); + } + } else { show(); + } } void Overlay::toggle_record() { @@ -787,7 +1340,7 @@ namespace gsr { const char *notification_type_str = notification_type_to_string(notification_type); if(notification_type_str) { notification_args[9] = "--icon"; - notification_args[10] = "record", + notification_args[10] = notification_type_str; notification_args[11] = nullptr; } else { notification_args[9] = nullptr; @@ -799,13 +1352,40 @@ namespace gsr { waitpid(notification_process, &status, 0); } - notification_process = exec_program(notification_args); + notification_process = exec_program(notification_args, NULL); } bool Overlay::is_open() const { return visible; } + bool Overlay::should_exit(std::string &reason) const { + reason.clear(); + if(do_exit) + reason = exit_reason; + return do_exit; + } + + void Overlay::exit() { + do_exit = true; + } + + const Config& Overlay::get_config() const { + return config; + } + + void Overlay::unbind_all_keyboard_hotkeys() { + if(global_hotkeys) + global_hotkeys->unbind_all_keys(); + } + + void Overlay::rebind_all_keyboard_hotkeys() { + unbind_all_keyboard_hotkeys(); + // TODO: Check if type is GlobalHotkeysLinux + if(global_hotkeys) + bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this); + } + void Overlay::update_notification_process_status() { if(notification_process <= 0) return; @@ -819,32 +1399,128 @@ namespace gsr { notification_process = -1; } + static void string_replace_characters(char *str, const char *characters_to_replace, char new_character) { + for(; *str != '\0'; ++str) { + for(const char *p = characters_to_replace; *p != '\0'; ++p) { + if(*str == *p) + *str = new_character; + } + } + } + + static std::string filepath_get_directory(const char *filepath) { + std::string result = filepath; + const size_t last_slash_index = result.rfind('/'); + if(last_slash_index == std::string::npos) + result = "."; + else + result.erase(last_slash_index); + return result; + } + + static std::string filepath_get_filename(const char *filepath) { + std::string result = filepath; + const size_t last_slash_index = result.rfind('/'); + if(last_slash_index != std::string::npos) + result.erase(0, last_slash_index + 1); + 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; + const std::string video_filename = filepath_get_filename(video_filepath); + + const Window gsr_ui_window = window ? window->get_system_handle() : None; + std::string focused_window_name = get_window_name_at_cursor_position(display, gsr_ui_window); + if(focused_window_name.empty()) + focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED); + if(focused_window_name.empty()) + focused_window_name = "Game"; + + string_replace_characters(focused_window_name.data(), "/\\", '_'); + + std::string video_directory = filepath_get_directory(video_filepath) + "/" + focused_window_name; + create_directory_recursive(video_directory.data()); + + const std::string new_video_filepath = video_directory + "/" + video_filename; + rename(video_filepath, new_video_filepath.c_str()); + + truncate_string(focused_window_name, 20); + std::string text; + switch(notification_type) { + case NotificationType::RECORD: { + if(!config.record_config.show_video_saved_notifications) + return; + text = "Saved recording to '" + focused_window_name + "/" + video_filename + "'"; + break; + } + case NotificationType::REPLAY: { + if(!config.replay_config.show_replay_saved_notifications) + return; + text = "Saved replay to '" + focused_window_name + "/" + video_filename + "'"; + break; + } + case NotificationType::NONE: + case NotificationType::STREAM: + break; + } + show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type); + } + + void Overlay::on_replay_saved(const char *replay_saved_filepath) { + replay_save_show_notification = false; + if(config.replay_config.save_video_in_game_folder) { + save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY); + } else { + const std::string text = "Saved replay to '" + filepath_get_filename(replay_saved_filepath) + "'"; + show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + } + } + + void Overlay::update_gsr_replay_save() { + if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) { + replay_save_show_notification = false; + show_notification("Saving replay, this might take some time", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + } + + if(gpu_screen_recorder_process_output_file) { + char buffer[1024]; + char *replay_saved_filepath = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file); + if(!replay_saved_filepath || replay_saved_filepath[0] == '\0') + return; + + const int line_len = strlen(replay_saved_filepath); + if(replay_saved_filepath[line_len - 1] == '\n') + replay_saved_filepath[line_len - 1] = '\0'; + + on_replay_saved(replay_saved_filepath); + } else if(gpu_screen_recorder_process_output_fd > 0) { + char buffer[1024]; + read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer)); + } + } + void Overlay::update_gsr_process_status() { if(gpu_screen_recorder_process <= 0) return; - errno = 0; int status; if(waitpid(gpu_screen_recorder_process, &status, WNOHANG) == 0) { // Still running return; } + close_gpu_screen_recorder_output(); + int exit_code = -1; - // The process is no longer a child process since gsr ui has restarted - if(errno == ECHILD) { - errno = 0; - kill(gpu_screen_recorder_process, 0); - if(errno != ESRCH) { - // Still running - return; - } - // We cant know the exit status, so we assume it succeeded - exit_code = 0; - } else { - if(WIFEXITED(status)) - exit_code = WEXITSTATUS(status); - } + if(WIFEXITED(status)) + exit_code = WEXITSTATUS(status); switch(recording_status) { case RecordingStatus::NONE: @@ -856,16 +1532,13 @@ namespace gsr { show_notification("Replay stopped", 3.0, 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", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); + show_notification("Replay stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); } break; } case RecordingStatus::RECORD: { update_ui_recording_stopped(); - if(exit_code != 0) { - 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", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); - } + on_stop_recording(exit_code); break; } case RecordingStatus::STREAM: { @@ -875,7 +1548,7 @@ namespace gsr { show_notification("Streaming has stopped", 3.0, 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", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + show_notification("Streaming stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); } break; } @@ -901,7 +1574,7 @@ namespace gsr { mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; - const Window focused_window = get_focused_window(display); + const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED); if(window && focused_window == window->get_system_handle()) return; @@ -915,6 +1588,7 @@ namespace gsr { } } + // TODO: Instead of checking power supply status periodically listen to power supply event void Overlay::update_power_supply_status() { if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_power_supply_connected") return; @@ -929,6 +1603,20 @@ namespace gsr { } } + void Overlay::on_stop_recording(int exit_code) { + if(exit_code == 0) { + if(config.record_config.save_video_in_game_folder) { + save_video_in_current_game_directory(record_filepath.c_str(), NotificationType::RECORD); + } else { + const std::string text = "Saved recording to '" + filepath_get_filename(record_filepath.c_str()) + "'"; + show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + } + } 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", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + } + } + void Overlay::update_ui_recording_paused() { if(!visible || recording_status != RecordingStatus::RECORD) return; @@ -1088,12 +1776,35 @@ namespace gsr { args.push_back(audio_track.c_str()); } } + + if(record_options.restore_portal_session) { + args.push_back("-restore-portal-session"); + args.push_back("yes"); + } + } + + static bool validate_capture_target(const GsrInfo &gsr_info, const std::string &capture_target) { + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + // TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number) + if(capture_target == "focused") { + return capture_options.focused; + } else if(capture_target == "portal") { + return capture_options.portal; + } else { + for(const GsrMonitor &monitor : capture_options.monitors) { + if(capture_target == monitor.name) + return true; + } + return false; + } } void Overlay::on_press_save_replay() { if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0) return; + replay_save_show_notification = true; + replay_save_clock.restart(); kill(gpu_screen_recorder_process, SIGUSR1); } @@ -1111,10 +1822,13 @@ namespace gsr { } paused = false; + replay_save_show_notification = false; // window->close(); // usleep(1000 * 50); // 50 milliseconds + close_gpu_screen_recorder_output(); + if(gpu_screen_recorder_process > 0) { kill(gpu_screen_recorder_process, SIGINT); int status; @@ -1133,6 +1847,13 @@ namespace gsr { return; } + if(!validate_capture_target(gsr_info, config.replay_config.record_options.record_area_option)) { + 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", config.replay_config.record_options.record_area_option.c_str()); + show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::REPLAY); + return; + } + // 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); @@ -1149,7 +1870,9 @@ namespace gsr { } char region[64]; - snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height); + region[0] = '\0'; + if(config.replay_config.record_options.record_area_option == "focused") + snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height); if(config.replay_config.record_options.record_area_option != "focused" && config.replay_config.record_options.change_video_resolution) snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height); @@ -1169,16 +1892,16 @@ namespace gsr { "-o", output_directory.c_str() }; - add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); + if(config.replay_config.restart_replay_on_save && gsr_info.system_info.gsr_version >= GsrVersion{5, 0, 3}) { + args.push_back("-restart-replay-on-save"); + args.push_back("yes"); + } - setenv("GSR_SHOW_SAVED_NOTIFICATION", config.replay_config.show_replay_saved_notifications ? "1" : "0", true); - const std::string script_to_run_on_save = resources_path + (config.replay_config.save_video_in_game_folder ? "scripts/save-video-in-game-folder.sh" : "scripts/notify-saved-name.sh"); - args.push_back("-sc"); - args.push_back(script_to_run_on_save.c_str()); + add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); args.push_back(nullptr); - gpu_screen_recorder_process = exec_program(args.data()); + gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd); if(gpu_screen_recorder_process == -1) { // TODO: Show notification failed to start } else { @@ -1186,6 +1909,12 @@ namespace gsr { update_ui_replay_started(); } + const int fdl = fcntl(gpu_screen_recorder_process_output_fd, F_GETFL); + fcntl(gpu_screen_recorder_process_output_fd, F_SETFL, fdl | O_NONBLOCK); + gpu_screen_recorder_process_output_file = fdopen(gpu_screen_recorder_process_output_fd, "r"); + if(gpu_screen_recorder_process_output_file) + gpu_screen_recorder_process_output_fd = -1; + // TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video. // Make clear to the user that the recording starts after the notification is gone. // Maybe have the option in notification to show timer until its getting hidden, then the notification can say: @@ -1223,17 +1952,29 @@ namespace gsr { if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) { perror("waitpid failed"); /* Ignore... */ + } else { + int exit_code = -1; + if(WIFEXITED(status)) + exit_code = WEXITSTATUS(status); + on_stop_recording(exit_code); } - // window->set_visible(false); - // window->close(); - // return; - //exit(0); + gpu_screen_recorder_process = -1; recording_status = RecordingStatus::NONE; update_ui_recording_stopped(); + record_filepath.clear(); return; } + if(!validate_capture_target(gsr_info, config.record_config.record_options.record_area_option)) { + 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", config.record_config.record_options.record_area_option.c_str()); + show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD); + return; + } + + record_filepath.clear(); + // TODO: Validate input, fallback to valid values const std::string fps = std::to_string(config.record_config.record_options.fps); const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate); @@ -1249,7 +1990,9 @@ namespace gsr { } char region[64]; - snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height); + region[0] = '\0'; + if(config.record_config.record_options.record_area_option == "focused") + snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height); if(config.record_config.record_options.record_area_option != "focused" && config.record_config.record_options.change_video_resolution) snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height); @@ -1270,14 +2013,10 @@ namespace gsr { add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); - setenv("GSR_SHOW_SAVED_NOTIFICATION", config.record_config.show_video_saved_notifications ? "1" : "0", true); - const std::string script_to_run_on_save = resources_path + (config.record_config.save_video_in_game_folder ? "scripts/save-video-in-game-folder.sh" : "scripts/notify-saved-name.sh"); - args.push_back("-sc"); - args.push_back(script_to_run_on_save.c_str()); - args.push_back(nullptr); - gpu_screen_recorder_process = exec_program(args.data()); + record_filepath = output_file; + gpu_screen_recorder_process = exec_program(args.data(), nullptr); if(gpu_screen_recorder_process == -1) { // TODO: Show notification failed to start } else { @@ -1363,6 +2102,13 @@ namespace gsr { return; } + if(!validate_capture_target(gsr_info, config.streaming_config.record_options.record_area_option)) { + 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", config.streaming_config.record_options.record_area_option.c_str()); + show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::STREAM); + 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); @@ -1383,7 +2129,9 @@ namespace gsr { const std::string url = streaming_get_url(config); char region[64]; - snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height); + region[0] = '\0'; + if(config.record_config.record_options.record_area_option == "focused") + snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height); if(config.record_config.record_options.record_area_option != "focused" && config.streaming_config.record_options.change_video_resolution) snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height); @@ -1397,16 +2145,16 @@ namespace gsr { "-fm", framerate_mode.c_str(), "-encoder", encoder, "-f", fps.c_str(), - "-f", fps.c_str(), "-v", "no", "-o", url.c_str() }; + config.streaming_config.record_options.merge_audio_tracks = true; add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); args.push_back(nullptr); - gpu_screen_recorder_process = exec_program(args.data()); + gpu_screen_recorder_process = exec_program(args.data(), nullptr); if(gpu_screen_recorder_process == -1) { // TODO: Show notification failed to start } else { @@ -1427,7 +2175,7 @@ namespace gsr { show_notification("Streaming has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM); } - bool Overlay::update_compositor_texture(const mgl_monitor *monitor) { + bool Overlay::update_compositor_texture(const Monitor &monitor) { window_texture_deinit(&window_texture); window_texture_sprite.set_texture(nullptr); screenshot_texture.clear(); @@ -1440,15 +2188,17 @@ namespace gsr { return false; bool window_texture_loaded = false; - const Window window_at_cursor_position = get_window_at_cursor_position(display); - if(is_window_fullscreen_on_monitor(display, window_at_cursor_position, monitor) && window_at_cursor_position) - window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), window_at_cursor_position, egl_funcs) == 0; + Window focused_window = get_focused_window(display, WindowCaptureType::CURSOR); + if(!focused_window) + focused_window = get_focused_window(display, WindowCaptureType::FOCUSED); + if(focused_window && is_window_fullscreen_on_monitor(display, focused_window, monitor)) + window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), focused_window, egl_funcs) == 0; if(window_texture_loaded && window_texture.texture_id) { window_texture_texture = mgl::Texture(window_texture.texture_id, MGL_TEXTURE_FORMAT_RGB); window_texture_sprite.set_texture(&window_texture_texture); } else { - XImage *img = XGetImage(display, DefaultRootWindow(display), monitor->pos.x, monitor->pos.y, monitor->size.x, monitor->size.y, AllPlanes, ZPixmap); + XImage *img = XGetImage(display, DefaultRootWindow(display), monitor.position.x, monitor.position.y, monitor.size.x, monitor.size.y, AllPlanes, ZPixmap); if(!img) fprintf(stderr, "Error: failed to take a screenshot\n"); @@ -1474,4 +2224,4 @@ namespace gsr { XFlush(display); } } -}
\ No newline at end of file +} |