Compare commits

...

25 Commits
1.0.3 ... 1.0.6

Author SHA1 Message Date
dec05eba
affa44e387 1.0.6 2025-01-07 16:55:26 +01:00
dec05eba
df2eec24a3 Check event window 2025-01-07 16:34:29 +01:00
dec05eba
9a5c20836a Try and support different keyboard layouts through x11 xkb mapping 2025-01-07 01:15:56 +01:00
dec05eba
ee123ceb0a Fix ui being on incorrect monitor on gnome and sway 2025-01-06 22:59:47 +01:00
dec05eba
f2544020b3 Hide decorations on gnome wayland when opening ui when non-x11 application is focused 2025-01-06 15:04:27 +01:00
dec05eba
0c6e2aff07 1.0.5 - fix ui not visible on some kde plasma wayland systems 2025-01-05 18:09:07 +01:00
dec05eba
26ff639f25 Comment 2025-01-05 18:04:36 +01:00
dec05eba
a812d7dbbb Hide window from taskbar 2025-01-05 17:55:40 +01:00
dec05eba
57ae00063c Test dialog 2025-01-05 17:48:16 +01:00
dec05eba
fa5b7a0c75 Only grab left alt, to allow altgr+z to be used for keyboard that type ż with it 2025-01-05 03:22:38 +01:00
dec05eba
52ce22ae22 Add option to only grab virtual devices, to support input remapping software 2025-01-04 05:39:16 +01:00
dec05eba
f379b87b33 Flatpak: disable hotkey section 2025-01-04 03:35:46 +01:00
dec05eba
2ddb9980e1 1.0.4 2025-01-04 03:33:44 +01:00
dec05eba
6b023051eb Fallback to focused window for x11 uncomposited 2025-01-04 03:06:49 +01:00
dec05eba
2aaf6b8380 Fix some games receiving mouse input on wayland 2025-01-04 02:50:48 +01:00
dec05eba
f4dc077299 pidof ignore self 2025-01-04 02:30:14 +01:00
dec05eba
36c7bbfda3 Simplify gsr-ui-cli 2025-01-04 01:44:25 +01:00
dec05eba
9998db8afa Check if gsr-ui is running with pidof equivalent, show ui on restart after enabling/disabling hotkeys 2025-01-03 23:34:53 +01:00
dec05eba
6c03137610 Add option to disable hotkeys, add gsr-ui-cli tool to control gsr-ui remotely 2025-01-03 22:37:13 +01:00
dec05eba
5439fa8a71 README global hotkey info 2025-01-03 21:28:23 +01:00
dec05eba
60e8da0aa9 Remove merge audio tracks option for streaming. Streaming sites dont support multiple audio tracks (in general) 2025-01-03 17:20:16 +01:00
dec05eba
170b2493fc Show estimated file size for cbr for recording and mb/sec for streaming 2025-01-03 04:33:12 +01:00
dec05eba
f7fbb06a92 README email 2025-01-03 02:31:21 +01:00
dec05eba
88b8c80c2b A little bit of glitch fixing, warp cursor after hiding ui when XI grabbing 2025-01-03 02:27:20 +01:00
dec05eba
2a07c74112 Fix some application (using xi) receiving mouse input when UI is shown 2025-01-03 01:57:30 +01:00
24 changed files with 1029 additions and 298 deletions

View File

@@ -7,10 +7,10 @@ Note: This software is still in early alpha. Expect bugs, and please report any
You can report an issue by emailing the issue to dec05eba@protonmail.com.
# Usage
Run `gsr-ui` and press `Alt+Z` to show/hide the UI. You can start the overlay UI at system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
Run `gsr-ui` and press `Left Alt+Z` to show/hide the UI. You can start the overlay UI at system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
There is also an option in the settings to enable/disable starting the program on system startup. This option only works on systems that use systemd.
You have to manually add `gsr-ui` to system startup on systems that uses another init system.\
Note that at the moment different keyboard layouts are not supported. The physical layout of keys are used for global hotkeys. If your Z and Y keys are swapped for example then you need to press Alt+Y instead of Alt+Z to open/hide the UI.
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
# Installation
If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\
@@ -24,6 +24,7 @@ GPU Screen Recorder UI uses meson build system so you need to install `meson` to
These are the dependencies needed to build GPU Screen Recorder UI:
* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxi)
* libxcursor
* libglvnd (which provides libgl, libglx and libegl)
* linux-api-headers
@@ -33,6 +34,10 @@ There are also additional dependencies needed at runtime:
* [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/)
* [GPU Screen Recorder Notification](https://git.dec05eba.com/gpu-screen-recorder-notification/)
## Program behavior notes
This program has to grab all keyboards and create a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.
This might cause issues for you if you use input remapping software. To workaround this you can go into settings and select "Only grab virtual devices"
# License
This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.
@@ -49,6 +54,5 @@ If you want to donate you can donate via bitcoin or monero.
* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
# Known issues
* Some games receive mouse input while the UI is open
* When the UI is open the wallpaper is shown instead of the game on Hyprland and Sway. This is an issue with Hyprland and Sway. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
* Different keyboard layouts are not supported at the moment. The physical layout of keys are used for global hotkeys. If your Z and Y keys are swapped for example then you need to press Alt+Y instead of Alt+Z to open/hide the UI.
* Opening the UI when a game is fullscreened can mess up the game window a bit on Hyprland and Sway. I believe this is an issue in Hyprland and Sway.

38
TODO
View File

@@ -1,12 +1,4 @@
setcap nice for good performance when opening overlay when game is running below 60 fps.
Maybe grab cursor with xi, as that will prevent games from detecting movement with xi2 api.
Fullscreen on wayland doesn't render windows behind because it's a compositor optimization, to not draw anything behind (only draw the window directly without compositing).
Fix this by drawing the window smaller, or have two windows (left and right half monitor width).
Maybe change design to have black triangles appear and get larger until they fill the screen, with even spaces being left with no triangles.
Exclude triangles from a diagonal line across the screen.
Have buttons appear slanted in 3D.
All of these things should be done with vertex buffer, for real 3D.
setcap nice for good performance when opening overlay when game is running below 60 fps (on amd).
WAYLAND_DISPLAY gamescope-0, DISPLAY=:1 (gamescope xwayland)
@@ -47,7 +39,7 @@ Add global setting. In that setting there should be an option to enable/disable
Add profiles and hotkey to switch between profiles (show notification when switching profile).
Fix first frame being black.
Fix first frame being black when running without a compositor.
Add support for systray.
@@ -70,24 +62,17 @@ On nvidia check if suspend fix is applied. If not, show a popup asking the user
Show warning when using steam deck or when trying to capture hevc/av1 on amd (the same warnings as gpu screen recorder gtk).
Add option to capture application audio. This should show a popup where you can use one of the available applications or a custom one and choose to record that application or all applications except that one.
Add profile option. Convert view to profile, add an option at the bottom that says "Edit profiles..." which should show a popup where you can create/remove profiles. New profiles should always be in advanced view.
Verify monitor/audio when starting recording. Give an error if the options are no longer valid.
Get focused window when opening gsr-ui and pass that to the save replay script, to ignore gsr-ui when getting game name.
gsr ui window has _NET_WM_STATE _NET_WM_STATE_ABOVE, not _NET_WM_STATE_FULLSCREEN
gsr ui window has _NET_WM_STATE _NET_WM_STATE_ABOVE, not _NET_WM_STATE_FULLSCREEN.
For replay on fullscreen detect focused fullscreen window by checking if the window size is the same as the monitor size instead of _NET_WM_STATE_FULLSCREEN.
Add audio devices/app refresh button.
Play camera shutter sound when saving recording. When another sound when starting recording.
Some games such as "The Finals" crashes/freezes when they lose focus when running them on x11 on a laptop with prime setup and the monitor runs on the iGPU while the game runs on the dGPU.
Try to reproduce this and if it happens try grab cursor and keyboard instead of setting gsr ui focus and make gsr ui click through like gsr notify. This might fix the issue.
Run `systemctl status --user gpu-screen-recorder` when starting recording and give a notification warning if it returns 0 (running). Or run pidof gpu-screen-recorder.
@@ -95,8 +80,6 @@ Add option to select which gpu to record with, or list all monitors and automati
Test global hotkeys with azerty instead of qwerty.
Fix cursor grab not working in owlboy, need to use xigrab.
Dont allow autostart of replay if capture option is window recording (when window recording is added).
Use global shortcuts desktop portal protocol on wayland when available.
@@ -109,7 +92,7 @@ Move ui hover code from ::draw to ::on_event, to properly handle widget event st
Save audio devices by name instead of id. This is more robust since audio id can change(?).
Improve linux global hotkeys startup time by parsing /proc/bus/input/devices instead of ioctl.
Improve linux global hotkeys startup time by parsing /proc/bus/input/devices instead of ioctl. <- Do this!
We can get the name of the running steam game without x11 by listing processes and finding the one that runs a program called "reaper" with the arguments SteamLaunch AppId=<number>. The binary comes after the -- argument, get the name of the game by parsing out name from that, in the format steamapps/common/<name>/.
@@ -117,10 +100,15 @@ All steam game names by ID are available at https://api.steampowered.com/ISteamA
Dont put widget position to int position when scrolling. This makes the UI jitter when it's coming to a halt.
Show a popup asking if the user wants to add the program to system startup when launching the program, with a dismiss option and "Do not show again".
Show warning if another instance of gpu screen recorder is already running when starting recording?
Change gsr-global-hotkeys to outputs keys pressed instead of the command. This can be done pretty safely by only output keys when modifiers (such as alt) is pressed.
Keyboard leds get turned off when stopping gsr-global-hotkeys (for example numlock). The numlock key has to be pressed twice again to make it look correct to match its state.
Keyboard leds get turned off when stopping gsr-global-hotkeys (for example numlock). The numlock key has to be pressed twice again to make it look correct to match its state.
Implement hotkey changing in global settings by getting mgl key events. During this time gsr-global-hotkey would either need to be paused or add code in the callback handler for the existing hotkeys since they are grabbing hotkeys.
This can only be done after gsr-global-hotkeys properly handle different keyboard layouts to make sure mgl keys match gsr-global-hotkey keys.
Re-enable hotkey disable option for flatpak.
Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change ExecStart to do flatpak run ... || gsr-ui, but make it run as a shell command first with /bin/sh -c "".
When enabling X11 global hotkey again only grab lalt, not ralt.

View File

@@ -25,7 +25,7 @@ namespace gsr {
int32_t video_height = 0;
int32_t fps = 60;
int32_t video_bitrate = 15000;
bool merge_audio_tracks = true;
bool merge_audio_tracks = true; // Currently unused for streaming because all known streaming sites only support 1 audio track
bool application_audio_invert = false;
bool change_video_resolution = false;
std::vector<std::string> audio_tracks;
@@ -43,6 +43,7 @@ namespace gsr {
struct MainConfig {
int32_t config_file_version = 0;
bool software_encoding_warning_shown = false;
std::string hotkeys_enable_option = "enable_hotkeys";
std::string tint_color;
};

View File

@@ -7,7 +7,12 @@
namespace gsr {
class GlobalHotkeysLinux : public GlobalHotkeys {
public:
GlobalHotkeysLinux();
enum class GrabType {
ALL,
VIRTUAL
};
GlobalHotkeysLinux(GrabType grab_type);
GlobalHotkeysLinux(const GlobalHotkeysLinux&) = delete;
GlobalHotkeysLinux& operator=(const GlobalHotkeysLinux&) = delete;
~GlobalHotkeysLinux() override;
@@ -20,5 +25,6 @@ namespace gsr {
int pipes[2];
FILE *read_file = nullptr;
std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id;
GrabType grab_type;
};
}

View File

@@ -58,6 +58,8 @@ namespace gsr {
bool is_open() const;
bool should_exit(std::string &reason) const;
void exit();
const Config& get_config() const;
private:
void xi_setup();
void handle_xi_events();
@@ -65,7 +67,6 @@ namespace gsr {
void grab_mouse_and_keyboard();
void xi_setup_fake_cursor();
void xi_grab_all_devices();
void xi_warp_pointer(mgl::vec2i position);
void close_gpu_screen_recorder_output();
@@ -124,7 +125,6 @@ namespace gsr {
mgl::Texture cursor_texture;
mgl::Sprite cursor_sprite;
mgl::vec2i cursor_hotspot;
bool cursor_drawn = false;
WindowTexture window_texture;
PageStack page_stack;
@@ -166,5 +166,8 @@ namespace gsr {
bool do_exit = false;
std::string exit_reason;
mgl::vec2i window_size = { 1280, 720 };
mgl::vec2i window_pos = { 0, 0 };
};
}

View File

@@ -21,5 +21,5 @@ namespace gsr {
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
// host machine with flatpak-spawn --host
int exec_program_on_host_get_stdout(const char **args, std::string &result);
pid_t pidof(const char *process_name);
pid_t pidof(const char *process_name, pid_t ignore_pid);
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include <mglpp/system/vec.hpp>
#include <string>
#include <X11/Xlib.h>
@@ -11,4 +12,8 @@ namespace gsr {
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
mgl::vec2i get_cursor_position(Display *dpy, Window *window);
mgl::vec2i create_window_get_center_position(Display *display);
std::string get_window_manager_name(Display *display);
bool is_compositor_running(Display *dpy, int screen);
}

View File

@@ -31,6 +31,7 @@ namespace gsr {
private:
std::unique_ptr<Subsection> create_appearance_subsection(ScrollablePage *parent_page);
std::unique_ptr<Subsection> create_startup_subsection(ScrollablePage *parent_page);
std::unique_ptr<Subsection> create_hotkey_subsection(ScrollablePage *parent_page);
std::unique_ptr<Button> create_exit_program_button();
std::unique_ptr<Button> create_go_back_to_old_ui_button();
std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page);
@@ -43,5 +44,6 @@ namespace gsr {
PageStack *page_stack = nullptr;
RadioButton *tint_color_radio_button_ptr = nullptr;
RadioButton *startup_radio_button_ptr = nullptr;
RadioButton *enable_hotkeys_radio_button_ptr = nullptr;
};
}

View File

@@ -69,7 +69,7 @@ namespace gsr {
std::unique_ptr<Widget> create_audio_track_section();
std::unique_ptr<Widget> create_audio_section();
std::unique_ptr<List> create_video_quality_box();
std::unique_ptr<Entry> create_video_bitrate_entry();
std::unique_ptr<List> create_video_bitrate_entry();
std::unique_ptr<List> create_video_bitrate();
std::unique_ptr<ComboBox> create_color_range_box();
std::unique_ptr<List> create_color_range();
@@ -97,9 +97,11 @@ namespace gsr {
std::unique_ptr<List> create_replay_time();
std::unique_ptr<RadioButton> create_start_replay_automatically();
std::unique_ptr<CheckBox> create_save_replay_in_game_folder();
std::unique_ptr<Label> create_estimated_file_size();
void update_estimated_file_size();
std::unique_ptr<Label> create_estimated_replay_file_size();
void update_estimated_replay_file_size();
std::unique_ptr<CheckBox> create_save_recording_in_game_folder();
std::unique_ptr<Label> create_estimated_record_file_size();
void update_estimated_record_file_size();
void add_replay_widgets();
void add_record_widgets();

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.0.3', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.0.6', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -58,6 +58,7 @@ executable(
dependency('xcomposite'),
dependency('xfixes'),
dependency('xi'),
dependency('xcursor'),
],
cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"',
)
@@ -73,6 +74,14 @@ executable(
install : true
)
executable(
'gsr-ui-cli',
[
'tools/gsr-ui-cli/main.c'
],
install : true
)
install_subdir('images', install_dir : gsr_ui_resources_path)
install_subdir('fonts', install_dir : gsr_ui_resources_path)

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.0.3"
version = "1.0.6"
platforms = ["posix"]
[lang.cpp]
@@ -13,4 +13,5 @@ ignore_dirs = ["build", "tools"]
[dependencies]
xcomposite = ">=0"
xfixes = ">=0"
xi = ">=0"
xi = ">=0"
xcursor = ">=1"

View File

@@ -58,6 +58,7 @@ namespace gsr {
return {
{"main.config_file_version", &config.main_config.config_file_version},
{"main.software_encoding_warning_shown", &config.main_config.software_encoding_warning_shown},
{"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option},
{"main.tint_color", &config.main_config.tint_color},
{"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option},

View File

@@ -9,7 +9,15 @@
#define PIPE_WRITE 1
namespace gsr {
GlobalHotkeysLinux::GlobalHotkeysLinux() {
static const char* grab_type_to_arg(GlobalHotkeysLinux::GrabType grab_type) {
switch(grab_type) {
case GlobalHotkeysLinux::GrabType::ALL: return "--all";
case GlobalHotkeysLinux::GrabType::VIRTUAL: return "--virtual";
}
return "--all";
}
GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) {
for(int i = 0; i < 2; ++i) {
pipes[i] = -1;
}
@@ -32,6 +40,7 @@ namespace gsr {
}
bool GlobalHotkeysLinux::start() {
const char *grab_type_arg = grab_type_to_arg(grab_type);
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
const char *user_homepath = getenv("HOME");
if(!user_homepath)
@@ -61,14 +70,14 @@ namespace gsr {
}
if(inside_flatpak) {
const char *args[] = { "flatpak-spawn", "--host", "--", gsr_global_hotkeys_flatpak, NULL };
const char *args[] = { "flatpak-spawn", "--host", "--", gsr_global_hotkeys_flatpak, grab_type_arg, nullptr };
execvp(args[0], (char* const*)args);
} else {
const char *args[] = { "gsr-global-hotkeys", NULL };
const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr };
execvp(args[0], (char* const*)args);
}
perror("execvp");
perror("gsr-global-hotkeys");
_exit(127);
} else { /* parent */
process_id = pid;

View File

@@ -29,6 +29,7 @@
#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>
@@ -41,10 +42,6 @@ namespace gsr {
static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.0;
static bool is_focused_application_wayland(Display *dpy) {
return get_focused_window(dpy, WindowCaptureType::FOCUSED) == 0;
}
static mgl::Texture texture_from_ximage(XImage *img) {
uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3);
// TODO:
@@ -70,17 +67,17 @@ namespace gsr {
return texture;
}
static bool texture_from_x11_cursor(XFixesCursorImage *x11_cursor_image, bool *visible, mgl::vec2i *hotspot, mgl::Texture &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 long *pixels = NULL;
const unsigned int *pixels = NULL;
*visible = false;
if(!x11_cursor_image)
goto err;
return false;
if(!x11_cursor_image->pixels)
goto err;
return false;
hotspot->x = x11_cursor_image->xhot;
hotspot->y = x11_cursor_image->yhot;
@@ -88,12 +85,12 @@ namespace gsr {
pixels = x11_cursor_image->pixels;
cursor_data = (uint8_t*)malloc((int)x11_cursor_image->width * (int)x11_cursor_image->height * 4);
if(!cursor_data)
goto err;
return false;
out = cursor_data;
/* Un-premultiply alpha */
for(int y = 0; y < x11_cursor_image->height; ++y) {
for(int x = 0; x < x11_cursor_image->width; ++x) {
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];
@@ -114,13 +111,7 @@ namespace gsr {
texture.load_from_memory(cursor_data, x11_cursor_image->width, x11_cursor_image->height, MGL_IMAGE_FORMAT_RGBA);
free(cursor_data);
XFree(x11_cursor_image);
return true;
err:
if(x11_cursor_image)
XFree(x11_cursor_image);
return false;
}
static char hex_value_to_str(uint8_t v) {
@@ -299,81 +290,6 @@ namespace gsr {
return &win->monitors[0];
}
static mgl::vec2i create_window_get_center_position(Display *display) {
const int size = 16;
XSetWindowAttributes window_attr;
window_attr.event_mask = StructureNotifyMask;
window_attr.background_pixel = 0;
const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
if(!window)
return {0, 0};
const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False);
const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
const Atom window_type_atoms[2] = {
net_wm_window_type_notification_atom,
net_wm_window_type_utility
};
XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L);
const double alpha = 0.0;
const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
XSizeHints *size_hints = XAllocSizeHints();
size_hints->width = size;
size_hints->min_width = size;
size_hints->max_width = size;
size_hints->height = size;
size_hints->min_height = size;
size_hints->max_height = size;
size_hints->flags = PSize | PMinSize | PMaxSize;
XSetWMNormalHints(display, window, size_hints);
XFree(size_hints);
XMapWindow(display, window);
XFlush(display);
const int x_fd = XConnectionNumber(display);
mgl::vec2i window_pos;
XEvent xev;
while(true) {
struct pollfd poll_fd;
poll_fd.fd = x_fd;
poll_fd.events = POLLIN;
poll_fd.revents = 0;
const int fds_ready = poll(&poll_fd, 1, 1000);
if(fds_ready == 0) {
fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n");
break;
} else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
continue;
}
XNextEvent(display, &xev);
if(xev.type == ConfigureNotify) {
window_pos.x = xev.xconfigure.x + xev.xconfigure.width / 2;
window_pos.y = xev.xconfigure.y + xev.xconfigure.height / 2;
break;
}
}
XDestroyWindow(display, window);
XFlush(display);
return window_pos;
}
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;
}
static std::string get_power_supply_online_filepath() {
std::string result;
const char *paths[] = {
@@ -431,9 +347,6 @@ namespace gsr {
top_bar_background({0.0f, 0.0f}),
close_button_widget({0.0f, 0.0f})
{
// TODO:
//xi_setup();
key_bindings[0].key_event.code = mgl::Keyboard::Escape;
key_bindings[0].key_event.alt = false;
key_bindings[0].key_event.control = false;
@@ -481,11 +394,7 @@ namespace gsr {
}
close_gpu_screen_recorder_output();
free(xi_input_xev);
free(xi_output_xev);
if(xi_display)
XCloseDisplay(xi_display);
deinit_color_theme();
}
void Overlay::xi_setup() {
@@ -509,6 +418,7 @@ namespace gsr {
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);
@@ -567,8 +477,8 @@ namespace gsr {
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->event_x;
xi_output_xev->xmotion.y = de->event_y;
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:
@@ -580,8 +490,8 @@ namespace gsr {
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->event_x;
xi_output_xev->xbutton.y = de->event_y;
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:
@@ -594,8 +504,8 @@ namespace gsr {
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->event_x;
xi_output_xev->xkey.y = de->event_y;
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;
@@ -694,12 +604,6 @@ namespace gsr {
page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
if(cursor_texture.is_valid()) {
if(!cursor_drawn) {
cursor_drawn = true;
XFixesHideCursor(xi_display, DefaultRootWindow(xi_display));
XFlush(xi_display);
}
cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f());
window->draw(cursor_sprite);
}
@@ -737,13 +641,32 @@ namespace gsr {
if(!xi_display)
return;
XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
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 = XcursorShapeLoadImage(XC_left_ptr, cursor_theme, cursor_size);
if(!cursor_image) {
fprintf(stderr, "Error: failed to get cursor\n");
return;
}
bool cursor_visible = false;
texture_from_x11_cursor(XFixesGetCursorImage(xi_display), &cursor_visible, &cursor_hotspot, cursor_texture);
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);
}
void Overlay::xi_grab_all_devices() {
@@ -755,38 +678,22 @@ namespace gsr {
if(!info)
return;
for (int i = 0; i < num_devices; ++i) {
const XIDeviceInfo *dev = &info[i];
XIEventMask masks[1];
unsigned char mask0[XIMaskLen(XI_LASTEVENT)];
memset(mask0, 0, sizeof(mask0));
XISetMask(mask0, XI_Motion);
XISetMask(mask0, XI_ButtonPress);
XISetMask(mask0, XI_ButtonRelease);
XISetMask(mask0, XI_KeyPress);
XISetMask(mask0, XI_KeyRelease);
masks[0].deviceid = dev->deviceid;
masks[0].mask_len = sizeof(mask0);
masks[0].mask = mask0;
XIGrabDevice(xi_display, dev->deviceid, window->get_system_handle(), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, masks);
}
XFlush(xi_display);
XIFreeDeviceInfo(info);
}
void Overlay::xi_warp_pointer(mgl::vec2i position) {
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];
XIWarpPointer(xi_display, dev->deviceid, DefaultRootWindow(xi_display), DefaultRootWindow(xi_display), 0, 0, 0, 0, position.x, position.y);
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);
@@ -805,24 +712,37 @@ namespace gsr {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const std::string wm_name = get_window_manager_name(display);
const bool is_kwin = wm_name == "KWin";
// 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);
// 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 || !is_focused_application_wayland(display);
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window;
mgl::vec2i window_size = { 1280, 720 };
mgl::vec2i window_pos = { 0, 0 };
window_size = { 32, 32 };
window_pos = { 0, 0 };
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;
if(prevent_game_minimizing) {
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.hidden = prevent_game_minimizing;
window_create_params.override_redirect = prevent_game_minimizing;
window_create_params.background_color = bg_color;
window_create_params.support_alpha = true;
window_create_params.window_type = MGL_WINDOW_TYPE_NORMAL;
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;
window_create_params.render_api = MGL_RENDER_API_EGL;
if(!window->create("gsr ui", window_create_params))
@@ -842,19 +762,23 @@ namespace gsr {
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;
}
// 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
const mgl::vec2i monitor_position_query_value = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? create_window_get_center_position(display) : mgl::vec2i(win->cursor_position.x, win->cursor_position.y);
const mgl_monitor *focused_monitor = find_monitor_at_position(*window, monitor_position_query_value);
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(window_pos);
}
win->cursor_position.x = cursor_position.x - window_pos.x;
win->cursor_position.y = cursor_position.y - window_pos.y;
update_compositor_texture(focused_monitor);
@@ -1032,6 +956,11 @@ namespace gsr {
return 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.
if(prevent_game_minimizing)
xi_setup();
//window->set_fullscreen(true);
if(gsr_info.system_info.display_server == DisplayServer::X11)
make_window_click_through(display, window->get_system_handle());
@@ -1045,7 +974,7 @@ namespace gsr {
XFreeCursor(display, default_cursor);
default_cursor = 0;
}
default_cursor = XCreateFontCursor(display, XC_arrow);
default_cursor = XCreateFontCursor(display, XC_left_ptr);
XFlush(display);
grab_mouse_and_keyboard();
@@ -1107,10 +1036,6 @@ namespace gsr {
XFlush(display);
if(xi_display) {
// TODO: Only show cursor if it wasn't hidden before the ui was shown
cursor_drawn = false;
//XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
//XFlush(xi_display);
cursor_texture.clear();
cursor_sprite.set_texture(nullptr);
}
@@ -1123,29 +1048,45 @@ namespace gsr {
visible = false;
drawn_first_frame = false;
if(window) {
const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position();
window->set_visible(false);
window.reset();
if(xi_input_xev) {
free(xi_input_xev);
xi_input_xev = nullptr;
}
if(xi_display) {
XFlush(display);
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);
//xi_warp_pointer(new_cursor_position);
XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
XFlush(xi_display);
XFixesShowCursor(display, DefaultRootWindow(display));
XFlush(display);
}
}
if(window) {
window->set_visible(false);
window.reset();
}
deinit_theme();
}
void Overlay::toggle_show() {
if(visible) {
//hide();
// We dont want to hide immediately because hide is called in event callback, in which it destroys the window.
// 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();
@@ -1241,6 +1182,10 @@ namespace gsr {
do_exit = true;
}
const Config& Overlay::get_config() const {
return config;
}
void Overlay::update_notification_process_status() {
if(notification_process <= 0)
return;
@@ -1983,6 +1928,7 @@ namespace gsr {
"-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);
@@ -2021,8 +1967,10 @@ namespace gsr {
return false;
bool window_texture_loaded = false;
const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED);
if(is_window_fullscreen_on_monitor(display, focused_window, monitor) && focused_window)
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) {

View File

@@ -59,7 +59,7 @@ namespace gsr {
const pid_t second_child = vfork();
if(second_child == 0) { // child
execvp(args[0], (char* const*)args);
perror("execvp");
perror(args[0]);
_exit(127);
} else if(second_child != -1) {
// TODO:
@@ -98,7 +98,7 @@ namespace gsr {
close(fds[PIPE_WRITE]);
execvp(args[0], (char* const*)args);
perror("execvp");
perror(args[0]);
_exit(127);
} else { /* parent */
close(fds[PIPE_WRITE]);
@@ -206,7 +206,7 @@ namespace gsr {
return false;
}
pid_t pidof(const char *process_name) {
pid_t pidof(const char *process_name, pid_t ignore_pid) {
pid_t result = -1;
DIR *dir = opendir("/proc");
if(!dir)
@@ -222,8 +222,11 @@ namespace gsr {
snprintf(cmdline_filepath, sizeof(cmdline_filepath), "/proc/%s/cmdline", entry->d_name);
if(read_cmdline_arg0(cmdline_filepath, arg0, sizeof(arg0)) && strcmp(process_name, arg0) == 0) {
result = atoi(entry->d_name);
break;
const pid_t pid = atoi(entry->d_name);
if(pid != ignore_pid) {
result = pid;
break;
}
}
}

View File

@@ -7,8 +7,31 @@
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#define MAX_PROPERTY_VALUE_LEN 4096
namespace gsr {
static unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
Atom ret_property_type = None;
int ret_format = 0;
unsigned long num_items = 0;
unsigned long num_remaining_bytes = 0;
unsigned char *data = nullptr;
const Atom atom = XInternAtom(dpy, property_name, False);
if(XGetWindowProperty(dpy, window, atom, 0, MAX_PROPERTY_VALUE_LEN / 4, False, property_type, &ret_property_type, &ret_format, &num_items, &num_remaining_bytes, &data) != Success || !data) {
return nullptr;
}
if(ret_property_type != property_type) {
XFree(data);
return nullptr;
}
*property_size = (ret_format / (32 / sizeof(long))) * num_items;
return data;
}
static bool window_has_atom(Display *dpy, Window window, Atom atom) {
Atom type;
unsigned long len, bytes_left;
@@ -29,19 +52,56 @@ 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) {
if(window == None)
return None;
if(window_is_user_program(display, window))
return window;
Window root;
Window parent;
Window *children = nullptr;
unsigned int num_children = 0;
if(!XQueryTree(display, window, &root, &parent, &children, &num_children) || !children)
return None;
Window found_window = None;
for(int i = num_children - 1; i >= 0; --i) {
if(children[i] && window_is_user_program(display, children[i])) {
found_window = children[i];
goto finished;
}
}
for(int i = num_children - 1; i >= 0; --i) {
if(children[i]) {
Window win = window_get_target_window_child(display, children[i]);
if(win) {
found_window = win;
goto finished;
}
}
}
finished:
XFree(children);
return found_window;
}
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);
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &dummy_i, &dummy_i, &dummy_u);
if(window)
window = window_get_target_window_child(dpy, window);
return window;
}
Window get_focused_window(Display *dpy, WindowCaptureType cap_type) {
const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
//const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
Window focused_window = None;
if(cap_type == WindowCaptureType::FOCUSED) {
@@ -68,7 +128,7 @@ namespace gsr {
}
focused_window = get_window_at_cursor_position(dpy);
if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
if(focused_window && focused_window != DefaultRootWindow(dpy))
return focused_window;
return None;
@@ -123,7 +183,7 @@ namespace gsr {
return str;
}
static std::string string_string(const char *str) {
static std::string strip_strip(const char *str) {
int len = 0;
str = strip(str, &len);
return std::string(str, len);
@@ -138,7 +198,7 @@ namespace gsr {
// Window title is not always ideal (for example for a browser), but for games its pretty much required
char *window_title = get_window_title(dpy, focused_window);
if(window_title) {
result = string_string(window_title);
result = strip_strip(window_title);
XFree(window_title);
return result;
}
@@ -146,10 +206,255 @@ namespace gsr {
XClassHint class_hint = {nullptr, nullptr};
XGetClassHint(dpy, focused_window, &class_hint);
if(class_hint.res_class) {
result = string_string(class_hint.res_class);
result = strip_strip(class_hint.res_class);
return result;
}
return result;
}
mgl::vec2i get_cursor_position(Display *dpy, Window *window) {
Window root_window = None;
*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);
// This dumb shit is done to satisfy gnome wayland. Only set |window| if a valid x11 window is focused
if(window) {
XWindowAttributes attr;
if(XGetWindowAttributes(dpy, *window, &attr) && attr.override_redirect)
*window = None;
int revert_to = 0;
Window input_focus_window = None;
if(XGetInputFocus(dpy, &input_focus_window, &revert_to)) {
if(input_focus_window) {
if(XGetWindowAttributes(dpy, input_focus_window, &attr) && attr.override_redirect)
*window = None;
} else {
*window = None;
}
}
}
return root_pos;
}
typedef struct {
unsigned long flags;
unsigned long functions;
unsigned long decorations;
long input_mode;
unsigned long status;
} MotifHints;
#define MWM_HINTS_DECORATIONS 2
#define MWM_DECOR_NONE 0
#define MWM_DECOR_ALL 1
static void window_set_decorations_visible(Display *display, Window window, bool visible) {
const Atom motif_wm_hints_atom = XInternAtom(display, "_MOTIF_WM_HINTS", False);
MotifHints motif_hints;
memset(&motif_hints, 0, sizeof(motif_hints));
motif_hints.flags = MWM_HINTS_DECORATIONS;
motif_hints.decorations = visible ? MWM_DECOR_ALL : MWM_DECOR_NONE;
XChangeProperty(display, window, motif_wm_hints_atom, motif_wm_hints_atom, 32, PropModeReplace, (unsigned char*)&motif_hints, sizeof(motif_hints) / sizeof(long));
}
static bool create_window_get_center_position_kde(Display *display, mgl::vec2i &position) {
const int size = 1;
XSetWindowAttributes window_attr;
window_attr.event_mask = StructureNotifyMask;
window_attr.background_pixel = 0;
const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
if(!window)
return false;
const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False);
const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
const Atom window_type_atoms[2] = {
net_wm_window_type_notification_atom,
net_wm_window_type_utility
};
XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L);
const double alpha = 0.0;
const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
window_set_decorations_visible(display, window, false);
XSizeHints *size_hints = XAllocSizeHints();
size_hints->width = size;
size_hints->height = size;
size_hints->min_width = size;
size_hints->min_height = size;
size_hints->max_width = size;
size_hints->max_height = size;
size_hints->flags = PSize | PMinSize | PMaxSize;
XSetWMNormalHints(display, window, size_hints);
XFree(size_hints);
XMapWindow(display, window);
XFlush(display);
bool got_data = false;
const int x_fd = XConnectionNumber(display);
XEvent xev;
while(true) {
struct pollfd poll_fd;
poll_fd.fd = x_fd;
poll_fd.events = POLLIN;
poll_fd.revents = 0;
const int fds_ready = poll(&poll_fd, 1, 1000);
if(fds_ready == 0) {
fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n");
break;
} else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
continue;
}
XNextEvent(display, &xev);
if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
break;
}
}
XDestroyWindow(display, window);
XFlush(display);
return got_data;
}
static bool create_window_get_center_position_gnome(Display *display, mgl::vec2i &position) {
const int size = 32;
XSetWindowAttributes window_attr;
window_attr.event_mask = StructureNotifyMask | ExposureMask;
window_attr.background_pixel = 0;
const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
if(!window)
return false;
const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
const double alpha = 0.0;
const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
window_set_decorations_visible(display, window, false);
XSizeHints *size_hints = XAllocSizeHints();
size_hints->width = size;
size_hints->height = size;
size_hints->min_width = size;
size_hints->min_height = size;
size_hints->max_width = size;
size_hints->max_height = size;
size_hints->flags = PSize | PMinSize | PMaxSize;
XSetWMNormalHints(display, window, size_hints);
XFree(size_hints);
XMapWindow(display, window);
XFlush(display);
bool got_data = false;
const int x_fd = XConnectionNumber(display);
XEvent xev;
while(true) {
struct pollfd poll_fd;
poll_fd.fd = x_fd;
poll_fd.events = POLLIN;
poll_fd.revents = 0;
const int fds_ready = poll(&poll_fd, 1, 1000);
if(fds_ready == 0) {
fprintf(stderr, "Error: timed out waiting for MapNotify/ConfigureNotify after XCreateWindow\n");
break;
} else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
continue;
}
XNextEvent(display, &xev);
if(xev.type == MapNotify && xev.xmap.window == window) {
int x = 0;
int y = 0;
Window w = None;
XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w);
got_data = x > 0 && y > 0;
position.x = x + size / 2;
position.y = y + size / 2;
if(got_data)
break;
} else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
if(got_data)
break;
}
}
XDestroyWindow(display, window);
XFlush(display);
return got_data;
}
mgl::vec2i create_window_get_center_position(Display *display) {
mgl::vec2i pos;
if(!create_window_get_center_position_kde(display, pos)) {
pos.x = 0;
pos.y = 0;
create_window_get_center_position_gnome(display, pos);
}
return pos;
}
std::string get_window_manager_name(Display *display) {
std::string wm_name;
unsigned int property_size = 0;
Window window = None;
unsigned char *net_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_NET_SUPPORTING_WM_CHECK", &property_size);
if(net_supporting_wm_check) {
if(property_size == 8)
window = *(Window*)net_supporting_wm_check;
XFree(net_supporting_wm_check);
}
if(!window) {
unsigned char *win_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_WIN_SUPPORTING_WM_CHECK", &property_size);
if(win_supporting_wm_check) {
if(property_size == 8)
window = *(Window*)win_supporting_wm_check;
XFree(win_supporting_wm_check);
}
}
if(!window)
return wm_name;
char *window_title = get_window_title(display, window);
if(window_title) {
wm_name = strip_strip(window_title);
XFree(window_title);
}
return wm_name;
}
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;
}
}

View File

@@ -88,6 +88,31 @@ namespace gsr {
return std::make_unique<Subsection>("Startup", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
}
std::unique_ptr<Subsection> GlobalSettingsPage::create_hotkey_subsection(ScrollablePage *parent_page) {
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL);
enable_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get();
enable_hotkeys_radio_button->add_item("Enable hotkeys", "enable_hotkeys");
if(!inside_flatpak)
enable_hotkeys_radio_button->add_item("Disable hotkeys", "disable_hotkeys");
enable_hotkeys_radio_button->add_item("Only grab virtual devices (supports input remapping software)", "enable_hotkeys_virtual_devices");
enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) {
if(!on_click_exit_program_button)
return true;
if(id == "enable_hotkeys")
on_click_exit_program_button("restart");
else if(id == "disable_hotkeys")
on_click_exit_program_button("restart");
else if(id == "enable_hotkeys_virtual_devices")
on_click_exit_program_button("restart");
return true;
};
return std::make_unique<Subsection>("Hotkeys", std::move(enable_hotkeys_radio_button), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
}
std::unique_ptr<Button> GlobalSettingsPage::create_exit_program_button() {
auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Exit program", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
exit_program_button->on_click = [&]() {
@@ -108,7 +133,6 @@ namespace gsr {
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) {
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
list->add_widget(create_exit_program_button());
if(inside_flatpak)
@@ -123,6 +147,7 @@ namespace gsr {
settings_list->set_spacing(0.018f);
settings_list->add_widget(create_appearance_subsection(scrollable_page.get()));
settings_list->add_widget(create_startup_subsection(scrollable_page.get()));
settings_list->add_widget(create_hotkey_subsection(scrollable_page.get()));
settings_list->add_widget(create_application_options_subsection(scrollable_page.get()));
scrollable_page->add_widget(std::move(settings_list));
@@ -143,10 +168,13 @@ namespace gsr {
std::string stdout_str;
const int exit_status = exec_program_on_host_get_stdout(args, stdout_str);
startup_radio_button_ptr->set_selected_item(exit_status == 0 ? "start_on_system_startup" : "dont_start_on_system_startup", false, false);
enable_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false);
}
void GlobalSettingsPage::save() {
config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id();
config.main_config.hotkeys_enable_option = enable_hotkeys_radio_button_ptr->get_selected_id();
save_config(config);
}
}

View File

@@ -304,7 +304,8 @@ namespace gsr {
std::unique_ptr<Widget> SettingsPage::create_audio_section() {
auto audio_device_section_list = std::make_unique<List>(List::Orientation::VERTICAL);
audio_device_section_list->add_widget(create_audio_track_section());
audio_device_section_list->add_widget(create_merge_audio_tracks_checkbox());
if(type != Type::STREAM)
audio_device_section_list->add_widget(create_merge_audio_tracks_checkbox());
audio_device_section_list->add_widget(create_application_audio_invert_checkbox());
audio_device_section_list->add_widget(create_audio_codec());
return std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
@@ -338,11 +339,27 @@ namespace gsr {
return list;
}
std::unique_ptr<Entry> SettingsPage::create_video_bitrate_entry() {
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));
video_bitrate_entry->validate_handler = create_entry_validator_integer_in_range(1, 500000);
video_bitrate_entry_ptr = video_bitrate_entry.get();
return video_bitrate_entry;
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.92MB", get_color_theme().text_color);
Label *size_mb_label_ptr = size_mb_label.get();
list->add_widget(std::move(size_mb_label));
video_bitrate_entry_ptr->on_changed = [size_mb_label_ptr](const std::string &text) {
const double video_bitrate_mb_per_seconds = (double)atoi(text.c_str()) / 1000LL / 8LL * 1.024;
char buffer[32];
snprintf(buffer, sizeof(buffer), "%.2fMB", video_bitrate_mb_per_seconds);
size_mb_label_ptr->set_text(buffer);
};
}
return list;
}
std::unique_ptr<List> SettingsPage::create_video_bitrate() {
@@ -387,20 +404,20 @@ namespace gsr {
video_codec_box->add_item("H264", "h264");
if(gsr_info->supported_video_codecs.hevc)
video_codec_box->add_item("HEVC", "hevc");
if(gsr_info->supported_video_codecs.hevc_10bit)
video_codec_box->add_item("HEVC (10 bit, reduces banding)", "hevc_10bit");
if(gsr_info->supported_video_codecs.hevc_hdr)
video_codec_box->add_item("HEVC (HDR)", "hevc_hdr");
if(gsr_info->supported_video_codecs.av1)
video_codec_box->add_item("AV1", "av1");
if(gsr_info->supported_video_codecs.av1_10bit)
video_codec_box->add_item("AV1 (10 bit, reduces banding)", "av1_10bit");
if(gsr_info->supported_video_codecs.av1_hdr)
video_codec_box->add_item("AV1 (HDR)", "av1_hdr");
if(gsr_info->supported_video_codecs.vp8)
video_codec_box->add_item("VP8", "vp8");
if(gsr_info->supported_video_codecs.vp9)
video_codec_box->add_item("VP9", "vp9");
if(gsr_info->supported_video_codecs.hevc_hdr)
video_codec_box->add_item("HEVC (HDR)", "hevc_hdr");
if(gsr_info->supported_video_codecs.hevc_10bit)
video_codec_box->add_item("HEVC (10 bit, reduces banding)", "hevc_10bit");
if(gsr_info->supported_video_codecs.av1_hdr)
video_codec_box->add_item("AV1 (HDR)", "av1_hdr");
if(gsr_info->supported_video_codecs.av1_10bit)
video_codec_box->add_item("AV1 (10 bit, reduces banding)", "av1_10bit");
if(gsr_info->supported_video_codecs.h264_software)
video_codec_box->add_item("H264 Software Encoder (Slow, not recommended)", "h264_software");
video_codec_box_ptr = video_codec_box.get();
@@ -647,16 +664,16 @@ namespace gsr {
return checkbox;
}
std::unique_ptr<Label> SettingsPage::create_estimated_file_size() {
auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video max file size in RAM: 5.23MB", get_color_theme().text_color);
std::unique_ptr<Label> SettingsPage::create_estimated_replay_file_size() {
auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video max file size in RAM: 57.60MB", get_color_theme().text_color);
estimated_file_size_ptr = label.get();
return label;
}
void SettingsPage::update_estimated_file_size() {
void SettingsPage::update_estimated_replay_file_size() {
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) / 1024.0 / 1024.0;
const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024;
char buffer[512];
snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB", video_filesize_mb);
@@ -670,7 +687,7 @@ namespace gsr {
file_info_data_list->add_widget(create_container_section());
file_info_data_list->add_widget(create_replay_time());
file_info_list->add_widget(std::move(file_info_data_list));
file_info_list->add_widget(create_estimated_file_size());
file_info_list->add_widget(create_estimated_replay_file_size());
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);
@@ -713,11 +730,11 @@ namespace gsr {
view_radio_button_ptr->on_selection_changed("Simple", "simple");
replay_time_entry_ptr->on_changed = [this](const std::string&) {
update_estimated_file_size();
update_estimated_replay_file_size();
};
video_bitrate_entry_ptr->on_changed = [this](const std::string&) {
update_estimated_file_size();
update_estimated_replay_file_size();
};
}
@@ -729,11 +746,29 @@ namespace gsr {
return checkbox;
}
std::unique_ptr<Label> SettingsPage::create_estimated_record_file_size() {
auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video file size per minute (excluding audio): 345.60MB", get_color_theme().text_color);
estimated_file_size_ptr = label.get();
return label;
}
void SettingsPage::update_estimated_record_file_size() {
const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL;
const double video_filesize_mb_per_minute = (60.0 * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024;
char buffer[512];
snprintf(buffer, sizeof(buffer), "Estimated video file size per minute (excluding audio): %.2fMB", video_filesize_mb_per_minute);
estimated_file_size_ptr->set_text(buffer);
}
void SettingsPage::add_record_widgets() {
auto file_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
file_list->add_widget(create_save_directory("Directory to save the video:"));
file_list->add_widget(create_container_section());
settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
file_info_data_list->add_widget(create_save_directory("Directory to save the video:"));
file_info_data_list->add_widget(create_container_section());
file_info_list->add_widget(std::move(file_info_data_list));
file_info_list->add_widget(create_estimated_record_file_size());
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_save_recording_in_game_folder());
@@ -767,6 +802,10 @@ namespace gsr {
return true;
};
view_radio_button_ptr->on_selection_changed("Simple", "simple");
video_bitrate_entry_ptr->on_changed = [this](const std::string&) {
update_estimated_record_file_size();
};
}
std::unique_ptr<ComboBox> SettingsPage::create_streaming_service_box() {
@@ -969,7 +1008,8 @@ namespace gsr {
void SettingsPage::load_common(RecordOptions &record_options) {
record_area_box_ptr->set_selected_item(record_options.record_area_option);
merge_audio_tracks_checkbox_ptr->set_checked(record_options.merge_audio_tracks);
if(merge_audio_tracks_checkbox_ptr)
merge_audio_tracks_checkbox_ptr->set_checked(record_options.merge_audio_tracks);
application_audio_invert_checkbox_ptr->set_checked(record_options.application_audio_invert);
change_video_resolution_checkbox_ptr->set_checked(record_options.change_video_resolution);
load_audio_tracks(record_options);
@@ -1090,7 +1130,8 @@ namespace gsr {
record_options.video_height = atoi(video_height_entry_ptr->get_text().c_str());
record_options.fps = atoi(framerate_entry_ptr->get_text().c_str());
record_options.video_bitrate = atoi(video_bitrate_entry_ptr->get_text().c_str());
record_options.merge_audio_tracks = merge_audio_tracks_checkbox_ptr->is_checked();
if(merge_audio_tracks_checkbox_ptr)
record_options.merge_audio_tracks = merge_audio_tracks_checkbox_ptr->is_checked();
record_options.application_audio_invert = application_audio_invert_checkbox_ptr->is_checked();
record_options.change_video_resolution = change_video_resolution_checkbox_ptr->is_checked();
save_audio_tracks(record_options.audio_tracks, audio_track_list_ptr);

View File

@@ -1,5 +1,4 @@
#include "../include/GsrInfo.hpp"
#include "../include/Theme.hpp"
#include "../include/Overlay.hpp"
#include "../include/GlobalHotkeysX11.hpp"
#include "../include/GlobalHotkeysLinux.hpp"
@@ -96,8 +95,8 @@ static std::unique_ptr<gsr::GlobalHotkeysX11> register_x11_hotkeys(gsr::Overlay
return global_hotkeys;
}
static std::unique_ptr<gsr::GlobalHotkeysLinux> register_linux_hotkeys(gsr::Overlay *overlay) {
auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysLinux>();
static std::unique_ptr<gsr::GlobalHotkeysLinux> register_linux_hotkeys(gsr::Overlay *overlay, gsr::GlobalHotkeysLinux::GrabType grab_type) {
auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysLinux>(grab_type);
if(!global_hotkeys->start())
fprintf(stderr, "error: failed to start global hotkeys\n");
@@ -134,6 +133,43 @@ static std::unique_ptr<gsr::GlobalHotkeysLinux> register_linux_hotkeys(gsr::Over
return global_hotkeys;
}
static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
rpc->add_handler("show_ui", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->show();
});
rpc->add_handler("toggle-show", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->toggle_show();
});
rpc->add_handler("toggle-record", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->toggle_record();
});
rpc->add_handler("toggle-pause", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->toggle_pause();
});
rpc->add_handler("toggle-stream", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->toggle_stream();
});
rpc->add_handler("toggle-replay", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->toggle_replay();
});
rpc->add_handler("replay-save", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->save_replay();
});
}
static bool is_gsr_ui_virtual_keyboard_running() {
FILE *f = fopen("/proc/bus/input/devices", "rb");
if(!f)
@@ -193,7 +229,9 @@ int main(int argc, char **argv) {
// 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.
if(is_gsr_ui_virtual_keyboard_running()) {
// 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.
if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) {
gsr::Rpc 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");
@@ -204,12 +242,6 @@ int main(int argc, char **argv) {
}
return 1;
}
// const pid_t gsr_ui_pid = gsr::pidof("gsr-ui");
// if(gsr_ui_pid != -1) {
// const char *args[] = { "gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
// gsr::exec_program_daemonized(args);
// return 1;
// }
// Cant get window texture when prime-run is used
disable_prime_run();
@@ -271,20 +303,21 @@ int main(int argc, char **argv) {
fprintf(stderr, "Info: gsr ui is now ready, waiting for inputs. Press alt+z to show/hide the overlay\n");
auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs);
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");
auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs);
rpc_add_commands(rpc.get(), overlay.get());
rpc->add_handler("show_ui", [&](const std::string&) {
overlay->show();
});
std::unique_ptr<gsr::GlobalHotkeys> global_hotkeys = register_linux_hotkeys(overlay.get());
if(launch_action == LaunchAction::LAUNCH_SHOW)
overlay->show();
std::unique_ptr<gsr::GlobalHotkeys> global_hotkeys = nullptr;
if(overlay->get_config().main_config.hotkeys_enable_option == "enable_hotkeys")
global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::ALL);
else if(overlay->get_config().main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices")
global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::VIRTUAL);
// 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.
@@ -296,7 +329,10 @@ int main(int argc, char **argv) {
gsr::set_frame_delta_seconds(frame_delta_seconds);
rpc->poll();
global_hotkeys->poll_events();
if(global_hotkeys)
global_hotkeys->poll_events();
overlay->handle_events(global_hotkeys.get());
if(!overlay->draw()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -306,15 +342,17 @@ int main(int argc, char **argv) {
fprintf(stderr, "Info: shutting down!\n");
rpc.reset();
global_hotkeys.reset();
if(global_hotkeys)
global_hotkeys.reset();
overlay.reset();
gsr::deinit_theme();
gsr::deinit_color_theme();
mgl_deinit();
if(exit_reason == "back-to-old-ui") {
const char *args[] = { "gpu-screen-recorder-gtk", "use-old-ui", nullptr };
execvp(args[0], (char* const*)args);
} else if(exit_reason == "restart") {
const char *args[] = { "gsr-ui", "launch-show", nullptr };
execvp(args[0], (char* const*)args);
}
return 0;

View File

@@ -25,6 +25,24 @@
#define KEY_STATES_SIZE (KEY_MAX/8 + 1)
#define KEYCODE_TO_XKB_KEYCODE(key) ((key) + 8)
#define XK_Shift_L 0xffe1 /* Left shift */
#define XK_Shift_R 0xffe2 /* Right shift */
#define XK_Control_L 0xffe3 /* Left control */
#define XK_Control_R 0xffe4 /* Right control */
#define XK_Alt_L 0xffe9 /* Left alt */
#define XK_Alt_R 0xffea /* Right alt */
#define XK_Super_L 0xffeb /* Left super */
#define XK_Super_R 0xffec /* Right super */
#define XK_z 0x007a
#define XK_F7 0xffc4
#define XK_F8 0xffc5
#define XK_F9 0xffc6
#define XK_F10 0xffc7
#define XK_F11 0xffc8
static inline int count_num_bits_set(unsigned char c) {
int n = 0;
n += (c & 1);
@@ -117,6 +135,35 @@ static void keyboard_event_process_key_state_change(keyboard_event *self, struct
}
}
static uint32_t keycode_to_keysym(keyboard_event *self, uint16_t keycode) {
const unsigned long xkb_keycode = KEYCODE_TO_XKB_KEYCODE(keycode);
if(self->x_context.display && self->x_context.XKeycodeToKeysym && xkb_keycode <= 255)
return self->x_context.XKeycodeToKeysym(self->x_context.display, xkb_keycode, 0);
else
return 0;
}
/* TODO: Support more keys when needed */
static uint32_t keysym_to_keycode(uint32_t keysym) {
switch(keysym) {
case XK_Control_L: return KEY_LEFTCTRL;
case XK_Shift_L: return KEY_LEFTSHIFT;
case XK_Alt_L: return KEY_LEFTALT;
case XK_Super_L: return KEY_LEFTMETA;
case XK_Control_R: return KEY_RIGHTCTRL;
case XK_Shift_R: return KEY_RIGHTSHIFT;
case XK_Alt_R: return KEY_RIGHTALT;
case XK_Super_R: return KEY_RIGHTMETA;
case XK_z: return KEY_Z;
case XK_F7: return KEY_F7;
case XK_F8: return KEY_F8;
case XK_F9: return KEY_F9;
case XK_F10: return KEY_F10;
case XK_F11: return KEY_F11;
default: return 0;
}
}
static void keyboard_event_process_input_event_data(keyboard_event *self, event_extra_data *extra_data, int fd, key_callback callback, void *userdata) {
struct input_event event;
if(read(fd, &event, sizeof(event)) != sizeof(event)) {
@@ -137,7 +184,12 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
if(event.type == EV_KEY) {
keyboard_event_process_key_state_change(self, event, extra_data, fd);
switch(event.code) {
uint32_t keycode = event.code;
const uint32_t keysym = keycode_to_keysym(self, event.code);
if(keysym)
keycode = keysym_to_keycode(keysym);
switch(keycode) {
case KEY_LEFTSHIFT:
self->lshift_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
@@ -165,21 +217,24 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
default: {
const bool shift_pressed = self->lshift_button_state == KEYBOARD_BUTTON_PRESSED || self->rshift_button_state == KEYBOARD_BUTTON_PRESSED;
const bool ctrl_pressed = self->lctrl_button_state == KEYBOARD_BUTTON_PRESSED || self->rctrl_button_state == KEYBOARD_BUTTON_PRESSED;
const bool alt_pressed = self->lalt_button_state == KEYBOARD_BUTTON_PRESSED || self->ralt_button_state == KEYBOARD_BUTTON_PRESSED;
const bool lalt_pressed = self->lalt_button_state == KEYBOARD_BUTTON_PRESSED;
const bool ralt_pressed = self->ralt_button_state == KEYBOARD_BUTTON_PRESSED;
const bool meta_pressed = self->lmeta_button_state == KEYBOARD_BUTTON_PRESSED || self->rmeta_button_state == KEYBOARD_BUTTON_PRESSED;
//fprintf(stderr, "pressed key: %d, state: %d, shift: %s, ctrl: %s, alt: %s, meta: %s\n", event.code, event.value,
//fprintf(stderr, "pressed key: %d, state: %d, shift: %s, ctrl: %s, alt: %s, meta: %s\n", keycode, event.value,
// shift_pressed ? "yes" : "no", ctrl_pressed ? "yes" : "no", alt_pressed ? "yes" : "no", meta_pressed ? "yes" : "no");
uint32_t modifiers = 0;
if(shift_pressed)
modifiers |= KEYBOARD_MODKEY_SHIFT;
if(ctrl_pressed)
modifiers |= KEYBOARD_MODKEY_CTRL;
if(alt_pressed)
modifiers |= KEYBOARD_MODKEY_ALT;
if(lalt_pressed)
modifiers |= KEYBOARD_MODKEY_LALT;
if(ralt_pressed)
modifiers |= KEYBOARD_MODKEY_RALT;
if(meta_pressed)
modifiers |= KEYBOARD_MODKEY_SUPER;
if(!callback(event.code, modifiers, event.value, userdata))
if(!callback(keycode, modifiers, event.value, userdata))
return;
break;
@@ -213,11 +268,41 @@ static bool keyboard_event_has_event_with_dev_input_fd(keyboard_event *self, int
return false;
}
/* TODO: Is there a more efficient way to do this? */
static bool dev_input_is_virtual(int dev_input_id) {
DIR *dir = opendir("/sys/devices/virtual/input");
if(!dir)
return false;
bool is_virtual = false;
char virtual_input_filepath[1024];
for(;;) {
struct dirent *entry = readdir(dir);
if(!entry)
break;
if(strncmp(entry->d_name, "input", 5) != 0)
continue;
snprintf(virtual_input_filepath, sizeof(virtual_input_filepath), "/sys/devices/virtual/input/%s/event%d", entry->d_name, dev_input_id);
if(access(virtual_input_filepath, F_OK) == 0) {
is_virtual = true;
break;
}
}
closedir(dir);
return is_virtual;
}
static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, const char *dev_input_filepath) {
const int dev_input_id = get_dev_input_id_from_filepath(dev_input_filepath);
if(dev_input_id == -1)
return false;
if(self->grab_type == KEYBOARD_GRAB_TYPE_VIRTUAL && !dev_input_is_virtual(dev_input_id))
return false;
if(keyboard_event_has_event_with_dev_input_fd(self, dev_input_id))
return false;
@@ -373,10 +458,12 @@ static int setup_virtual_keyboard_input(const char *name) {
return fd;
}
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab) {
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab, keyboard_grab_type grab_type, x11_context x_context) {
memset(self, 0, sizeof(*self));
self->stdout_event_index = -1;
self->hotplug_event_index = -1;
self->grab_type = grab_type;
self->x_context = x_context;
if(exclusive_grab) {
self->uinput_fd = setup_virtual_keyboard_input(GSR_UI_VIRTUAL_KEYBOARD_NAME);
@@ -456,7 +543,25 @@ static void on_device_added_callback(const char *devname, void *userdata) {
keyboard_event_try_add_device_if_keyboard(keyboard_ev, dev_input_filepath);
}
#define MappingNotify 34
static void keyboard_event_poll_x11_events(keyboard_event *self) {
if(!self->x_context.display || !self->x_context.XPending || !self->x_context.XNextEvent || !self->x_context.XRefreshKeyboardMapping)
return;
XEvent xev;
while(self->x_context.XPending(self->x_context.display)) {
xev.type = 0;
self->x_context.XNextEvent(self->x_context.display, &xev);
if(xev.type == MappingNotify)
self->x_context.XRefreshKeyboardMapping(xev.data);
}
}
void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds, key_callback callback, void *userdata) {
/* TODO: Add the x11 connection to the below poll? */
keyboard_event_poll_x11_events(self);
if(poll(self->event_polls, self->num_event_polls, timeout_milliseconds) <= 0)
return;

View File

@@ -17,11 +17,32 @@
#define MAX_EVENT_POLLS 32
typedef struct {
union {
int type;
unsigned char data[192];
};
} XEvent;
typedef unsigned long (*XKeycodeToKeysym_FUNC)(void *display, unsigned char keycode, int index);
typedef int (*XPending_FUNC)(void *display);
typedef int (*XNextEvent_FUNC)(void *display, XEvent *event_return);
typedef int (*XRefreshKeyboardMapping_FUNC)(void* event_map);
typedef struct {
void *display;
XKeycodeToKeysym_FUNC XKeycodeToKeysym;
XPending_FUNC XPending;
XNextEvent_FUNC XNextEvent;
XRefreshKeyboardMapping_FUNC XRefreshKeyboardMapping;
} x11_context;
typedef enum {
KEYBOARD_MODKEY_ALT = 1 << 0,
KEYBOARD_MODKEY_SUPER = 1 << 1,
KEYBOARD_MODKEY_CTRL = 1 << 2,
KEYBOARD_MODKEY_SHIFT = 1 << 3
KEYBOARD_MODKEY_LALT = 1 << 0,
KEYBOARD_MODKEY_RALT = 1 << 2,
KEYBOARD_MODKEY_SUPER = 1 << 3,
KEYBOARD_MODKEY_CTRL = 1 << 4,
KEYBOARD_MODKEY_SHIFT = 1 << 5
} keyboard_modkeys;
typedef enum {
@@ -36,6 +57,11 @@ typedef struct {
int num_keys_pressed;
} event_extra_data;
typedef enum {
KEYBOARD_GRAB_TYPE_ALL,
KEYBOARD_GRAB_TYPE_VIRTUAL
} keyboard_grab_type;
typedef struct {
struct pollfd event_polls[MAX_EVENT_POLLS]; /* Current size is |num_event_polls| */
event_extra_data event_extra_data[MAX_EVENT_POLLS]; /* Current size is |num_event_polls| */
@@ -45,6 +71,8 @@ typedef struct {
int hotplug_event_index;
int uinput_fd;
bool stdout_failed;
keyboard_grab_type grab_type;
x11_context x_context;
hotplug_event hotplug_ev;
@@ -62,7 +90,7 @@ typedef struct {
/* Return true to allow other applications to receive the key input (when using exclusive grab) */
typedef bool (*key_callback)(uint32_t key, uint32_t modifiers, int press_status, void *userdata);
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab);
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab, keyboard_grab_type grab_type, x11_context x_context);
void keyboard_event_deinit(keyboard_event *self);
/* If |timeout_milliseconds| is -1 then wait until an event is received */

View File

@@ -3,9 +3,11 @@
/* C stdlib */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
/* POSIX */
#include <unistd.h>
#include <dlfcn.h>
typedef struct {
uint32_t key;
@@ -15,12 +17,12 @@ typedef struct {
#define NUM_GLOBAL_HOTKEYS 6
static global_hotkey global_hotkeys[NUM_GLOBAL_HOTKEYS] = {
{ .key = KEY_Z, .modifiers = KEYBOARD_MODKEY_ALT, .action = "show_hide" },
{ .key = KEY_F9, .modifiers = KEYBOARD_MODKEY_ALT, .action = "record" },
{ .key = KEY_F7, .modifiers = KEYBOARD_MODKEY_ALT, .action = "pause" },
{ .key = KEY_F8, .modifiers = KEYBOARD_MODKEY_ALT, .action = "stream" },
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_ALT | KEYBOARD_MODKEY_SHIFT, .action = "replay_start" },
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_ALT, .action = "replay_save" }
{ .key = KEY_Z, .modifiers = KEYBOARD_MODKEY_LALT, .action = "show_hide" },
{ .key = KEY_F9, .modifiers = KEYBOARD_MODKEY_LALT, .action = "record" },
{ .key = KEY_F7, .modifiers = KEYBOARD_MODKEY_LALT, .action = "pause" },
{ .key = KEY_F8, .modifiers = KEYBOARD_MODKEY_LALT, .action = "stream" },
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_LALT | KEYBOARD_MODKEY_SHIFT, .action = "replay_start" },
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_LALT, .action = "replay_save" }
};
static bool on_key_callback(uint32_t key, uint32_t modifiers, int press_status, void *userdata) {
@@ -37,7 +39,104 @@ static bool on_key_callback(uint32_t key, uint32_t modifiers, int press_status,
return true;
}
int main(void) {
static void usage(void) {
fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual]\n");
fprintf(stderr, "OPTIONS:\n");
fprintf(stderr, " --all Grab all devices.\n");
fprintf(stderr, " --virtual Grab all virtual devices only.\n");
}
typedef void* (*XOpenDisplay_FUNC)(const char*);
typedef int (*XErrorHandler_FUNC)(void *display, void* error_event);
typedef XErrorHandler_FUNC (*XSetErrorHandler_FUNC)(XErrorHandler_FUNC handler);
static int x_ignore_error(void *display, void *ee) {
(void)display;
(void)ee;
return 0;
}
static x11_context setup_x11_context(void) {
x11_context x_context = {0};
XSetErrorHandler_FUNC XSetErrorHandler = NULL;
void *x11_lib = dlopen("libX11.so.6", RTLD_LAZY);
if(!x11_lib) {
fprintf(stderr, "Warning: dlopen libX11.so.6 failed\n");
return x_context;
}
XOpenDisplay_FUNC XOpenDisplay = dlsym(x11_lib, "XOpenDisplay");
if(!XOpenDisplay) {
fprintf(stderr, "Warning: dlsym XOpenDisplay failed\n");
goto fail;
}
x_context.XKeycodeToKeysym = dlsym(x11_lib, "XKeycodeToKeysym");
if(!x_context.XKeycodeToKeysym) {
fprintf(stderr, "Warning: dlsym XKeycodeToKeysym failed\n");
goto fail;
}
x_context.XPending = dlsym(x11_lib, "XPending");
if(!x_context.XPending) {
fprintf(stderr, "Warning: dlsym XPending failed\n");
goto fail;
}
x_context.XNextEvent = dlsym(x11_lib, "XNextEvent");
if(!x_context.XNextEvent) {
fprintf(stderr, "Warning: dlsym XNextEvent failed\n");
goto fail;
}
x_context.XRefreshKeyboardMapping = dlsym(x11_lib, "XRefreshKeyboardMapping");
if(!x_context.XRefreshKeyboardMapping) {
fprintf(stderr, "Warning: dlsym XRefreshKeyboardMapping failed\n");
goto fail;
}
x_context.display = XOpenDisplay(NULL);
if(!x_context.display) {
fprintf(stderr, "Warning: XOpenDisplay failed\n");
goto fail;
}
XSetErrorHandler = dlsym(x11_lib, "XSetErrorHandler");
if(XSetErrorHandler)
XSetErrorHandler(x_ignore_error);
else
fprintf(stderr, "Warning: dlsym XSetErrorHandler failed\n");
return x_context;
fail:
memset(&x_context, 0, sizeof(x_context));
dlclose(x11_lib);
return x_context;
}
int main(int argc, char **argv) {
keyboard_grab_type grab_type = KEYBOARD_GRAB_TYPE_ALL;
if(argc == 2) {
const char *grab_type_arg = argv[1];
if(strcmp(grab_type_arg, "--all") == 0) {
grab_type = KEYBOARD_GRAB_TYPE_ALL;
} else if(strcmp(grab_type_arg, "--virtual") == 0) {
grab_type = KEYBOARD_GRAB_TYPE_VIRTUAL;
} else {
fprintf(stderr, "Error: expected --all or --virtual, got %s\n", grab_type_arg);
usage();
return 1;
}
} else if(argc != 1) {
fprintf(stderr, "Error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
usage();
return 1;
}
x11_context x_context = setup_x11_context();
const uid_t user_id = getuid();
if(geteuid() != 0) {
if(setuid(0) == -1) {
@@ -47,7 +146,7 @@ int main(void) {
}
keyboard_event keyboard_ev;
if(!keyboard_event_init(&keyboard_ev, true, true)) {
if(!keyboard_event_init(&keyboard_ev, true, true, grab_type, x_context)) {
fprintf(stderr, "Error: failed to setup hotplugging and no keyboard input devices were found\n");
setuid(user_id);
return 1;

105
tools/gsr-ui-cli/main.c Normal file
View File

@@ -0,0 +1,105 @@
#include <limits.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {
char dir[PATH_MAX];
const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
if(runtime_dir)
snprintf(dir, sizeof(dir), "%s", runtime_dir);
else
snprintf(dir, sizeof(dir), "/run/user/%d", geteuid());
if(access(dir, F_OK) != 0)
snprintf(dir, sizeof(dir), "/tmp");
snprintf(buffer, buffer_size, "%s/%s", dir, filename);
}
/* Assumes |str| size is less than 256 */
static void fifo_write_all(int file_fd, const char *str) {
char command[256];
const ssize_t command_size = snprintf(command, sizeof(command), "%s\n", str);
if(command_size >= (ssize_t)sizeof(command)) {
fprintf(stderr, "Error: command too long: %s\n", str);
return;
}
ssize_t offset = 0;
while(offset < (ssize_t)command_size) {
const ssize_t bytes_written = write(file_fd, str + offset, command_size - offset);
if(bytes_written > 0)
offset += bytes_written;
}
}
static void usage(void) {
printf("usage: gsr-ui-cli <command>\n");
printf("Run commands on the running gsr-ui instance.\n");
printf("\n");
printf("COMMANDS:\n");
printf(" toggle-show Show/hide the UI.\n");
printf(" toggle-record Start/stop recording.\n");
printf(" toggle-pause Pause/unpause recording. Only applies to regular recording.\n");
printf(" toggle-stream Start/stop streaming.\n");
printf(" toggle-replay Start/stop replay.\n");
printf(" replay-save Save replay.\n");
printf("\n");
printf("EXAMPLES:\n");
printf(" gsr-ui-cli toggle-show\n");
printf(" gsr-ui-cli toggle-record\n");
exit(1);
}
static bool is_valid_command(const char *command) {
const char *commands[] = {
"toggle-show",
"toggle-record",
"toggle-pause",
"toggle-stream",
"toggle-replay",
"replay-save",
NULL
};
for(int i = 0; commands[i]; ++i) {
if(strcmp(command, commands[i]) == 0)
return true;
}
return false;
}
int main(int argc, char **argv) {
if(argc != 2) {
printf("Error: expected 1 argument, %d provided\n", argc - 1);
usage();
}
const char *command = argv[1];
if(strcmp(command, "-h") == 0 || strcmp(command, "--help") == 0)
usage();
if(!is_valid_command(command)) {
fprintf(stderr, "Error: invalid command: \"%s\"\n", command);
usage();
}
char fifo_filepath[PATH_MAX];
get_runtime_filepath(fifo_filepath, sizeof(fifo_filepath), "gsr-ui");
const int fifo_fd = open(fifo_filepath, O_RDWR | O_NONBLOCK);
if(fifo_fd <= 0) {
fprintf(stderr, "Error: failed to open fifo file %s. Maybe gsr-ui is not running?\n", fifo_filepath);
exit(2);
}
fifo_write_all(fifo_fd, command);
close(fifo_fd);
return 0;
}