Compare commits

..

7 Commits
1.6.4 ... 1.6.5

Author SHA1 Message Date
dec05eba
fded9b8d57 Match gsr monitor name with wayland monitor name. Thanks info@leocodes 2025-05-25 19:08:57 +02:00
dec05eba
b80e3f8beb Fix crash when opening settings page because of recent change 2025-05-24 18:24:18 +02:00
dec05eba
b807712d79 Mention setcap dependency 2025-05-24 16:38:36 +02:00
dec05eba
2df417f23f gsr-global-hotkeys: better error messages 2025-05-24 14:00:23 +02:00
dec05eba
a82d1a2dfc Only show replay storage option in advanced view 2025-05-21 23:41:52 +02:00
dec05eba
043b6df255 Add livestream url for rumble 2025-05-21 23:00:42 +02:00
dec05eba
831f583f89 Add support for rumble streaming by default 2025-05-21 22:57:55 +02:00
12 changed files with 116 additions and 45 deletions

View File

@@ -14,7 +14,7 @@ A program called `gsr-ui-cli` is also installed when installing this software. T
# 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`).\
If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes both this UI and gpu-screen-recorder so no need to install that first.
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI.
# Dependencies
GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI.
@@ -28,6 +28,7 @@ These are the dependencies needed to build GPU Screen Recorder UI:
* libpulse (libpulse-simple)
* libdrm
* wayland (wayland-client, wayland-egl, wayland-scanner)
* setcap (libcap)
## Runtime dependencies
There are also additional dependencies needed at runtime:

2
TODO
View File

@@ -37,8 +37,6 @@ Fix first frame being black when running without a compositor.
Add support for systray.
Add option to take screenshot.
Move event callbacks to a global list instead of std::function object in each widget. This reduces the size of widgets,
since most widgets wont have the event callback set.
This event callback would pass the widget as an argument.

View File

@@ -55,7 +55,7 @@ namespace gsr {
std::string video_quality = "very_high";
std::string video_codec = "auto";
std::string audio_codec = "opus";
std::string framerate_mode = "vfr";
std::string framerate_mode = "auto";
bool advanced_view = false;
bool overclock = false;
bool record_cursor = true;
@@ -79,6 +79,10 @@ namespace gsr {
std::string stream_key;
};
struct RumbleStreamConfig {
std::string stream_key;
};
struct CustomStreamConfig {
std::string url;
std::string container = "flv";
@@ -91,6 +95,7 @@ namespace gsr {
std::string streaming_service = "twitch";
YoutubeStreamConfig youtube;
TwitchStreamConfig twitch;
RumbleStreamConfig rumble;
CustomStreamConfig custom;
ConfigHotkey start_stop_hotkey;
};

View File

@@ -192,6 +192,7 @@ namespace gsr {
Button *save_directory_button_ptr = nullptr;
Entry *twitch_stream_key_entry_ptr = nullptr;
Entry *youtube_stream_key_entry_ptr = nullptr;
Entry *rumble_stream_key_entry_ptr = nullptr;
Entry *stream_url_entry_ptr = nullptr;
Entry *replay_time_entry_ptr = nullptr;
RadioButton *replay_storage_button_ptr = nullptr;

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.6.4', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.6.5', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])

View File

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

View File

@@ -201,6 +201,7 @@ namespace gsr {
{"streaming.service", &config.streaming_config.streaming_service},
{"streaming.youtube.key", &config.streaming_config.youtube.stream_key},
{"streaming.twitch.key", &config.streaming_config.twitch.stream_key},
{"streaming.rumble.key", &config.streaming_config.rumble.stream_key},
{"streaming.custom.url", &config.streaming_config.custom.url},
{"streaming.custom.container", &config.streaming_config.custom.container},
{"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey},

View File

@@ -9,14 +9,8 @@
namespace gsr {
static const int MAX_CONNECTORS = 32;
static const int CONNECTOR_TYPE_COUNTS = 32;
static const uint32_t plane_property_all = 0xF;
typedef struct {
int type;
int count;
} drm_connector_type_count;
typedef enum {
PLANE_PROPERTY_CRTC_X = 1 << 0,
PLANE_PROPERTY_CRTC_Y = 1 << 1,
@@ -105,22 +99,6 @@ namespace gsr {
return get_drm_property_by_name(drm_fd, &properties, name, result);
}
static drm_connector_type_count* drm_connector_types_get_index(drm_connector_type_count *type_counts, int *num_type_counts, int connector_type) {
for(int i = 0; i < *num_type_counts; ++i) {
if(type_counts[i].type == connector_type)
return &type_counts[i];
}
if(*num_type_counts == CONNECTOR_TYPE_COUNTS)
return NULL;
const int index = *num_type_counts;
type_counts[index].type = connector_type;
type_counts[index].count = 0;
++*num_type_counts;
return &type_counts[index];
}
// Note: this monitor name logic is kept in sync with gpu screen recorder
static std::string get_monitor_name_from_crtc_id(int drm_fd, uint32_t crtc_id) {
std::string result;
@@ -128,27 +106,23 @@ namespace gsr {
if(!resources)
return result;
drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS];
int num_type_counts = 0;
for(int i = 0; i < resources->count_connectors; ++i) {
uint64_t connector_crtc_id = 0;
drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]);
if(!connector)
continue;
drm_connector_type_count *connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector->connector_type);
const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type);
if(connector_type)
++connector_type->count;
if(!connection_name)
goto next;
if(connector->connection != DRM_MODE_CONNECTED)
goto next;
if(connector_type && connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
if(connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
result = connection_name;
result += "-";
result += std::to_string(connector_type->count);
result += std::to_string(connector->connector_type_id);
drmModeFreeConnector(connector);
break;
}

View File

@@ -14,6 +14,68 @@ namespace gsr {
static constexpr int axis_up_down = 7;
static constexpr int axis_left_right = 6;
struct DeviceId {
uint16_t vendor;
uint16_t product;
};
static bool read_file_hex_number(const char *path, unsigned int *value) {
*value = 0;
FILE *f = fopen(path, "rb");
if(!f)
return false;
fscanf(f, "%x", value);
fclose(f);
return true;
}
static DeviceId joystick_get_device_id(const char *path) {
DeviceId device_id;
device_id.vendor = 0;
device_id.product = 0;
const char *js_path_id = nullptr;
const int len = strlen(path);
for(int i = len - 1; i >= 0; --i) {
if(path[i] == '/') {
js_path_id = path + i + 1;
break;
}
}
if(!js_path_id)
return device_id;
unsigned int vendor = 0;
unsigned int product = 0;
char path_buf[1024];
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id);
if(!read_file_hex_number(path_buf, &vendor))
return device_id;
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id);
if(!read_file_hex_number(path_buf, &product))
return device_id;
device_id.vendor = vendor;
device_id.product = product;
return device_id;
}
static bool is_ps4_controller(DeviceId device_id) {
return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4);
}
static bool is_ps5_controller(DeviceId device_id) {
return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6);
}
static bool is_stadia_controller(DeviceId device_id) {
return device_id.vendor == 0x18D1 && (device_id.product == 0x9400);
}
// Returns -1 on error
static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) {
if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0)
@@ -276,6 +338,8 @@ namespace gsr {
dev_input_id
};
//const DeviceId device_id = joystick_get_device_id(dev_input_filepath);
++num_poll_fd;
fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath);
return true;

View File

@@ -37,6 +37,7 @@
#include <X11/Xcursor/Xcursor.h>
#include <mglpp/system/Rect.hpp>
#include <mglpp/window/Event.hpp>
#include <mglpp/system/Utf8.hpp>
extern "C" {
#include <mgl/mgl.h>
@@ -903,6 +904,7 @@ namespace gsr {
// Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused.
// If the focused window is a wayland application then don't use override redirect and instead create
// a fullscreen window for the ui.
// 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;
if(prevent_game_minimizing) {
@@ -1641,8 +1643,21 @@ namespace gsr {
}
static void truncate_string(std::string &str, int max_length) {
if((int)str.size() > max_length)
str.replace(str.begin() + max_length, str.end(), "...");
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) {
@@ -2579,6 +2594,9 @@ namespace gsr {
} else if(config.streaming_config.streaming_service == "youtube") {
url += "rtmp://a.rtmp.youtube.com/live2/";
url += config.streaming_config.youtube.stream_key;
} else if(config.streaming_config.streaming_service == "rumble") {
url += "rtmp://rtmp.rumble.com/live/";
url += config.streaming_config.rumble.stream_key;
} else if(config.streaming_config.streaming_service == "custom") {
url = config.streaming_config.custom.url;
if(url.size() >= 7 && strncmp(url.c_str(), "rtmp://", 7) == 0)

View File

@@ -964,6 +964,7 @@ namespace gsr {
auto streaming_service_box = std::make_unique<ComboBox>(&get_theme().body_font);
streaming_service_box->add_item("Twitch", "twitch");
streaming_service_box->add_item("YouTube", "youtube");
streaming_service_box->add_item("Rumble", "rumble");
streaming_service_box->add_item("Custom", "custom");
streaming_service_box_ptr = streaming_service_box.get();
return streaming_service_box;
@@ -988,6 +989,10 @@ namespace gsr {
youtube_stream_key_entry_ptr = youtube_stream_key_entry.get();
stream_key_list->add_widget(std::move(youtube_stream_key_entry));
auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
rumble_stream_key_entry_ptr = rumble_stream_key_entry.get();
stream_key_list->add_widget(std::move(rumble_stream_key_entry));
stream_key_list_ptr = stream_key_list.get();
return stream_key_list;
}
@@ -1049,12 +1054,14 @@ namespace gsr {
streaming_service_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
const bool twitch_option = id == "twitch";
const bool youtube_option = id == "youtube";
const bool rumble_option = id == "rumble";
const bool custom_option = id == "custom";
stream_key_list_ptr->set_visible(!custom_option);
stream_url_list_ptr->set_visible(custom_option);
container_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->set_visible(twitch_option);
youtube_stream_key_entry_ptr->set_visible(youtube_option);
rumble_stream_key_entry_ptr->set_visible(rumble_option);
return true;
};
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");
@@ -1256,6 +1263,7 @@ namespace gsr {
streaming_service_box_ptr->set_selected_item(config.streaming_config.streaming_service);
youtube_stream_key_entry_ptr->set_text(config.streaming_config.youtube.stream_key);
twitch_stream_key_entry_ptr->set_text(config.streaming_config.twitch.stream_key);
rumble_stream_key_entry_ptr->set_text(config.streaming_config.rumble.stream_key);
stream_url_entry_ptr->set_text(config.streaming_config.custom.url);
container_box_ptr->set_selected_item(config.streaming_config.custom.container);
}
@@ -1397,6 +1405,7 @@ namespace gsr {
config.streaming_config.streaming_service = streaming_service_box_ptr->get_selected_id();
config.streaming_config.youtube.stream_key = youtube_stream_key_entry_ptr->get_text();
config.streaming_config.twitch.stream_key = twitch_stream_key_entry_ptr->get_text();
config.streaming_config.rumble.stream_key = rumble_stream_key_entry_ptr->get_text();
config.streaming_config.custom.url = stream_url_entry_ptr->get_text();
config.streaming_config.custom.container = container_box_ptr->get_selected_id();
}

View File

@@ -44,42 +44,42 @@ int main(int argc, char **argv) {
} 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);
fprintf(stderr, "gsr-global-hotkeys 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);
fprintf(stderr, "gsr-global-hotkeys error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
usage();
return 1;
}
if(is_gsr_global_hotkeys_already_running()) {
fprintf(stderr, "Error: gsr-global-hotkeys is already running\n");
fprintf(stderr, "gsr-global-hotkeys error: gsr-global-hotkeys is already running\n");
return 1;
}
const uid_t user_id = getuid();
if(geteuid() != 0) {
if(setuid(0) == -1) {
fprintf(stderr, "Error: failed to change user to root\n");
fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");
return 1;
}
}
keyboard_event keyboard_ev;
if(!keyboard_event_init(&keyboard_ev, true, grab_type)) {
fprintf(stderr, "Error: failed to setup hotplugging and no keyboard input devices were found\n");
fprintf(stderr, "gsr-global-hotkeys error: failed to setup hotplugging and no keyboard input devices were found\n");
setuid(user_id);
return 1;
}
fprintf(stderr, "Info: global hotkeys setup, waiting for hotkeys to be pressed\n");
fprintf(stderr, "gsr-global-hotkeys info: global hotkeys setup, waiting for hotkeys to be pressed\n");
for(;;) {
keyboard_event_poll_events(&keyboard_ev, -1);
if(keyboard_event_stdin_has_failed(&keyboard_ev)) {
fprintf(stderr, "Info: stdin closed (parent process likely closed this process), exiting...\n");
fprintf(stderr, "gsr-global-hotkeys info: stdin closed (parent process likely closed this process), exiting...\n");
break;
}
}