mirror of
https://repo.dec05eba.com/gpu-screen-recorder-ui
synced 2026-04-08 12:24:52 +09:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eab194c5f | ||
|
|
abeaf5cb61 | ||
|
|
575592a12d | ||
|
|
d72ce588fb | ||
|
|
7d2f2e9b47 | ||
|
|
636150ef08 | ||
|
|
612fe6a9c2 | ||
|
|
57448f6579 | ||
|
|
4d7526d21e |
@@ -61,8 +61,8 @@ I'm looking for somebody that can create sound effects for the notifications.
|
||||

|
||||
|
||||
# Known issues
|
||||
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
|
||||
* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland.
|
||||
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay. Change your waybar dock mode to "dock" in its config to fix this.
|
||||
* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland. Change your waybar dock mode to "dock" in its config to fix this.
|
||||
* The background of the UI is black when opening the UI while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
|
||||
* Unable to close the region selection with escape key while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
|
||||
|
||||
|
||||
20
TODO
20
TODO
@@ -12,8 +12,6 @@ Handle events in draw function because the render position of elements is availa
|
||||
|
||||
Add nvidia overclock option.
|
||||
|
||||
Add support for window selection in capture.
|
||||
|
||||
Filechooser should have the option to select list view, search bar and common folders/mounted drives on the left side for quick navigation. Also a button to create a new directory.
|
||||
|
||||
Restart replay on system start if monitor resolution changes.
|
||||
@@ -70,8 +68,6 @@ Run `systemctl status --user gpu-screen-recorder` when starting recording and gi
|
||||
|
||||
Add option to select which gpu to record with, or list all monitors and automatically use the gpu associated with the monitor. Do the same in gtk application.
|
||||
|
||||
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.
|
||||
|
||||
Support CJK.
|
||||
@@ -96,9 +92,6 @@ Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change Ex
|
||||
|
||||
When enabling X11 global hotkey again only grab lalt, not ralt.
|
||||
|
||||
When adding window capture only add it to recording and streaming and do the window selection when recording starts, to make it more ergonomic with hotkeys.
|
||||
If hotkey for recording/streaming start is pressed on the button for start is clicked then hide the ui if it's visible and show the window selection option (cursor).
|
||||
|
||||
Show an error that prime run will be disabled when using desktop portal capture option. This can cause issues as the user may have selected a video codec option that isn't available on their iGPU but is available on the prime-run dGPU.
|
||||
|
||||
For keyboards that report supporting mice the keyboard grab will be delayed until any key has been pressed (and then released), see: https://github.com/dec05eba/gpu-screen-recorder-issues/issues/97
|
||||
@@ -116,9 +109,6 @@ When clicking on current directory in file manager show a dropdown menu where yo
|
||||
|
||||
Maybe change gsr-ui startup retry time in the systemd service, from 5 seconds to 2 seconds.
|
||||
|
||||
Add support for window capture. This should not prompt for window selection directly but instead prompt for window selection when recording starts and hide the ui first.
|
||||
For screenshots window capture should exist but "follow focused" option should not exist.
|
||||
|
||||
Make it possible to take a screenshot through a button in the ui instead of having to use hotkey.
|
||||
|
||||
Handle failing to save a replay. gsr should output "failed to save replay, or something like that" to make it possible to detect that.
|
||||
@@ -139,8 +129,6 @@ Make inactive buttons gray (in dropdown boxes and in the front page with save, e
|
||||
|
||||
Add option to do screen-direct recording. But make it clear that it should not be used, except for gsync on x11 nvidia.
|
||||
|
||||
Add window capture option (for x11).
|
||||
|
||||
Add systray for recording status.
|
||||
|
||||
Add a desktop icon when gsr-ui has a window mode option (which should be the default launch option).
|
||||
@@ -191,3 +179,11 @@ Show message that replay/streaming has to be restarted if recording settings are
|
||||
Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
|
||||
|
||||
Support freetype for text rendering. Maybe load freetype as runtime (with dlopen) and use that when available and fallback to stb_freetype if not available.
|
||||
|
||||
Show .webm container option. It's currently chosen automatically if vp8/vp9 is chosen. The available containers should automatically switch depending on the video codec.
|
||||
|
||||
In settings show audio levels for each audio. Maybe show audio level image beside the audio name in the dropdown box and switch to a different image (have 3-4 different images for each level) depending on the volume.
|
||||
|
||||
Only use fake cursor on wayland if the focused x11 window is fullscreen.
|
||||
|
||||
Create window as a real overlay window, using layer shell protocol, when possible. This will however minimize windows on floating wms. Check if this can be fixed somehow, or only use layer shell in tiling wms.
|
||||
@@ -45,7 +45,7 @@ namespace gsr {
|
||||
int32_t video_width = 0;
|
||||
int32_t video_height = 0;
|
||||
int32_t fps = 60;
|
||||
int32_t video_bitrate = 15000;
|
||||
int32_t video_bitrate = 8000;
|
||||
bool merge_audio_tracks = true; // TODO: Remove in the future
|
||||
bool application_audio_invert = false; // TODO: Remove in the future
|
||||
bool change_video_resolution = false;
|
||||
|
||||
@@ -56,6 +56,8 @@ namespace gsr {
|
||||
bool down_pressed = false;
|
||||
bool left_pressed = false;
|
||||
bool right_pressed = false;
|
||||
bool l3_button_pressed = false;
|
||||
bool r3_button_pressed = false;
|
||||
|
||||
bool save_replay = false;
|
||||
bool save_1_min_replay = false;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "GlobalHotkeys/GlobalHotkeysJoystick.hpp"
|
||||
#include "AudioPlayer.hpp"
|
||||
#include "RegionSelector.hpp"
|
||||
#include "WindowSelector.hpp"
|
||||
#include "CursorTracker/CursorTracker.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
@@ -116,10 +117,10 @@ namespace gsr {
|
||||
void on_press_save_replay();
|
||||
void on_press_save_replay_1_min_replay();
|
||||
void on_press_save_replay_10_min_replay();
|
||||
bool on_press_start_replay(bool disable_notification, bool finished_region_selection);
|
||||
void on_press_start_record(bool finished_region_selection);
|
||||
void on_press_start_stream(bool finished_region_selection);
|
||||
void on_press_take_screenshot(bool finished_region_selection, bool force_region_capture);
|
||||
bool on_press_start_replay(bool disable_notification, bool finished_selection);
|
||||
void on_press_start_record(bool finished_selection);
|
||||
void on_press_start_stream(bool finished_selection);
|
||||
void on_press_take_screenshot(bool finished_selection, bool force_region_capture);
|
||||
bool update_compositor_texture(const Monitor &monitor);
|
||||
|
||||
std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);
|
||||
@@ -213,10 +214,15 @@ namespace gsr {
|
||||
int replay_save_duration_min = 0;
|
||||
|
||||
AudioPlayer audio_player;
|
||||
|
||||
RegionSelector region_selector;
|
||||
bool start_region_capture = false;
|
||||
std::function<void()> on_region_selected;
|
||||
|
||||
WindowSelector window_selector;
|
||||
bool start_window_capture = false;
|
||||
std::function<void()> on_window_selected;
|
||||
|
||||
std::string recording_capture_target;
|
||||
std::string screenshot_capture_target;
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace gsr {
|
||||
|
||||
bool failed() const;
|
||||
bool poll_events();
|
||||
bool is_selected() const;
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Region get_selection() const;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace gsr {
|
||||
void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func);
|
||||
bool starts_with(std::string_view str, const char *substr);
|
||||
bool ends_with(std::string_view str, const char *substr);
|
||||
std::string strip(const std::string &str);
|
||||
|
||||
std::string get_home_dir();
|
||||
std::string get_config_dir();
|
||||
|
||||
33
include/WindowSelector.hpp
Normal file
33
include/WindowSelector.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class WindowSelector {
|
||||
public:
|
||||
WindowSelector();
|
||||
WindowSelector(const WindowSelector&) = delete;
|
||||
WindowSelector& operator=(const WindowSelector&) = delete;
|
||||
~WindowSelector();
|
||||
|
||||
bool start(mgl::Color border_color);
|
||||
void stop();
|
||||
bool is_started() const;
|
||||
|
||||
bool failed() const;
|
||||
bool poll_events();
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Window get_selection() const;
|
||||
private:
|
||||
Display *dpy = nullptr;
|
||||
Cursor crosshair_cursor = None;
|
||||
Colormap border_window_colormap = None;
|
||||
Window border_window = None;
|
||||
Window selected_window = None;
|
||||
bool selected = false;
|
||||
bool canceled = false;
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ namespace gsr {
|
||||
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window);
|
||||
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window);
|
||||
void set_window_size_not_resizable(Display *dpy, Window window, int width, int height);
|
||||
Window window_get_target_window_child(Display *display, Window window);
|
||||
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);
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace gsr {
|
||||
private:
|
||||
std::unique_ptr<ComboBox> create_record_area_box();
|
||||
std::unique_ptr<Widget> create_record_area();
|
||||
std::unique_ptr<List> create_select_window();
|
||||
std::unique_ptr<Entry> create_image_width_entry();
|
||||
std::unique_ptr<Entry> create_image_height_entry();
|
||||
std::unique_ptr<List> create_image_resolution();
|
||||
@@ -56,7 +55,6 @@ namespace gsr {
|
||||
|
||||
GsrPage *content_page_ptr = nullptr;
|
||||
ScrollablePage *settings_scrollable_page_ptr = nullptr;
|
||||
List *select_window_list_ptr = nullptr;
|
||||
List *image_resolution_list_ptr = nullptr;
|
||||
List *restore_portal_session_list_ptr = nullptr;
|
||||
List *color_range_list_ptr = nullptr;
|
||||
|
||||
@@ -46,7 +46,6 @@ namespace gsr {
|
||||
std::unique_ptr<RadioButton> create_view_radio_button();
|
||||
std::unique_ptr<ComboBox> create_record_area_box();
|
||||
std::unique_ptr<Widget> create_record_area();
|
||||
std::unique_ptr<List> create_select_window();
|
||||
std::unique_ptr<Entry> create_area_width_entry();
|
||||
std::unique_ptr<Entry> create_area_height_entry();
|
||||
std::unique_ptr<List> create_area_size();
|
||||
@@ -147,7 +146,6 @@ namespace gsr {
|
||||
GsrPage *content_page_ptr = nullptr;
|
||||
ScrollablePage *settings_scrollable_page_ptr = nullptr;
|
||||
List *settings_list_ptr = nullptr;
|
||||
List *select_window_list_ptr = nullptr;
|
||||
List *area_size_list_ptr = nullptr;
|
||||
List *video_resolution_list_ptr = nullptr;
|
||||
List *restore_portal_session_list_ptr = nullptr;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.6.5', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.6.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
|
||||
if get_option('buildtype') == 'debug'
|
||||
add_project_arguments('-g3', language : ['c', 'cpp'])
|
||||
@@ -40,6 +40,7 @@ src = [
|
||||
'src/Utils.cpp',
|
||||
'src/WindowUtils.cpp',
|
||||
'src/RegionSelector.cpp',
|
||||
'src/WindowSelector.cpp',
|
||||
'src/Config.cpp',
|
||||
'src/GsrInfo.cpp',
|
||||
'src/Process.cpp',
|
||||
@@ -61,7 +62,7 @@ datadir = get_option('datadir')
|
||||
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
|
||||
|
||||
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
|
||||
add_project_arguments('-DGSR_FLATPAK_VERSION="5.6.0"', language: ['c', 'cpp'])
|
||||
add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.0"', language: ['c', 'cpp'])
|
||||
|
||||
executable(
|
||||
meson.project_name(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gsr-ui"
|
||||
type = "executable"
|
||||
version = "1.6.5"
|
||||
version = "1.6.7"
|
||||
platforms = ["posix"]
|
||||
|
||||
[lang.cpp]
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace gsr {
|
||||
|
||||
streaming_config.record_options.video_quality = "custom";
|
||||
streaming_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
streaming_config.record_options.video_bitrate = 15000;
|
||||
streaming_config.record_options.video_bitrate = 8000;
|
||||
|
||||
record_config.save_directory = default_videos_save_directory;
|
||||
record_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace gsr {
|
||||
static constexpr int triangle_button = 2;
|
||||
static constexpr int options_button = 9;
|
||||
static constexpr int playstation_button = 10;
|
||||
static constexpr int l3_button = 11;
|
||||
static constexpr int r3_button = 12;
|
||||
static constexpr int axis_up_down = 7;
|
||||
static constexpr int axis_left_right = 6;
|
||||
|
||||
@@ -266,7 +268,8 @@ namespace gsr {
|
||||
if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) {
|
||||
switch(event.number) {
|
||||
case playstation_button: {
|
||||
playstation_button_pressed = event.value == button_pressed;
|
||||
// Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time
|
||||
playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed;
|
||||
break;
|
||||
}
|
||||
case options_button: {
|
||||
@@ -284,6 +287,14 @@ namespace gsr {
|
||||
save_10_min_replay = true;
|
||||
break;
|
||||
}
|
||||
case l3_button: {
|
||||
l3_button_pressed = event.value == button_pressed;
|
||||
break;
|
||||
}
|
||||
case r3_button: {
|
||||
r3_button_pressed = event.value == button_pressed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) {
|
||||
const int trigger_threshold = 16383;
|
||||
|
||||
325
src/Overlay.cpp
325
src/Overlay.cpp
@@ -26,6 +26,7 @@
|
||||
#include <malloc.h>
|
||||
#include <stdexcept>
|
||||
#include <algorithm>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
@@ -272,6 +273,33 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_hyprland_waybar_running_as_dock() {
|
||||
const char *args[] = { "hyprctl", "layers", nullptr };
|
||||
std::string stdout_str;
|
||||
if(exec_program_on_host_get_stdout(args, stdout_str) != 0)
|
||||
return false;
|
||||
|
||||
int waybar_layer_level = -1;
|
||||
int current_layer_level = 0;
|
||||
string_split_char(stdout_str, '\n', [&](const std::string_view line) {
|
||||
if(line.find("Layer level 0") != std::string_view::npos)
|
||||
current_layer_level = 0;
|
||||
else if(line.find("Layer level 1") != std::string_view::npos)
|
||||
current_layer_level = 1;
|
||||
else if(line.find("Layer level 2") != std::string_view::npos)
|
||||
current_layer_level = 2;
|
||||
else if(line.find("Layer level 3") != std::string_view::npos)
|
||||
current_layer_level = 3;
|
||||
else if(line.find("namespace: waybar") != std::string_view::npos) {
|
||||
waybar_layer_level = current_layer_level;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return waybar_layer_level >= 0 && waybar_layer_level <= 1;
|
||||
}
|
||||
|
||||
static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) {
|
||||
return {
|
||||
(uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key),
|
||||
@@ -678,6 +706,22 @@ namespace gsr {
|
||||
on_region_selected = nullptr;
|
||||
}
|
||||
|
||||
window_selector.poll_events();
|
||||
if(window_selector.take_canceled()) {
|
||||
on_window_selected = nullptr;
|
||||
} else if(window_selector.take_selection() && on_window_selected) {
|
||||
mgl_context *context = mgl_get_context();
|
||||
Display *display = (Display*)context->connection;
|
||||
|
||||
const Window selected_window = window_selector.get_selection();
|
||||
if(selected_window && selected_window != DefaultRootWindow(display)) {
|
||||
on_window_selected();
|
||||
} else {
|
||||
show_notification("No window selected", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
|
||||
}
|
||||
on_window_selected = nullptr;
|
||||
}
|
||||
|
||||
if(!visible || !window)
|
||||
return;
|
||||
|
||||
@@ -723,7 +767,16 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
if(region_selector.is_started()) {
|
||||
if(start_window_capture) {
|
||||
start_window_capture = false;
|
||||
hide();
|
||||
if(!window_selector.start(get_color_theme().tint_color)) {
|
||||
show_notification("Failed to start window capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
|
||||
on_window_selected = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if(region_selector.is_started() || window_selector.is_started()) {
|
||||
usleep(5 * 1000); // 5 ms
|
||||
return true;
|
||||
}
|
||||
@@ -857,7 +910,7 @@ namespace gsr {
|
||||
if(visible)
|
||||
return;
|
||||
|
||||
if(region_selector.is_started())
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
drawn_first_frame = false;
|
||||
@@ -878,6 +931,8 @@ namespace gsr {
|
||||
const std::string wm_name = get_window_manager_name(display);
|
||||
const bool is_kwin = wm_name == "KWin";
|
||||
const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
|
||||
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
|
||||
const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock();
|
||||
|
||||
std::optional<CursorInfo> cursor_info;
|
||||
if(cursor_tracker) {
|
||||
@@ -905,7 +960,7 @@ namespace gsr {
|
||||
// If the focused window is a wayland application then don't use override redirect and instead create
|
||||
// a fullscreen window for the ui.
|
||||
// TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor))
|
||||
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots;
|
||||
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland;
|
||||
|
||||
if(prevent_game_minimizing) {
|
||||
window_pos = focused_monitor->position;
|
||||
@@ -1009,7 +1064,7 @@ namespace gsr {
|
||||
// Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
|
||||
xi_grab_all_mouse_devices(xi_display);
|
||||
|
||||
if(!is_wlroots)
|
||||
if(!is_wlroots && !hyprland_waybar_is_dock)
|
||||
window->set_fullscreen(true);
|
||||
|
||||
visible = true;
|
||||
@@ -1313,6 +1368,7 @@ namespace gsr {
|
||||
visible = false;
|
||||
drawn_first_frame = false;
|
||||
start_region_capture = false;
|
||||
start_window_capture = false;
|
||||
|
||||
if(xi_input_xev) {
|
||||
free(xi_input_xev);
|
||||
@@ -1435,6 +1491,24 @@ namespace gsr {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void truncate_string(std::string &str, int max_length) {
|
||||
int index = 0;
|
||||
size_t byte_index = 0;
|
||||
|
||||
while(index < max_length && byte_index < str.size()) {
|
||||
uint32_t codepoint = 0;
|
||||
size_t codepoint_length = 0;
|
||||
mgl::utf8_decode((const unsigned char*)str.c_str() + byte_index, str.size() - byte_index, &codepoint, &codepoint_length);
|
||||
if(codepoint_length == 0)
|
||||
codepoint_length = 1;
|
||||
|
||||
index += 1;
|
||||
byte_index += codepoint_length;
|
||||
}
|
||||
|
||||
str.erase(byte_index);
|
||||
}
|
||||
|
||||
static bool is_hex_num(char c) {
|
||||
return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
|
||||
}
|
||||
@@ -1462,8 +1536,44 @@ namespace gsr {
|
||||
return is_hex && !hex_start;
|
||||
}
|
||||
|
||||
static bool is_number(const char *str) {
|
||||
const char *p = str;
|
||||
while(*p) {
|
||||
char c = *p;
|
||||
if(c < '0' || c > '9')
|
||||
return false;
|
||||
++p;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_capture_target_monitor(const char *capture_target) {
|
||||
return strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target);
|
||||
return strcmp(capture_target, "window") != 0 && strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target);
|
||||
}
|
||||
|
||||
static std::string capture_target_get_notification_name(const char *capture_target) {
|
||||
std::string result;
|
||||
if(is_capture_target_monitor(capture_target)) {
|
||||
result = "this monitor";
|
||||
} else if(is_number(capture_target)) {
|
||||
mgl_context *context = mgl_get_context();
|
||||
Display *display = (Display*)context->connection;
|
||||
|
||||
int64_t window_id = None;
|
||||
sscanf(capture_target, "%" PRIi64, &window_id);
|
||||
|
||||
const std::optional<std::string> window_title = get_window_title(display, window_id);
|
||||
if(window_title) {
|
||||
result = strip(window_title.value());
|
||||
truncate_string(result, 20);
|
||||
result = "window \"" + result + "\"";
|
||||
} else {
|
||||
result = std::string("window ") + capture_target;
|
||||
}
|
||||
} else {
|
||||
result = capture_target;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string get_valid_monitor_x11(const std::string &target_monitor_name, const std::vector<Monitor> &monitors) {
|
||||
@@ -1642,24 +1752,6 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
static void truncate_string(std::string &str, int max_length) {
|
||||
int index = 0;
|
||||
size_t byte_index = 0;
|
||||
|
||||
while(index < max_length && byte_index < str.size()) {
|
||||
uint32_t codepoint = 0;
|
||||
size_t codepoint_length = 0;
|
||||
mgl::utf8_decode((const unsigned char*)str.c_str() + byte_index, str.size() - byte_index, &codepoint, &codepoint_length);
|
||||
if(codepoint_length == 0)
|
||||
codepoint_length = 1;
|
||||
|
||||
index += 1;
|
||||
byte_index += codepoint_length;
|
||||
}
|
||||
|
||||
str.erase(byte_index);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1689,11 +1781,7 @@ namespace gsr {
|
||||
if(!config.record_config.show_video_saved_notifications)
|
||||
return;
|
||||
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of this monitor to \"%s\"", focused_window_name.c_str());
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", recording_capture_target.c_str(), focused_window_name.c_str());
|
||||
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str());
|
||||
capture_target = recording_capture_target.c_str();
|
||||
break;
|
||||
}
|
||||
@@ -1707,11 +1795,7 @@ namespace gsr {
|
||||
else
|
||||
snprintf(duration, sizeof(duration), " ");
|
||||
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of this monitor to \"%s\"", duration, focused_window_name.c_str());
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, recording_capture_target.c_str(), focused_window_name.c_str());
|
||||
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str());
|
||||
capture_target = recording_capture_target.c_str();
|
||||
break;
|
||||
}
|
||||
@@ -1719,11 +1803,7 @@ namespace gsr {
|
||||
if(!config.screenshot_config.show_screenshot_saved_notifications)
|
||||
return;
|
||||
|
||||
if(is_capture_target_monitor(screenshot_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of this monitor to \"%s\"", focused_window_name.c_str());
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", screenshot_capture_target.c_str(), focused_window_name.c_str());
|
||||
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str(), focused_window_name.c_str());
|
||||
capture_target = screenshot_capture_target.c_str();
|
||||
break;
|
||||
}
|
||||
@@ -1756,10 +1836,7 @@ namespace gsr {
|
||||
snprintf(duration, sizeof(duration), " ");
|
||||
|
||||
char msg[512];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of this monitor", duration);
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
|
||||
}
|
||||
}
|
||||
@@ -1880,10 +1957,7 @@ namespace gsr {
|
||||
save_video_in_current_game_directory(screenshot_filepath.c_str(), NotificationType::SCREENSHOT);
|
||||
} else if(config.screenshot_config.show_screenshot_saved_notifications) {
|
||||
char msg[512];
|
||||
if(is_capture_target_monitor(screenshot_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s", screenshot_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
|
||||
}
|
||||
} else {
|
||||
@@ -1982,10 +2056,7 @@ namespace gsr {
|
||||
save_video_in_current_game_directory(video_filepath.c_str(), NotificationType::RECORD);
|
||||
} else if(config.record_config.show_video_saved_notifications) {
|
||||
char msg[512];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of %s", recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
|
||||
}
|
||||
} else {
|
||||
@@ -2178,11 +2249,12 @@ namespace gsr {
|
||||
}
|
||||
|
||||
static bool validate_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
|
||||
// TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number)
|
||||
if(capture_target == "region") {
|
||||
return capture_options.region;
|
||||
if(capture_target == "window") {
|
||||
return capture_options.window;
|
||||
} else if(capture_target == "focused") {
|
||||
return capture_options.focused;
|
||||
} else if(capture_target == "region") {
|
||||
return capture_options.region;
|
||||
} else if(capture_target == "portal") {
|
||||
return capture_options.portal;
|
||||
} else if(capture_target == "focused_monitor") {
|
||||
@@ -2214,7 +2286,9 @@ namespace gsr {
|
||||
}
|
||||
|
||||
std::string Overlay::get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
|
||||
if(capture_target == "focused_monitor") {
|
||||
if(capture_target == "window") {
|
||||
return std::to_string(window_selector.get_selection());
|
||||
} else if(capture_target == "focused_monitor") {
|
||||
std::optional<CursorInfo> cursor_info;
|
||||
if(cursor_tracker) {
|
||||
cursor_tracker->update();
|
||||
@@ -2283,8 +2357,50 @@ namespace gsr {
|
||||
kill(gpu_screen_recorder_process, SIGRTMIN+5);
|
||||
}
|
||||
|
||||
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_region_selection) {
|
||||
if(region_selector.is_started())
|
||||
static const char* switch_video_codec_to_usable_hardware_encoder(const GsrInfo &gsr_info) {
|
||||
if(gsr_info.supported_video_codecs.h264)
|
||||
return "h264";
|
||||
else if(gsr_info.supported_video_codecs.hevc)
|
||||
return "hevc";
|
||||
else if(gsr_info.supported_video_codecs.av1)
|
||||
return "av1";
|
||||
else if(gsr_info.supported_video_codecs.vp8)
|
||||
return "vp8";
|
||||
else if(gsr_info.supported_video_codecs.vp9)
|
||||
return "vp9";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* change_container_if_codec_not_supported(const char *video_codec, const char *container) {
|
||||
if(strcmp(video_codec, "vp8") == 0 || strcmp(video_codec, "vp9") == 0) {
|
||||
if(strcmp(container, "webm") != 0 && strcmp(container, "matroska") != 0) {
|
||||
fprintf(stderr, "Warning: container '%s' is not compatible with video codec '%s', using webm container instead\n", container, video_codec);
|
||||
return "webm";
|
||||
}
|
||||
} else if(strcmp(container, "webm") == 0) {
|
||||
fprintf(stderr, "Warning: container webm is not compatible with video codec '%s', using mp4 container instead\n", video_codec);
|
||||
return "mp4";
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
static void choose_video_codec_and_container_with_fallback(const GsrInfo &gsr_info, const char **video_codec, const char **container, const char **encoder) {
|
||||
*encoder = "gpu";
|
||||
if(strcmp(*video_codec, "h264_software") == 0) {
|
||||
*video_codec = "h264";
|
||||
*encoder = "cpu";
|
||||
} else if(strcmp(*video_codec, "auto") == 0) {
|
||||
*video_codec = switch_video_codec_to_usable_hardware_encoder(gsr_info);
|
||||
if(!*video_codec) {
|
||||
*video_codec = "h264";
|
||||
*encoder = "cpu";
|
||||
}
|
||||
}
|
||||
*container = change_container_if_codec_not_supported(*video_codec, *container);
|
||||
}
|
||||
|
||||
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return false;
|
||||
|
||||
switch(recording_status) {
|
||||
@@ -2334,7 +2450,7 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config.replay_config.record_options.record_area_option == "region" && !finished_region_selection) {
|
||||
if(config.replay_config.record_options.record_area_option == "region" && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [disable_notification, this]() {
|
||||
on_press_start_replay(disable_notification, true);
|
||||
@@ -2342,6 +2458,14 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config.replay_config.record_options.record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [disable_notification, this]() {
|
||||
on_press_start_replay(disable_notification, true);
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Validate input, fallback to valid values
|
||||
const std::string fps = std::to_string(config.replay_config.record_options.fps);
|
||||
const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate);
|
||||
@@ -2349,12 +2473,10 @@ namespace gsr {
|
||||
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info);
|
||||
const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode;
|
||||
const std::string replay_time = std::to_string(config.replay_config.replay_time);
|
||||
const char *container = config.replay_config.container.c_str();
|
||||
const char *video_codec = config.replay_config.record_options.video_codec.c_str();
|
||||
const char *encoder = "gpu";
|
||||
if(strcmp(video_codec, "h264_software") == 0) {
|
||||
video_codec = "h264";
|
||||
encoder = "cpu";
|
||||
}
|
||||
choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
|
||||
|
||||
char size[64];
|
||||
size[0] = '\0';
|
||||
@@ -2366,7 +2488,7 @@ namespace gsr {
|
||||
|
||||
std::vector<const char*> args = {
|
||||
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
|
||||
"-c", config.replay_config.container.c_str(),
|
||||
"-c", container,
|
||||
"-ac", config.replay_config.record_options.audio_codec.c_str(),
|
||||
"-cursor", config.replay_config.record_options.record_cursor ? "yes" : "no",
|
||||
"-cr", config.replay_config.record_options.color_range.c_str(),
|
||||
@@ -2421,18 +2543,15 @@ namespace gsr {
|
||||
// to see when the program has exit.
|
||||
if(!disable_notification && config.replay_config.show_replay_started_notifications) {
|
||||
char msg[256];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Started replaying this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Started replaying %s", recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Overlay::on_press_start_record(bool finished_region_selection) {
|
||||
if(region_selector.is_started())
|
||||
void Overlay::on_press_start_record(bool finished_selection) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
switch(recording_status) {
|
||||
@@ -2508,7 +2627,7 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.record_config.record_options.record_area_option == "region" && !finished_region_selection) {
|
||||
if(config.record_config.record_options.record_area_option == "region" && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [this]() {
|
||||
on_press_start_record(true);
|
||||
@@ -2516,6 +2635,14 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.record_config.record_options.record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [this]() {
|
||||
on_press_start_record(true);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
record_filepath.clear();
|
||||
|
||||
// TODO: Validate input, fallback to valid values
|
||||
@@ -2524,12 +2651,10 @@ namespace gsr {
|
||||
const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str());
|
||||
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info);
|
||||
const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode;
|
||||
const char *container = config.record_config.container.c_str();
|
||||
const char *video_codec = config.record_config.record_options.video_codec.c_str();
|
||||
const char *encoder = "gpu";
|
||||
if(strcmp(video_codec, "h264_software") == 0) {
|
||||
video_codec = "h264";
|
||||
encoder = "cpu";
|
||||
}
|
||||
choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
|
||||
|
||||
char size[64];
|
||||
size[0] = '\0';
|
||||
@@ -2541,7 +2666,7 @@ namespace gsr {
|
||||
|
||||
std::vector<const char*> args = {
|
||||
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
|
||||
"-c", config.record_config.container.c_str(),
|
||||
"-c", container,
|
||||
"-ac", config.record_config.record_options.audio_codec.c_str(),
|
||||
"-cursor", config.record_config.record_options.record_cursor ? "yes" : "no",
|
||||
"-cr", config.record_config.record_options.color_range.c_str(),
|
||||
@@ -2578,10 +2703,7 @@ namespace gsr {
|
||||
// 1...
|
||||
if(config.record_config.show_recording_started_notifications) {
|
||||
char msg[256];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Started recording this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Started recording %s", recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
|
||||
}
|
||||
}
|
||||
@@ -2621,8 +2743,8 @@ namespace gsr {
|
||||
return url;
|
||||
}
|
||||
|
||||
void Overlay::on_press_start_stream(bool finished_region_selection) {
|
||||
if(region_selector.is_started())
|
||||
void Overlay::on_press_start_stream(bool finished_selection) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
switch(recording_status) {
|
||||
@@ -2668,7 +2790,7 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.streaming_config.record_options.record_area_option == "region" && !finished_region_selection) {
|
||||
if(config.streaming_config.record_options.record_area_option == "region" && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [this]() {
|
||||
on_press_start_stream(true);
|
||||
@@ -2676,6 +2798,14 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.streaming_config.record_options.record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [this]() {
|
||||
on_press_start_stream(true);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Validate input, fallback to valid values
|
||||
const std::string fps = std::to_string(config.streaming_config.record_options.fps);
|
||||
const std::string video_bitrate = std::to_string(config.streaming_config.record_options.video_bitrate);
|
||||
@@ -2685,16 +2815,12 @@ namespace gsr {
|
||||
if(audio_tracks.size() > 1)
|
||||
audio_tracks.resize(1);
|
||||
const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode;
|
||||
const char *container = "flv";
|
||||
if(config.streaming_config.streaming_service == "custom")
|
||||
container = config.streaming_config.custom.container.c_str();
|
||||
const char *video_codec = config.streaming_config.record_options.video_codec.c_str();
|
||||
const char *encoder = "gpu";
|
||||
if(strcmp(video_codec, "h264_software") == 0) {
|
||||
video_codec = "h264";
|
||||
encoder = "cpu";
|
||||
}
|
||||
|
||||
std::string container = "flv";
|
||||
if(config.streaming_config.streaming_service == "custom")
|
||||
container = config.streaming_config.custom.container;
|
||||
choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
|
||||
|
||||
const std::string url = streaming_get_url(config);
|
||||
|
||||
@@ -2708,7 +2834,7 @@ namespace gsr {
|
||||
|
||||
std::vector<const char*> args = {
|
||||
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
|
||||
"-c", container.c_str(),
|
||||
"-c", container,
|
||||
"-ac", config.streaming_config.record_options.audio_codec.c_str(),
|
||||
"-cursor", config.streaming_config.record_options.record_cursor ? "yes" : "no",
|
||||
"-cr", config.streaming_config.record_options.color_range.c_str(),
|
||||
@@ -2751,16 +2877,13 @@ namespace gsr {
|
||||
// to see when the program has exit.
|
||||
if(config.streaming_config.show_streaming_started_notifications) {
|
||||
char msg[256];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Started streaming this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Started streaming %s", recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Overlay::on_press_take_screenshot(bool finished_region_selection, bool force_region_capture) {
|
||||
if(region_selector.is_started())
|
||||
void Overlay::on_press_take_screenshot(bool finished_selection, bool force_region_capture) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
if(gpu_screen_recorder_screenshot_process > 0) {
|
||||
@@ -2779,7 +2902,7 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(region_capture && !finished_region_selection) {
|
||||
if(region_capture && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [this, force_region_capture]() {
|
||||
usleep(200 * 1000); // Hack: wait 0.2 seconds before taking a screenshot to allow user to move cursor away. TODO: Remove this
|
||||
@@ -2788,6 +2911,14 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.screenshot_config.record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [this, force_region_capture]() {
|
||||
on_press_take_screenshot(true, force_region_capture);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Validate input, fallback to valid values
|
||||
const std::string output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format
|
||||
|
||||
|
||||
@@ -176,11 +176,21 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_basename(const char *path, int size) {
|
||||
for(int i = size - 1; i >= 0; --i) {
|
||||
if(path[i] == '/')
|
||||
return path + i + 1;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// |output_buffer| should be at least PATH_MAX in size
|
||||
bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) {
|
||||
output_buffer[0] = '\0';
|
||||
|
||||
const char *arg0_start = NULL;
|
||||
const char *arg0_end = NULL;
|
||||
int arg0_size = 0;
|
||||
int fd = open(filepath, O_RDONLY);
|
||||
if(fd == -1)
|
||||
return false;
|
||||
@@ -190,13 +200,16 @@ namespace gsr {
|
||||
if(bytes_read == -1)
|
||||
goto err;
|
||||
|
||||
arg0_end = (const char*)memchr(buffer, '\0', bytes_read);
|
||||
arg0_start = buffer;
|
||||
arg0_end = (const char*)memchr(arg0_start, '\0', bytes_read);
|
||||
if(!arg0_end)
|
||||
goto err;
|
||||
|
||||
if((arg0_end - buffer) + 1 <= output_buffer_size) {
|
||||
memcpy(output_buffer, buffer, arg0_end - buffer);
|
||||
output_buffer[arg0_end - buffer] = '\0';
|
||||
arg0_start = get_basename(arg0_start, arg0_end - arg0_start);
|
||||
arg0_size = arg0_end - arg0_start;
|
||||
if(arg0_size + 1 <= output_buffer_size) {
|
||||
memcpy(output_buffer, arg0_start, arg0_size);
|
||||
output_buffer[arg0_size] = '\0';
|
||||
close(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ namespace gsr {
|
||||
window_attr.background_pixel = is_wayland ? 0 : border_color_x11;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
window_attr.colormap = region_window_colormap;
|
||||
|
||||
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
||||
@@ -366,10 +366,6 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RegionSelector::is_selected() const {
|
||||
return selected;
|
||||
}
|
||||
|
||||
bool RegionSelector::take_selection() {
|
||||
const bool result = selected;
|
||||
selected = false;
|
||||
|
||||
@@ -32,6 +32,28 @@ namespace gsr {
|
||||
return str.size() >= len && memcmp(str.data() + str.size() - len, substr, len) == 0;
|
||||
}
|
||||
|
||||
std::string strip(const std::string &str) {
|
||||
int start_index = 0;
|
||||
int str_len = str.size();
|
||||
|
||||
for(int i = 0; i < str_len; ++i) {
|
||||
if(str[i] != ' ') {
|
||||
start_index += i;
|
||||
str_len -= i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = str_len - 1; i >= 0; --i) {
|
||||
if(str[i] != ' ') {
|
||||
str_len = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return str.substr(start_index, str_len);
|
||||
}
|
||||
|
||||
std::string get_home_dir() {
|
||||
const char *home_dir = getenv("HOME");
|
||||
if(!home_dir) {
|
||||
|
||||
229
src/WindowSelector.cpp
Normal file
229
src/WindowSelector.cpp
Normal file
@@ -0,0 +1,229 @@
|
||||
#include "../include/WindowSelector.hpp"
|
||||
#include "../include/WindowUtils.hpp"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <X11/extensions/shape.h>
|
||||
#include <X11/cursorfont.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
namespace gsr {
|
||||
static const int rectangle_border_size = 2;
|
||||
|
||||
static int max_int(int a, int b) {
|
||||
return a >= b ? a : b;
|
||||
}
|
||||
|
||||
static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) {
|
||||
if(width < 0) {
|
||||
x += width;
|
||||
width = abs(width);
|
||||
}
|
||||
|
||||
if(height < 0) {
|
||||
y += height;
|
||||
height = abs(height);
|
||||
}
|
||||
|
||||
XRectangle rectangles[] = {
|
||||
{
|
||||
(short)max_int(0, x), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
||||
}, // Left
|
||||
{
|
||||
(short)max_int(0, x + width - border_size), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
||||
}, // Right
|
||||
{
|
||||
(short)max_int(0, x + border_size), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Top
|
||||
{
|
||||
(short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size),
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Bottom
|
||||
};
|
||||
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
static unsigned long mgl_color_to_x11_color(mgl::Color color) {
|
||||
if(color.a == 0)
|
||||
return 0;
|
||||
return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF);
|
||||
}
|
||||
|
||||
static Window get_cursor_window(Display *dpy) {
|
||||
Window root_window = None;
|
||||
Window window = None;
|
||||
int dummy_i;
|
||||
unsigned int dummy_u;
|
||||
mgl::vec2i root_pos;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
|
||||
return window;
|
||||
}
|
||||
|
||||
static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) {
|
||||
Window root_window;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
unsigned int w = 0;
|
||||
unsigned int h = 0;
|
||||
unsigned int dummy_border, dummy_depth;
|
||||
XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth);
|
||||
pos.x = x;
|
||||
pos.y = y;
|
||||
size.x = w;
|
||||
size.y = h;
|
||||
}
|
||||
|
||||
WindowSelector::WindowSelector() {
|
||||
|
||||
}
|
||||
|
||||
WindowSelector::~WindowSelector() {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool WindowSelector::start(mgl::Color border_color) {
|
||||
if(dpy)
|
||||
return false;
|
||||
|
||||
const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color);
|
||||
dpy = XOpenDisplay(nullptr);
|
||||
if(!dpy) {
|
||||
fprintf(stderr, "Error: WindowSelector::start: failed to connect to the X11 server\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const Window cursor_window = get_cursor_window(dpy);
|
||||
mgl::vec2i cursor_window_pos, cursor_window_size;
|
||||
get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size);
|
||||
|
||||
XVisualInfo vinfo;
|
||||
memset(&vinfo, 0, sizeof(vinfo));
|
||||
XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
|
||||
border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
|
||||
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.background_pixel = border_color_x11;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
window_attr.colormap = border_window_colormap;
|
||||
|
||||
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
||||
border_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0,
|
||||
vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
|
||||
if(!border_window) {
|
||||
fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n");
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
|
||||
if(cursor_window && cursor_window != DefaultRootWindow(dpy))
|
||||
set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size);
|
||||
else
|
||||
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
|
||||
make_window_click_through(dpy, border_window);
|
||||
XMapWindow(dpy, border_window);
|
||||
|
||||
crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair);
|
||||
XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime);
|
||||
XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime);
|
||||
XFlush(dpy);
|
||||
|
||||
selected = false;
|
||||
canceled = false;
|
||||
selected_window = None;
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowSelector::stop() {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
XUngrabPointer(dpy, CurrentTime);
|
||||
XUngrabKeyboard(dpy, CurrentTime);
|
||||
|
||||
if(border_window_colormap) {
|
||||
XFreeColormap(dpy, border_window_colormap);
|
||||
border_window_colormap = 0;
|
||||
}
|
||||
|
||||
if(border_window) {
|
||||
XDestroyWindow(dpy, border_window);
|
||||
border_window = 0;
|
||||
}
|
||||
|
||||
if(crosshair_cursor) {
|
||||
XFreeCursor(dpy, crosshair_cursor);
|
||||
crosshair_cursor = None;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
}
|
||||
|
||||
bool WindowSelector::is_started() const {
|
||||
return dpy != nullptr;
|
||||
}
|
||||
|
||||
bool WindowSelector::failed() const {
|
||||
return !dpy;
|
||||
}
|
||||
|
||||
bool WindowSelector::poll_events() {
|
||||
if(!dpy || selected)
|
||||
return false;
|
||||
|
||||
XEvent xev;
|
||||
while(XPending(dpy)) {
|
||||
XNextEvent(dpy, &xev);
|
||||
|
||||
if(xev.type == MotionNotify) {
|
||||
const Window motion_window = xev.xmotion.subwindow;
|
||||
mgl::vec2i motion_window_pos, motion_window_size;
|
||||
get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size);
|
||||
if(motion_window && motion_window != DefaultRootWindow(dpy))
|
||||
set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size);
|
||||
else
|
||||
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
|
||||
XFlush(dpy);
|
||||
} else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) {
|
||||
selected_window = xev.xbutton.subwindow;
|
||||
const Window clicked_window_real = window_get_target_window_child(dpy, selected_window);
|
||||
if(clicked_window_real)
|
||||
selected_window = clicked_window_real;
|
||||
selected = true;
|
||||
|
||||
stop();
|
||||
break;
|
||||
} else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) {
|
||||
canceled = true;
|
||||
selected = false;
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowSelector::take_selection() {
|
||||
const bool result = selected;
|
||||
selected = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool WindowSelector::take_canceled() {
|
||||
const bool result = canceled;
|
||||
canceled = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
Window WindowSelector::get_selection() const {
|
||||
return selected_window;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "../include/WindowUtils.hpp"
|
||||
#include "../include/Utils.hpp"
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xutil.h>
|
||||
@@ -62,7 +63,7 @@ namespace gsr {
|
||||
return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
|
||||
}
|
||||
|
||||
static Window window_get_target_window_child(Display *display, Window window) {
|
||||
Window window_get_target_window_child(Display *display, Window window) {
|
||||
if(window == None)
|
||||
return None;
|
||||
|
||||
@@ -212,28 +213,6 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string strip(const std::string &str) {
|
||||
int start_index = 0;
|
||||
int str_len = str.size();
|
||||
|
||||
for(int i = 0; i < str_len; ++i) {
|
||||
if(str[i] != ' ') {
|
||||
start_index += i;
|
||||
str_len -= i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = str_len - 1; i >= 0; --i) {
|
||||
if(str[i] != ' ') {
|
||||
str_len = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return str.substr(start_index, str_len);
|
||||
}
|
||||
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
|
||||
std::string result;
|
||||
const Window focused_window = get_focused_window(dpy, window_capture_type);
|
||||
|
||||
@@ -35,9 +35,8 @@ namespace gsr {
|
||||
std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_record_area_box() {
|
||||
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
// TODO: Show options not supported but disable them
|
||||
// TODO: Enable this
|
||||
//if(capture_options.window)
|
||||
// record_area_box->add_item("Window", "window");
|
||||
if(capture_options.window)
|
||||
record_area_box->add_item("Window", "window");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(!capture_options.monitors.empty())
|
||||
@@ -60,14 +59,6 @@ namespace gsr {
|
||||
return record_area_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_select_window() {
|
||||
auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
|
||||
select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
|
||||
select_window_list_ptr = select_window_list.get();
|
||||
return select_window_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_width_entry() {
|
||||
auto image_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
|
||||
image_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
|
||||
@@ -124,7 +115,6 @@ namespace gsr {
|
||||
|
||||
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
capture_target_list->add_widget(create_record_area());
|
||||
capture_target_list->add_widget(create_select_window());
|
||||
capture_target_list->add_widget(create_image_resolution_section());
|
||||
capture_target_list->add_widget(create_restore_portal_session_section());
|
||||
|
||||
@@ -258,9 +248,7 @@ namespace gsr {
|
||||
content_page_ptr->add_widget(create_settings());
|
||||
|
||||
record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
const bool window_selected = id == "window";
|
||||
const bool portal_selected = id == "portal";
|
||||
select_window_list_ptr->set_visible(window_selected);
|
||||
image_resolution_list_ptr->set_visible(change_image_resolution_checkbox_ptr->is_checked());
|
||||
restore_portal_session_list_ptr->set_visible(portal_selected);
|
||||
return true;
|
||||
|
||||
@@ -65,13 +65,12 @@ namespace gsr {
|
||||
std::unique_ptr<ComboBox> SettingsPage::create_record_area_box() {
|
||||
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
// TODO: Show options not supported but disable them
|
||||
// TODO: Enable this
|
||||
//if(capture_options.window)
|
||||
// record_area_box->add_item("Window", "window");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(capture_options.window)
|
||||
record_area_box->add_item("Window", "window");
|
||||
if(capture_options.focused)
|
||||
record_area_box->add_item("Follow focused window", "focused");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(!capture_options.monitors.empty())
|
||||
record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor");
|
||||
for(const auto &monitor : capture_options.monitors) {
|
||||
@@ -92,14 +91,6 @@ namespace gsr {
|
||||
return record_area_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_select_window() {
|
||||
auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
|
||||
select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
|
||||
select_window_list_ptr = select_window_list.get();
|
||||
return select_window_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Entry> SettingsPage::create_area_width_entry() {
|
||||
auto area_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
|
||||
area_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
|
||||
@@ -186,7 +177,6 @@ namespace gsr {
|
||||
|
||||
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
capture_target_list->add_widget(create_record_area());
|
||||
capture_target_list->add_widget(create_select_window());
|
||||
capture_target_list->add_widget(create_area_size_section());
|
||||
capture_target_list->add_widget(create_video_resolution_section());
|
||||
capture_target_list->add_widget(create_restore_portal_session_section());
|
||||
@@ -451,13 +441,13 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_video_bitrate_entry() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "15000", (int)(get_theme().body_font.get_character_size() * 4.0f));
|
||||
auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "8000", (int)(get_theme().body_font.get_character_size() * 4.0f));
|
||||
video_bitrate_entry->validate_handler = create_entry_validator_integer_in_range(1, 500000);
|
||||
video_bitrate_entry_ptr = video_bitrate_entry.get();
|
||||
list->add_widget(std::move(video_bitrate_entry));
|
||||
|
||||
if(type == Type::STREAM) {
|
||||
auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "1.64MB", get_color_theme().text_color);
|
||||
auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "", get_color_theme().text_color);
|
||||
Label *size_mb_label_ptr = size_mb_label.get();
|
||||
list->add_widget(std::move(size_mb_label));
|
||||
|
||||
@@ -634,10 +624,8 @@ namespace gsr {
|
||||
content_page_ptr->add_widget(create_settings());
|
||||
|
||||
record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
const bool window_selected = id == "window";
|
||||
const bool focused_selected = id == "focused";
|
||||
const bool portal_selected = id == "portal";
|
||||
select_window_list_ptr->set_visible(window_selected);
|
||||
area_size_list_ptr->set_visible(focused_selected);
|
||||
video_resolution_list_ptr->set_visible(!focused_selected && change_video_resolution_checkbox_ptr->is_checked());
|
||||
change_video_resolution_checkbox_ptr->set_visible(!focused_selected);
|
||||
|
||||
@@ -240,6 +240,11 @@ int main(int argc, char **argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(gsr::pidof("gpu-screen-recorder", getpid()) != -1) {
|
||||
const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
|
||||
gsr::exec_program_daemonized(args);
|
||||
}
|
||||
|
||||
if(is_flatpak())
|
||||
install_flatpak_systemd_service();
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user