From c4100f3c6633607e5d3277af450bee00456f09f9 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 18 Feb 2023 21:13:55 +0100 Subject: Add hotkey/codec config, add option to merge audio tracks --- .gitignore | 2 + README.md | 4 +- TODO | 3 +- build.sh | 4 +- com.dec05eba.gpu_screen_recorder.appdata.xml | 12 + project.conf | 2 +- src/config.hpp | 60 +- src/egl.c | 195 ++++++ src/egl.h | 52 ++ src/library_loader.h | 42 ++ src/main.cpp | 997 +++++++++++++++++++++++---- 11 files changed, 1239 insertions(+), 134 deletions(-) create mode 100644 src/egl.c create mode 100644 src/egl.h create mode 100644 src/library_loader.h diff --git a/.gitignore b/.gitignore index 03cb7dd..d339a3a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,7 @@ compile_commands.json tests/sibs-build/ tests/compile_commands.json +*.o + .clangd/ gpu-screen-recorder-gtk diff --git a/README.md b/README.md index ed0e33b..90499d2 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ and then rebooting your laptop. ## Installation This program depends on [gpu-screen-recorder](https://git.dec05eba.com/gpu-screen-recorder/) which needs to be installed first.\ Run `./install.sh` as root or if you are running Arch Linux, then you can find gpu screen recorder gtk on aur under the name gpu-screen-recorder-gtk-git (`yay -S gpu-screen-recorder-gtk-git`).\ -Dependencies needed when building using `build.sh` or `install.sh`: `gtk3 libx11 libxrandr libpulse`.\ -You can also install gpu screen recorder (the gtk gui version) from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes gpu-screen-recorder so no need to install that first.\ +Dependencies needed when building using `build.sh` or `install.sh`: `gtk3 libx11 libxrandr libpulse libglvnd (which provides libgl and libegl)`.\ +You can also install gpu screen recorder (the gtk gui version) from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes gpu-screen-recorder so no need to install that first. ## Screenshots ![](https://www.dec05eba.com/images/gpu-screen-recorder.png) diff --git a/TODO b/TODO index 9163503..0456172 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,3 @@ Capture stderr (ignore fps: 250) and show that in notification on error. Make sure the resolution is allowed for streaming. -Allow to change hotkeys. -Add list of windows to select from. This makes it easier to select another window that is not in the view to be clickable. +Add list of windows to select from. This makes it easier to select another window that is not in the view to be clickable. \ No newline at end of file diff --git a/build.sh b/build.sh index 8c38b31..563cfec 100755 --- a/build.sh +++ b/build.sh @@ -3,4 +3,6 @@ dependencies="gtk+-3.0 x11 xrandr libpulse" includes="$(pkg-config --cflags $dependencies)" libs="$(pkg-config --libs $dependencies) -ldl" -g++ -o gpu-screen-recorder-gtk -O2 src/main.cpp -s $includes $libs +gcc -c src/egl.c -O2 -g0 -DNDEBUG $includes +g++ -c src/main.cpp -O2 -g0 -DNDEBUG $includes +g++ -o gpu-screen-recorder-gtk -O2 egl.o main.o -s $libs diff --git a/com.dec05eba.gpu_screen_recorder.appdata.xml b/com.dec05eba.gpu_screen_recorder.appdata.xml index eac4f0b..9f83665 100644 --- a/com.dec05eba.gpu_screen_recorder.appdata.xml +++ b/com.dec05eba.gpu_screen_recorder.appdata.xml @@ -33,6 +33,18 @@ + + +
    +
  • Switch to EGL (fixes possible window capture issues when using a compositor)
  • +
  • Add option to change hotkeys
  • +
  • Add option to merge audio tracks into one audio track
  • +
  • Add option to follow the focused window
  • +
  • Add option to force set h264/hevc (services such as discord can't play hevc videos directly in the application)
  • +
  • Show proper error when NVIDIA GPU is not in use
  • +
+
+

Allow choosing between mp4, flv and mkv for record/replay. mkv survives system crashes

diff --git a/project.conf b/project.conf index 8720ecb..77eaf53 100644 --- a/project.conf +++ b/project.conf @@ -1,7 +1,7 @@ [package] name = "gpu-screen-recorder-gtk" type = "executable" -version = "0.1.0" +version = "1.3.0" platforms = ["posix"] [dependencies] diff --git a/src/config.hpp b/src/config.hpp index feb68dd..fc19cd2 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -11,29 +11,40 @@ #include #include +struct ConfigHotkey { + int64_t keysym = 0; + uint32_t modifiers = 0; +}; + struct MainConfig { std::string record_area_option; int record_area_width = 0; int record_area_height = 0; int fps = 60; + bool merge_audio_tracks = true; std::vector audio_input; std::string quality; + std::string codec; }; struct StreamingConfig { std::string streaming_service; std::string stream_key; + ConfigHotkey start_recording_hotkey; }; struct RecordConfig { std::string save_directory; std::string container; + ConfigHotkey start_recording_hotkey; }; struct ReplayConfig { std::string save_directory; std::string container; int replay_time = 30; + ConfigHotkey start_recording_hotkey; + ConfigHotkey save_recording_hotkey; }; struct Config { @@ -207,40 +218,75 @@ static Config read_config() { config.main_config.record_area_option.assign(value.str, value.size); } else if(key == "main.record_area_width") { if(!string_to_int(std::string(value.str, value.size), config.main_config.record_area_width)) { - fprintf(stderr, "Warning: Invalid config option main.record_area_width\n"); + fprintf(stderr, "Warning: Invalid config option value for main.record_area_width\n"); config.main_config.record_area_width = 0; } } else if(key == "main.record_area_height") { if(!string_to_int(std::string(value.str, value.size), config.main_config.record_area_height)) { - fprintf(stderr, "Warning: Invalid config option main.record_area_height\n"); + fprintf(stderr, "Warning: Invalid config option value for main.record_area_height\n"); config.main_config.record_area_height = 0; } } else if(key == "main.fps") { if(!string_to_int(std::string(value.str, value.size), config.main_config.fps)) { - fprintf(stderr, "Warning: Invalid config option main.fps\n"); + fprintf(stderr, "Warning: Invalid config option value for main.fps\n"); config.main_config.fps = 60; } + } else if(key == "main.merge_audio_tracks") { + if(value == "true") + config.main_config.merge_audio_tracks = true; + else if(value == "false") + config.main_config.merge_audio_tracks = false; } else if(key == "main.audio_input") { config.main_config.audio_input.emplace_back(value.str, value.size); } else if(key == "main.quality") { config.main_config.quality.assign(value.str, value.size); + } else if(key == "main.codec") { + config.main_config.codec.assign(value.str, value.size); } else if(key == "streaming.service") { config.streaming_config.streaming_service.assign(value.str, value.size); } else if(key == "streaming.key") { config.streaming_config.stream_key.assign(value.str, value.size); + } else if(key == "streaming.start_recording_hotkey") { + std::string value_str(value.str, value.size); + if(sscanf(value_str.c_str(), "%ld %u", &config.streaming_config.start_recording_hotkey.keysym, &config.streaming_config.start_recording_hotkey.modifiers) != 2) { + fprintf(stderr, "Warning: Invalid config option value for streaming.start_recording_hotkey\n"); + config.streaming_config.start_recording_hotkey.keysym = 0; + config.streaming_config.start_recording_hotkey.modifiers = 0; + } } else if(key == "record.save_directory") { config.record_config.save_directory.assign(value.str, value.size); } else if(key == "record.container") { config.record_config.container.assign(value.str, value.size); + } else if(key == "record.start_recording_hotkey") { + std::string value_str(value.str, value.size); + if(sscanf(value_str.c_str(), "%ld %u", &config.record_config.start_recording_hotkey.keysym, &config.record_config.start_recording_hotkey.modifiers) != 2) { + fprintf(stderr, "Warning: Invalid config option value for record.start_recording_hotkey\n"); + config.record_config.start_recording_hotkey.keysym = 0; + config.record_config.start_recording_hotkey.modifiers = 0; + } } else if(key == "replay.save_directory") { config.replay_config.save_directory.assign(value.str, value.size); } else if(key == "replay.container") { config.replay_config.container.assign(value.str, value.size); } else if(key == "replay.time") { if(!string_to_int(std::string(value.str, value.size), config.replay_config.replay_time)) { - fprintf(stderr, "Warning: Invalid config option replay.time\n"); + fprintf(stderr, "Warning: Invalid config option value for replay.time\n"); config.replay_config.replay_time = 30; } + } else if(key == "replay.start_recording_hotkey") { + std::string value_str(value.str, value.size); + if(sscanf(value_str.c_str(), "%ld %u", &config.replay_config.start_recording_hotkey.keysym, &config.replay_config.start_recording_hotkey.modifiers) != 2) { + fprintf(stderr, "Warning: Invalid config option value for replay.start_recording_hotkey\n"); + config.replay_config.start_recording_hotkey.keysym = 0; + config.replay_config.start_recording_hotkey.modifiers = 0; + } + } else if(key == "replay.save_recording_hotkey") { + std::string value_str(value.str, value.size); + if(sscanf(value_str.c_str(), "%ld %u", &config.replay_config.save_recording_hotkey.keysym, &config.replay_config.save_recording_hotkey.modifiers) != 2) { + fprintf(stderr, "Warning: Invalid config option value for replay.save_recording_hotkey\n"); + config.replay_config.save_recording_hotkey.keysym = 0; + config.replay_config.save_recording_hotkey.modifiers = 0; + } } else { fprintf(stderr, "Warning: Invalid config option: %.*s\n", (int)line.size, line.str); } @@ -273,20 +319,26 @@ static void save_config(const Config &config) { fprintf(file, "main.record_area_width %d\n", config.main_config.record_area_width); fprintf(file, "main.record_area_height %d\n", config.main_config.record_area_height); fprintf(file, "main.fps %d\n", config.main_config.fps); + fprintf(file, "main.merge_audio_tracks %s\n", config.main_config.merge_audio_tracks ? "true" : "false"); for(const std::string &audio_input : config.main_config.audio_input) { fprintf(file, "main.audio_input %s\n", audio_input.c_str()); } fprintf(file, "main.quality %s\n", config.main_config.quality.c_str()); + fprintf(file, "main.codec %s\n", config.main_config.codec.c_str()); fprintf(file, "streaming.service %s\n", config.streaming_config.streaming_service.c_str()); fprintf(file, "streaming.key %s\n", config.streaming_config.stream_key.c_str()); + fprintf(file, "streaming.start_recording_hotkey %ld %u\n", config.streaming_config.start_recording_hotkey.keysym, config.streaming_config.start_recording_hotkey.modifiers); fprintf(file, "record.save_directory %s\n", config.record_config.save_directory.c_str()); fprintf(file, "record.container %s\n", config.record_config.container.c_str()); + fprintf(file, "record.start_recording_hotkey %ld %u\n", config.record_config.start_recording_hotkey.keysym, config.record_config.start_recording_hotkey.modifiers); fprintf(file, "replay.save_directory %s\n", config.replay_config.save_directory.c_str()); fprintf(file, "replay.container %s\n", config.replay_config.container.c_str()); fprintf(file, "replay.time %d\n", config.replay_config.replay_time); + fprintf(file, "replay.start_recording_hotkey %ld %u\n", config.replay_config.start_recording_hotkey.keysym, config.replay_config.start_recording_hotkey.modifiers); + fprintf(file, "replay.save_recording_hotkey %ld %u\n", config.replay_config.save_recording_hotkey.keysym, config.replay_config.save_recording_hotkey.modifiers); fclose(file); } diff --git a/src/egl.c b/src/egl.c new file mode 100644 index 0000000..ecefdf4 --- /dev/null +++ b/src/egl.c @@ -0,0 +1,195 @@ +#include "egl.h" +#include "library_loader.h" +#include + +static bool gsr_egl_create_window(gsr_egl *self) { + EGLConfig ecfg; + int32_t num_config = 0; + EGLDisplay egl_display = NULL; + EGLSurface egl_surface = NULL; + EGLContext egl_context = NULL; + Window window = None; + + int32_t attr[] = { + EGL_BUFFER_SIZE, 24, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_NONE + }; + + int32_t ctxattr[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + + window = XCreateWindow(self->dpy, DefaultRootWindow(self->dpy), 0, 0, 1, 1, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); + + if(!window) { + fprintf(stderr, "gsr error: gsr_gl_create_window failed: failed to create gl window\n"); + goto fail; + } + + egl_display = self->eglGetDisplay(self->dpy); + if(!egl_display) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglGetDisplay failed\n"); + goto fail; + } + + if(!self->eglInitialize(egl_display, NULL, NULL)) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglInitialize failed\n"); + goto fail; + } + + if(!self->eglChooseConfig(egl_display, attr, &ecfg, 1, &num_config) || num_config != 1) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a matching config\n"); + goto fail; + } + + egl_surface = self->eglCreateWindowSurface(egl_display, ecfg, (EGLNativeWindowType)window, NULL); + if(!egl_surface) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create window surface\n"); + goto fail; + } + + egl_context = self->eglCreateContext(egl_display, ecfg, NULL, ctxattr); + if(!egl_context) { + fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create egl context\n"); + goto fail; + } + + self->eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context); + + self->egl_display = egl_display; + self->egl_surface = egl_surface; + self->egl_context = egl_context; + self->window = window; + return true; + + fail: + if(egl_context) + self->eglDestroyContext(egl_display, egl_context); + if(egl_surface) + self->eglDestroySurface(egl_display, egl_surface); + if(egl_display) + self->eglTerminate(egl_display); + if(window) + XDestroyWindow(self->dpy, window); + return false; +} + +static bool gsr_egl_load_egl(gsr_egl *self, void *library) { + dlsym_assign required_dlsym[] = { + { (void**)&self->eglGetDisplay, "eglGetDisplay" }, + { (void**)&self->eglInitialize, "eglInitialize" }, + { (void**)&self->eglTerminate, "eglTerminate" }, + { (void**)&self->eglChooseConfig, "eglChooseConfig" }, + { (void**)&self->eglCreateWindowSurface, "eglCreateWindowSurface" }, + { (void**)&self->eglCreateContext, "eglCreateContext" }, + { (void**)&self->eglMakeCurrent, "eglMakeCurrent" }, + { (void**)&self->eglDestroyContext, "eglDestroyContext" }, + { (void**)&self->eglDestroySurface, "eglDestroySurface" }, + + { NULL, NULL } + }; + + if(!dlsym_load_list(library, required_dlsym)) { + fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libEGL.so.1\n"); + return false; + } + + return true; +} + +static bool gsr_egl_load_gl(gsr_egl *self, void *library) { + dlsym_assign required_dlsym[] = { + { (void**)&self->glGetString, "glGetString" }, + + { NULL, NULL } + }; + + if(!dlsym_load_list(library, required_dlsym)) { + fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGL.so.1\n"); + return false; + } + + return true; +} + +bool gsr_egl_load(gsr_egl *self, Display *dpy) { + memset(self, 0, sizeof(gsr_egl)); + self->dpy = dpy; + + dlerror(); /* clear */ + void *egl_lib = dlopen("libEGL.so.1", RTLD_LAZY); + if(!egl_lib) { + fprintf(stderr, "gsr error: gsr_egl_load: failed to load libEGL.so.1, error: %s\n", dlerror()); + return false; + } + + void *gl_lib = dlopen("libGL.so.1", RTLD_LAZY); + if(!egl_lib) { + fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGL.so.1, error: %s\n", dlerror()); + dlclose(egl_lib); + memset(self, 0, sizeof(gsr_egl)); + return false; + } + + if(!gsr_egl_load_egl(self, egl_lib)) { + dlclose(egl_lib); + dlclose(gl_lib); + memset(self, 0, sizeof(gsr_egl)); + return false; + } + + if(!gsr_egl_load_gl(self, gl_lib)) { + dlclose(egl_lib); + dlclose(gl_lib); + memset(self, 0, sizeof(gsr_egl)); + return false; + } + + if(!gsr_egl_create_window(self)) { + dlclose(egl_lib); + dlclose(gl_lib); + memset(self, 0, sizeof(gsr_egl)); + return false; + } + + self->egl_library = egl_lib; + self->gl_library = gl_lib; + return true; +} + +void gsr_egl_unload(gsr_egl *self) { + if(self->egl_context) { + self->eglDestroyContext(self->egl_display, self->egl_context); + self->egl_context = NULL; + } + + if(self->egl_surface) { + self->eglDestroySurface(self->egl_display, self->egl_surface); + self->egl_surface = NULL; + } + + if(self->egl_display) { + self->eglTerminate(self->egl_display); + self->egl_display = NULL; + } + + if(self->window) { + XDestroyWindow(self->dpy, self->window); + self->window = None; + } + + if(self->egl_library) { + dlclose(self->egl_library); + self->egl_library = NULL; + } + + if(self->gl_library) { + dlclose(self->gl_library); + self->gl_library = NULL; + } + + memset(self, 0, sizeof(gsr_egl)); +} diff --git a/src/egl.h b/src/egl.h new file mode 100644 index 0000000..52422bc --- /dev/null +++ b/src/egl.h @@ -0,0 +1,52 @@ +#ifndef GSR_EGL_H +#define GSR_EGL_H + +/* OpenGL EGL library with a hidden window context (to allow using the opengl functions) */ + +#include +#include +#include +#include + +typedef void* EGLDisplay; +typedef void* EGLSurface; +typedef void* EGLContext; +typedef void* EGLConfig; +typedef void* EGLNativeDisplayType; +typedef uintptr_t EGLNativeWindowType; + +#define EGL_BUFFER_SIZE 0x3020 +#define EGL_RENDERABLE_TYPE 0x3040 +#define EGL_OPENGL_ES2_BIT 0x0004 +#define EGL_NONE 0x3038 +#define EGL_CONTEXT_CLIENT_VERSION 0x3098 + +#define GL_VENDOR 0x1F00 +#define GL_RENDERER 0x1F01 + +typedef struct { + void *egl_library; + void *gl_library; + Display *dpy; + EGLDisplay egl_display; + EGLSurface egl_surface; + EGLContext egl_context; + Window window; + + EGLDisplay (*eglGetDisplay)(EGLNativeDisplayType display_id); + unsigned int (*eglInitialize)(EGLDisplay dpy, int32_t *major, int32_t *minor); + unsigned int (*eglTerminate)(EGLDisplay dpy); + unsigned int (*eglChooseConfig)(EGLDisplay dpy, const int32_t *attrib_list, EGLConfig *configs, int32_t config_size, int32_t *num_config); + EGLSurface (*eglCreateWindowSurface)(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const int32_t *attrib_list); + EGLContext (*eglCreateContext)(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const int32_t *attrib_list); + unsigned int (*eglMakeCurrent)(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx); + unsigned int (*eglDestroyContext)(EGLDisplay dpy, EGLContext ctx); + unsigned int (*eglDestroySurface)(EGLDisplay dpy, EGLSurface surface); + + const unsigned char* (*glGetString)(unsigned int name); +} gsr_egl; + +bool gsr_egl_load(gsr_egl *self, Display *dpy); +void gsr_egl_unload(gsr_egl *self); + +#endif /* GSR_EGL_H */ diff --git a/src/library_loader.h b/src/library_loader.h new file mode 100644 index 0000000..1622521 --- /dev/null +++ b/src/library_loader.h @@ -0,0 +1,42 @@ +#ifndef GSR_LIBRARY_LOADER_H +#define GSR_LIBRARY_LOADER_H + +#include +#include +#include + +typedef struct { + void **func; + const char *name; +} dlsym_assign; + +static void* dlsym_print_fail(void *handle, const char *name, bool required) { + dlerror(); + void *sym = dlsym(handle, name); + char *err_str = dlerror(); + + if(!sym) + fprintf(stderr, "%s: dlsym(handle, \"%s\") failed, error: %s\n", required ? "error" : "warning", name, err_str ? err_str : "(null)"); + + return sym; +} + +/* |dlsyms| should be null terminated */ +static bool dlsym_load_list(void *handle, const dlsym_assign *dlsyms) { + bool success = true; + for(int i = 0; dlsyms[i].func; ++i) { + *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, true); + if(!*dlsyms[i].func) + success = false; + } + return success; +} + +/* |dlsyms| should be null terminated */ +static void dlsym_load_list_optional(void *handle, const dlsym_assign *dlsyms) { + for(int i = 0; dlsyms[i].func; ++i) { + *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, false); + } +} + +#endif /* GSR_LIBRARY_LOADER_H */ diff --git a/src/main.cpp b/src/main.cpp index 84940f6..71f9fec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,9 @@ #include #include #include +extern "C" { +#include "egl.h" +} typedef struct { Display *display; @@ -33,6 +36,7 @@ typedef struct { GtkWidget *streaming_page; } PageNavigationUserdata; +static GtkWidget *window; static SelectWindowUserdata select_window_userdata; static PageNavigationUserdata page_navigation_userdata; static Cursor crosshair_cursor; @@ -44,6 +48,7 @@ static GtkSpinButton *area_height_entry; static GtkComboBoxText *record_area_selection_menu; static GtkComboBoxText *audio_input_menu_todo; static GtkComboBoxText *quality_input_menu; +static GtkComboBoxText *codec_input_menu; static GtkComboBoxText *stream_service_input_menu; static GtkComboBoxText *record_container; static GtkComboBoxText *replay_container; @@ -65,6 +70,15 @@ static GtkSpinButton *replay_time_entry; static GtkButton *select_window_button; static GtkWidget *audio_input_used_list; static GtkWidget *add_audio_input_button; +static GtkWidget *record_hotkey_button; +static GtkWidget *replay_start_stop_hotkey_button; +static GtkWidget *replay_save_hotkey_button; +static GtkWidget *streaming_hotkey_button; +static GtkWidget *merge_audio_tracks_button; + +static XIM xim; +static XIC xic; + static bool replaying = false; static bool recording = false; static bool streaming = false; @@ -72,6 +86,29 @@ static pid_t gpu_screen_recorder_process = -1; static Config config; static int num_audio_inputs_addable = 0; static std::string record_file_current_filename; +static bool nvfbc_installed = false; + +enum class HotkeyMode { + NoAction, + NewHotkey, + Record +}; + +static HotkeyMode hotkey_mode = HotkeyMode::NoAction; + +struct Hotkey { + uint32_t modkey_mask = 0; + KeySym keysym = None; + GtkWidget *hotkey_entry = nullptr; +}; + +static Hotkey *current_hotkey = nullptr; +static Hotkey pressed_hotkey; +static Hotkey latest_hotkey; +static Hotkey streaming_hotkey; +static Hotkey record_hotkey; +static Hotkey replay_start_stop_hotkey; +static Hotkey replay_save_hotkey; struct Container { const char *container_name; @@ -174,17 +211,16 @@ static void drag_data_received (GtkWidget *widget, GdkDragContext *context, static void enable_stream_record_button_if_info_filled() { const gchar *selected_window_area = gtk_combo_box_get_active_id(GTK_COMBO_BOX(record_area_selection_menu)); - if(strcmp(selected_window_area, "window") == 0 && select_window_userdata.selected_window == None) + if(strcmp(selected_window_area, "window") == 0 && select_window_userdata.selected_window == None) { + gtk_widget_set_sensitive(GTK_WIDGET(replay_button), false); + gtk_widget_set_sensitive(GTK_WIDGET(record_button), false); + gtk_widget_set_sensitive(GTK_WIDGET(stream_button), false); return; - - int num_audio_tracks = 0; - for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&num_audio_tracks](const AudioRow *audio_row) { - ++num_audio_tracks; - }); + } gtk_widget_set_sensitive(GTK_WIDGET(replay_button), true); gtk_widget_set_sensitive(GTK_WIDGET(record_button), true); - gtk_widget_set_sensitive(GTK_WIDGET(stream_button), num_audio_tracks <= 1); + gtk_widget_set_sensitive(GTK_WIDGET(stream_button), true); } static GtkWidget* create_used_audio_input_row(const char *id, const char *text) { @@ -318,22 +354,32 @@ static void save_configs() { config.main_config.record_area_width = gtk_spin_button_get_value_as_int(area_width_entry); config.main_config.record_area_height = gtk_spin_button_get_value_as_int(area_height_entry); config.main_config.fps = gtk_spin_button_get_value_as_int(fps_entry); + config.main_config.merge_audio_tracks = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button)); config.main_config.audio_input.clear(); for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [](const AudioRow *audio_row) { config.main_config.audio_input.push_back(gtk_label_get_text(GTK_LABEL(audio_row->label))); }); config.main_config.quality = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); + config.main_config.codec = gtk_combo_box_get_active_id(GTK_COMBO_BOX(codec_input_menu)); config.streaming_config.streaming_service = gtk_combo_box_get_active_id(GTK_COMBO_BOX(stream_service_input_menu)); config.streaming_config.stream_key = gtk_entry_get_text(stream_id_entry); + config.streaming_config.start_recording_hotkey.keysym = streaming_hotkey.keysym; + config.streaming_config.start_recording_hotkey.modifiers = streaming_hotkey.modkey_mask; config.record_config.save_directory = gtk_button_get_label(record_file_chooser_button); config.record_config.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(record_container)); + config.record_config.start_recording_hotkey.keysym = record_hotkey.keysym; + config.record_config.start_recording_hotkey.modifiers = record_hotkey.modkey_mask; config.replay_config.save_directory = gtk_button_get_label(replay_file_chooser_button); config.replay_config.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(replay_container)); config.replay_config.replay_time = gtk_spin_button_get_value_as_int(replay_time_entry); + config.replay_config.start_recording_hotkey.keysym = replay_start_stop_hotkey.keysym; + config.replay_config.start_recording_hotkey.modifiers = replay_start_stop_hotkey.modkey_mask; + config.replay_config.save_recording_hotkey.keysym = replay_save_hotkey.keysym; + config.replay_config.save_recording_hotkey.modifiers = replay_save_hotkey.modkey_mask; save_config(config); } @@ -520,15 +566,296 @@ static gboolean on_select_window_button_click(GtkButton *button, gpointer userda return true; } +static bool key_is_modifier(KeySym key_sym) { + return key_sym >= XK_Shift_L && key_sym <= XK_Super_R && key_sym != XK_Caps_Lock && key_sym != XK_Shift_Lock; +} + +static uint32_t modkey_to_mask(KeySym key_sym) { + assert(key_is_modifier(key_sym)); + return 1 << (key_sym - XK_Shift_L); +} + +static uint32_t key_mod_mask_to_x11_mask(uint32_t mask) { + uint32_t key_mod_masks = 0; + if(mask & (modkey_to_mask(XK_Control_L) | modkey_to_mask(XK_Control_R))) + key_mod_masks |= ControlMask; + if(mask & (modkey_to_mask(XK_Alt_L) | modkey_to_mask(XK_Alt_R))) + key_mod_masks |= Mod1Mask; + if(mask & (modkey_to_mask(XK_Shift_L) | modkey_to_mask(XK_Shift_R))) + key_mod_masks |= ShiftMask; + if(mask & (modkey_to_mask(XK_Super_L) | modkey_to_mask(XK_Super_R) | modkey_to_mask(XK_Meta_L)| modkey_to_mask(XK_Meta_R))) + key_mod_masks |= Mod4Mask; + //if(mask & (modkey_to_mask(XK_Caps_Lock) | modkey_to_mask(XK_Shift_Lock))) + // key_mod_masks |= LockMask; + return key_mod_masks; +} + +static unsigned int key_state_without_locks(unsigned int key_state) { + return key_state & ~(Mod2Mask|LockMask); +} + +struct CustomKeyName { + KeySym key_sym; + const char *name; +}; + +static int key_get_name(KeySym key_sym, char *buffer, int buffer_size) { + if(buffer_size == 0) + return 0; + + #define CUSTOM_KEY_NAME_LEN 23 + const CustomKeyName key_names[CUSTOM_KEY_NAME_LEN] = { + { XK_Caps_Lock, "Caps Lock" }, + { XK_Shift_Lock, "Caps Lock" }, + { XK_Return, "Return" }, + { XK_BackSpace, "BackSpace" }, + { XK_Tab, "Tab" }, + { XK_Delete, "Delete" }, + { XK_dead_acute, "`" }, + { XK_dead_diaeresis, "^" }, + { XK_Prior, "PageUp" }, + { XK_Next, "PageDown" }, + { ' ', "Space" }, + { XK_KP_Insert, "KeyPad 0" }, + { XK_KP_End, "KeyPad 1" }, + { XK_KP_Down, "KeyPad 2" }, + { XK_KP_Next, "KeyPad 3" }, + { XK_KP_Left, "KeyPad 4" }, + { XK_KP_Begin, "KeyPad 5" }, + { XK_KP_Right, "KeyPad 6" }, + { XK_KP_Home, "KeyPad 7" }, + { XK_KP_Up, "KeyPad 8" }, + { XK_KP_Prior, "KeyPad 9" }, + { XK_KP_Enter, "KeyPad Return" }, + { XK_KP_Delete, "KeyPad Delete" } + }; + + for(int i = 0; i < CUSTOM_KEY_NAME_LEN; ++i) { + const CustomKeyName custom_key_name = key_names[i]; + if(key_sym == custom_key_name.key_sym) { + const int key_len = strlen(custom_key_name.name); + if(buffer_size < key_len) + return 0; + + memcpy(buffer, custom_key_name.name, key_len); + return key_len; + } + } + + XKeyPressedEvent event; + event.type = KeyPress; + event.display = gdk_x11_get_default_xdisplay(); + event.state = 0; + event.keycode = XKeysymToKeycode(event.display, key_sym); + + KeySym ignore; + Status return_status; + int buflen = Xutf8LookupString(xic, &event, buffer, buffer_size, &ignore, &return_status); + if(return_status != XBufferOverflow && buflen > 0) + return buflen; + + const char *keysym_str = XKeysymToString(key_sym); + if(keysym_str) { + int keysym_str_len = strlen(keysym_str); + if(buffer_size >= keysym_str_len) { + memcpy(buffer, keysym_str, keysym_str_len); + return keysym_str_len; + } + } + + return 0; +} + +static int xerror_dummy(Display *dpy, XErrorEvent *ee) { + return 0; +} + +static bool x_failed = false; +static int xerror_grab_error(Display *dpy, XErrorEvent *ee) { + x_failed = true; + return 0; +} + +static void ungrab_keyboard(Display *display) { + XErrorHandler prev_error_handler = XSetErrorHandler(xerror_dummy); + XUngrabKeyboard(display, CurrentTime); + XSync(display, False); + XSetErrorHandler(prev_error_handler); +} + +static bool grab_ungrab_hotkey_combo(Display *display, Hotkey hotkey, bool grab) { + if(hotkey.keysym == None && hotkey.modkey_mask == 0) + return true; + + unsigned int numlockmask = 0; + KeyCode numlock_keycode = XKeysymToKeycode(display, XK_Num_Lock); + XModifierKeymap *modmap = XGetModifierMapping(display); + if(modmap) { + for(int i = 0; i < 8; ++i) { + for(int j = 0; j < modmap->max_keypermod; ++j) { + if(modmap->modifiermap[i * modmap->max_keypermod + j] == numlock_keycode) + numlockmask = (1 << i); + } + } + XFreeModifiermap(modmap); + } + + unsigned int key_mod_masks = 0; + KeySym key_sym = hotkey.keysym; + if(key_sym == None) { + // TODO: Set key_sym to one of the modkey mask values and set key_mod_masks to the other modkeys + } else { + key_mod_masks = key_mod_mask_to_x11_mask(hotkey.modkey_mask); + } + + XSync(display, False); + x_failed = false; + XErrorHandler prev_error_handler = XSetErrorHandler(xerror_grab_error); + + Window root_window = DefaultRootWindow(display); + unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; + if(key_sym != None) { + for(int i = 0; i < 4; ++i) { + if(grab) { + XGrabKey(display, XKeysymToKeycode(display, key_sym), key_mod_masks|modifiers[i], root_window, False, GrabModeAsync, GrabModeAsync); + } else { + XUngrabKey(display, XKeysymToKeycode(display, key_sym), key_mod_masks|modifiers[i], root_window); + } + } + } + XSync(display, False); + + bool success = !x_failed; + + if(!success && key_sym != None) { + for(int i = 0; i < 4; ++i) { + XUngrabKey(display, XKeysymToKeycode(display, key_sym), key_mod_masks|modifiers[i], root_window); + } + } + XSync(display, False); + + XSetErrorHandler(prev_error_handler); + return success; +} + +static void ungrab_keys(Display *display) { + grab_ungrab_hotkey_combo(display, streaming_hotkey, false); + grab_ungrab_hotkey_combo(display, record_hotkey, false); + grab_ungrab_hotkey_combo(display, replay_start_stop_hotkey, false); + grab_ungrab_hotkey_combo(display, replay_save_hotkey, false); +} + +static void set_hotkey_text_from_hotkey_data(GtkEntry *entry, Hotkey hotkey) { + struct ModkeyName { + KeySym key_sym; + const char *name; + }; + + const ModkeyName modkey_names[] = { + { XK_Control_L, "Ctrl" }, + { XK_Control_R, "Ctrl" }, + { XK_Super_L, "Super" }, + { XK_Super_R, "Super" }, + { XK_Meta_L, "Super" }, + { XK_Meta_R, "Super" }, + { XK_Shift_L, "Shift" }, + { XK_Shift_R, "Shift" }, + { XK_Alt_L, "Alt" }, + { XK_Alt_R, "Alt" }, + }; + + std::string hotkey_combo_str; + + for(auto modkey_name : modkey_names) { + if(hotkey.modkey_mask & modkey_to_mask(modkey_name.key_sym)) { + if(!hotkey_combo_str.empty()) + hotkey_combo_str += " + "; + hotkey_combo_str += modkey_name.name; + } + } + + if(!hotkey_combo_str.empty()) + hotkey_combo_str += " + "; + + char buffer[128]; + int buflen = key_get_name(hotkey.keysym, buffer, sizeof(buffer)); + if(buflen > 0) + hotkey_combo_str.append(buffer, buflen); + + gtk_entry_set_text(entry, hotkey_combo_str.c_str()); +} + +struct HotkeyResult { + bool record_hotkey_success = false; + bool streaming_hotkey_success = false; + bool replay_start_stop_hotkey_success = false; + bool replay_save_hotkey_success = false; +}; + +static HotkeyResult replace_grabbed_keys_depending_on_active_page() { + HotkeyResult hotkey_result; + ungrab_keys(gdk_x11_get_default_xdisplay()); + const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata.stack); + if(visible_page == page_navigation_userdata.recording_page) { + bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), record_hotkey, true); + hotkey_mode = HotkeyMode::Record; + hotkey_result.record_hotkey_success = grab_record_success; + } else if(visible_page == page_navigation_userdata.streaming_page) { + bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), streaming_hotkey, true); + hotkey_mode = HotkeyMode::Record; + hotkey_result.streaming_hotkey_success = grab_record_success; + } else if(visible_page == page_navigation_userdata.replay_page) { + bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), replay_start_stop_hotkey, true); + bool grab_save_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), replay_save_hotkey, true); + hotkey_mode = HotkeyMode::Record; + + hotkey_result.replay_start_stop_hotkey_success = grab_record_success; + hotkey_result.replay_save_hotkey_success = grab_save_success; + } + return hotkey_result; +} + static gboolean on_start_replay_click(GtkButton *button, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->replay_page); + HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); + if(!hotkey_result.replay_start_stop_hotkey_success) { + std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(replay_start_stop_hotkey.hotkey_entry)); + std::string error_text = "Replay start/stop hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey"; + gtk_entry_set_text(GTK_ENTRY(replay_start_stop_hotkey.hotkey_entry), ""); + replay_start_stop_hotkey.keysym = 0; + replay_start_stop_hotkey.modkey_mask = 0; + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + } + if(!hotkey_result.replay_save_hotkey_success) { + std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(replay_save_hotkey.hotkey_entry)); + std::string error_text = "Replay save hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey"; + gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey.hotkey_entry), ""); + replay_save_hotkey.keysym = 0; + replay_save_hotkey.modkey_mask = 0; + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + } return true; } static gboolean on_start_recording_click(GtkButton *button, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->recording_page); + HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); + if(!hotkey_result.record_hotkey_success) { + std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(record_hotkey.hotkey_entry)); + std::string error_text = "Record start/stop hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey"; + gtk_entry_set_text(GTK_ENTRY(record_hotkey.hotkey_entry), ""); + record_hotkey.keysym = 0; + record_hotkey.modkey_mask = 0; + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + } return true; } @@ -540,14 +867,40 @@ void on_stream_key_icon_click(GtkWidget *widget, gpointer data) { } static gboolean on_start_streaming_click(GtkButton *button, gpointer userdata) { + int num_audio_tracks = 0; + for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&num_audio_tracks](const AudioRow*) { + ++num_audio_tracks; + }); + + if(num_audio_tracks > 1) { + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "Streaming doesn't work with more than 1 audio track. Please remove all audio tracks or only use 1 audio track"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return true; + } + PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->streaming_page); + HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); + if(!hotkey_result.streaming_hotkey_success) { + std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(streaming_hotkey.hotkey_entry)); + std::string error_text = "Streaming start/stop hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey"; + gtk_entry_set_text(GTK_ENTRY(streaming_hotkey.hotkey_entry), ""); + streaming_hotkey.keysym = 0; + streaming_hotkey.modkey_mask = 0; + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + } return true; } -static gboolean on_streaming_recording_page_back_click(GtkButton *button, gpointer userdata) { +static gboolean on_streaming_recording_replay_page_back_click(GtkButton *button, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->common_settings_page); + ungrab_keys(gdk_x11_get_default_xdisplay()); + hotkey_mode = HotkeyMode::NoAction; return true; } @@ -658,17 +1011,30 @@ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdat const gchar* container_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(replay_container)); const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); + const gchar* codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(codec_input_menu)); char area[64]; snprintf(area, sizeof(area), "%dx%d", record_width, record_height); std::vector args = { - "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-q", quality_input_str, "-f", fps_str.c_str(), "-r", replay_time_str.c_str(), "-o", dir + "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-q", quality_input_str, "-k", codec_input_str, "-f", fps_str.c_str(), "-r", replay_time_str.c_str(), "-o", dir }; - for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&args](const AudioRow *audio_row) { - args.insert(args.end(), { "-a", audio_row->id.c_str() }); - }); + std::string merge_audio_tracks_arg_value; + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button))) { + for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&merge_audio_tracks_arg_value](const AudioRow *audio_row) { + if(!merge_audio_tracks_arg_value.empty()) + merge_audio_tracks_arg_value += '|'; + merge_audio_tracks_arg_value += audio_row->id; + }); + + if(!merge_audio_tracks_arg_value.empty()) + args.insert(args.end(), { "-a", merge_audio_tracks_arg_value.c_str() }); + } else { + for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&args](const AudioRow *audio_row) { + args.insert(args.end(), { "-a", audio_row->id.c_str() }); + }); + } if(follow_focused) args.insert(args.end(), { "-s", area }); @@ -707,7 +1073,7 @@ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdat static gboolean on_replay_save_button_click(GtkButton *button, gpointer userdata) { GtkApplication *app = (GtkApplication*)userdata; kill(gpu_screen_recorder_process, SIGUSR1); - show_notification(app, "GPU Screen Recorder", "Saved replay", G_NOTIFICATION_PRIORITY_NORMAL); + //show_notification(app, "GPU Screen Recorder", "Saved replay", G_NOTIFICATION_PRIORITY_NORMAL); return true; } @@ -725,7 +1091,7 @@ static gboolean on_start_recording_button_click(GtkButton *button, gpointer user if(exit_success) { std::string notification_body = std::string("The recording was saved to ") + record_file_current_filename; - show_notification(app, "GPU Screen Recorder", notification_body.c_str(), G_NOTIFICATION_PRIORITY_NORMAL); + //show_notification(app, "GPU Screen Recorder", notification_body.c_str(), G_NOTIFICATION_PRIORITY_NORMAL); } else { std::string notification_body = std::string("Failed to save the recording to ") + record_file_current_filename; show_notification(app, "GPU Screen Recorder", notification_body.c_str(), G_NOTIFICATION_PRIORITY_URGENT); @@ -765,17 +1131,30 @@ static gboolean on_start_recording_button_click(GtkButton *button, gpointer user const gchar* container_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(record_container)); const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); + const gchar* codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(codec_input_menu)); char area[64]; snprintf(area, sizeof(area), "%dx%d", record_width, record_height); std::vector args = { - "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-q", quality_input_str, "-f", fps_str.c_str(), "-o", record_file_current_filename.c_str() + "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-q", quality_input_str, "-k", codec_input_str, "-f", fps_str.c_str(), "-o", record_file_current_filename.c_str() }; - for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&args](const AudioRow *audio_row) { - args.insert(args.end(), { "-a", audio_row->id.c_str() }); - }); + std::string merge_audio_tracks_arg_value; + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button))) { + for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&merge_audio_tracks_arg_value](const AudioRow *audio_row) { + if(!merge_audio_tracks_arg_value.empty()) + merge_audio_tracks_arg_value += '|'; + merge_audio_tracks_arg_value += audio_row->id; + }); + + if(!merge_audio_tracks_arg_value.empty()) + args.insert(args.end(), { "-a", merge_audio_tracks_arg_value.c_str() }); + } else { + for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&args](const AudioRow *audio_row) { + args.insert(args.end(), { "-a", audio_row->id.c_str() }); + }); + } if(follow_focused) args.insert(args.end(), { "-s", area }); @@ -871,17 +1250,30 @@ static gboolean on_start_streaming_button_click(GtkButton *button, gpointer user } const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); + const gchar* codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(codec_input_menu)); char area[64]; snprintf(area, sizeof(area), "%dx%d", record_width, record_height); std::vector args = { - "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "flv", "-q", quality_input_str, "-f", fps_str.c_str(), "-o", stream_url.c_str() + "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "flv", "-q", quality_input_str, "-k", codec_input_str, "-f", fps_str.c_str(), "-o", stream_url.c_str() }; - for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&args](const AudioRow *audio_row) { - args.insert(args.end(), { "-a", audio_row->id.c_str() }); - }); + std::string merge_audio_tracks_arg_value; + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button))) { + for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&merge_audio_tracks_arg_value](const AudioRow *audio_row) { + if(!merge_audio_tracks_arg_value.empty()) + merge_audio_tracks_arg_value += '|'; + merge_audio_tracks_arg_value += audio_row->id; + }); + + if(!merge_audio_tracks_arg_value.empty()) + args.insert(args.end(), { "-a", merge_audio_tracks_arg_value.c_str() }); + } else { + for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&args](const AudioRow *audio_row) { + args.insert(args.end(), { "-a", audio_row->id.c_str() }); + }); + } if(follow_focused) args.insert(args.end(), { "-s", area }); @@ -997,6 +1389,64 @@ static std::vector get_pulseaudio_inputs() { } pa_mainloop_free(main_loop); + return {}; +} + +struct PulseAudioServerInfo { + std::string default_sink_name; + std::string default_source_name; +}; + +static void server_info_callback(pa_context*, const pa_server_info *server_info, void *userdata) { + PulseAudioServerInfo *u = (PulseAudioServerInfo*)userdata; + if(server_info->default_sink_name) + u->default_sink_name = std::string(server_info->default_sink_name) + ".monitor"; + if(server_info->default_source_name) + u->default_source_name = server_info->default_source_name; +} + +static PulseAudioServerInfo get_pulseaudio_default_inputs() { + PulseAudioServerInfo server_info; + pa_mainloop *main_loop = pa_mainloop_new(); + + pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder-gtk"); + pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL); + int state = 0; + int pa_ready = 0; + pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); + + pa_operation *pa_op = NULL; + + for(;;) { + // Not ready + if(pa_ready == 0) { + pa_mainloop_iterate(main_loop, 1, NULL); + continue; + } + + switch(state) { + case 0: { + pa_op = pa_context_get_server_info(ctx, server_info_callback, &server_info); + ++state; + break; + } + } + + // Couldn't get connection to the server + if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) { + if(pa_op) + pa_operation_unref(pa_op); + pa_context_disconnect(ctx); + pa_context_unref(ctx); + pa_mainloop_free(main_loop); + return server_info; + } + + pa_mainloop_iterate(main_loop, 1, NULL); + } + + pa_mainloop_free(main_loop); + return server_info; } static void record_area_item_change_callback(GtkComboBox *widget, gpointer userdata) { @@ -1021,6 +1471,183 @@ static bool is_nv_fbc_installed() { return lib != nullptr; } +typedef gboolean (*KeyPressHandler)(GtkButton *button, gpointer userdata); +static void keypress_toggle_recording(bool recording_state, GtkButton *record_button, KeyPressHandler keypress_handler, GtkApplication *app) { + if(!gtk_widget_get_sensitive(GTK_WIDGET(record_button))) + return; + + if(!recording_state) { + keypress_handler(record_button, app); + } else if(recording_state) { + keypress_handler(record_button, app); + } +} + +static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent *event, gpointer userdata) { + if(hotkey_mode == HotkeyMode::NoAction) + return GDK_FILTER_CONTINUE; + + PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; + XEvent *ev = (XEvent*)xevent; + if(ev->type != KeyPress && ev->type != KeyRelease) + return GDK_FILTER_CONTINUE; + + Display *display = gdk_x11_get_default_xdisplay(); + KeySym key_sym = XLookupKeysym(&ev->xkey, 0); + + if(hotkey_mode == HotkeyMode::Record && ev->type == KeyRelease) { + const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata->stack); + if(visible_page == page_navigation_userdata->recording_page) { + if(key_sym == record_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(record_hotkey.modkey_mask)) { + keypress_toggle_recording(recording, start_recording_button, on_start_recording_button_click, page_navigation_userdata->app); + } + } else if(visible_page == page_navigation_userdata->streaming_page) { + if(key_sym == streaming_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(streaming_hotkey.modkey_mask)) { + keypress_toggle_recording(streaming, start_streaming_button, on_start_streaming_button_click, page_navigation_userdata->app); + } + } else if(visible_page == page_navigation_userdata->replay_page) { + if(key_sym == replay_start_stop_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(replay_start_stop_hotkey.modkey_mask)) { + keypress_toggle_recording(replaying, start_replay_button, on_start_replay_button_click, page_navigation_userdata->app); + } else if(key_sym == replay_save_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(replay_save_hotkey.modkey_mask) && replaying && gpu_screen_recorder_process != -1) { + on_replay_save_button_click(nullptr, page_navigation_userdata->app); + } + } + return GDK_FILTER_CONTINUE; + } + + if(hotkey_mode != HotkeyMode::NewHotkey) + return GDK_FILTER_CONTINUE; + + if(ev->type == KeyPress && key_sym == XK_Escape) { + if(pressed_hotkey.modkey_mask == None && pressed_hotkey.keysym == None) { + ungrab_keyboard(display); + current_hotkey = nullptr; + hotkey_mode = HotkeyMode::Record; + } + return GDK_FILTER_CONTINUE; + } + + if(ev->type == KeyPress) { + // Ignore already pressed key + if(key_is_modifier(key_sym)) { + if(pressed_hotkey.modkey_mask & modkey_to_mask(key_sym)) + return GDK_FILTER_CONTINUE; + pressed_hotkey.modkey_mask |= modkey_to_mask(key_sym); + } else { + if(key_sym == pressed_hotkey.keysym) + return GDK_FILTER_CONTINUE; + pressed_hotkey.keysym = key_sym; + } + + latest_hotkey = pressed_hotkey; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), latest_hotkey); + } + + if(ev->type == KeyRelease) { + if(key_is_modifier(key_sym)) { + pressed_hotkey.modkey_mask &= ~modkey_to_mask(key_sym); + } else if(key_sym == pressed_hotkey.keysym) { + pressed_hotkey.keysym = None; + } + + if(pressed_hotkey.modkey_mask == None && pressed_hotkey.keysym == None && latest_hotkey.keysym == None) { + set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), *current_hotkey); + return GDK_FILTER_CONTINUE; + } + + if(pressed_hotkey.modkey_mask == None && pressed_hotkey.keysym == None) { + ungrab_keyboard(display); + ungrab_keys(gdk_x11_get_default_xdisplay()); + + bool hotkey_already_used_by_another_hotkey = false; + if(current_hotkey == &replay_start_stop_hotkey) + hotkey_already_used_by_another_hotkey = (latest_hotkey.keysym == replay_save_hotkey.keysym && latest_hotkey.modkey_mask == replay_save_hotkey.modkey_mask); + else if(current_hotkey == &replay_save_hotkey) + hotkey_already_used_by_another_hotkey = (latest_hotkey.keysym == replay_start_stop_hotkey.keysym && latest_hotkey.modkey_mask == replay_start_stop_hotkey.modkey_mask); + + if(hotkey_already_used_by_another_hotkey) { + std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); + std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used for something else. Please choose another hotkey"; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), *current_hotkey); + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + current_hotkey = nullptr; + return GDK_FILTER_CONTINUE; + } + + Hotkey prev_current_hotkey = *current_hotkey; + current_hotkey->keysym = latest_hotkey.keysym; + current_hotkey->modkey_mask = latest_hotkey.modkey_mask; + + HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); + bool hotkey_success = false; + if(current_hotkey == &record_hotkey) + hotkey_success = hotkey_result.record_hotkey_success; + else if(current_hotkey == &streaming_hotkey) + hotkey_success = hotkey_result.streaming_hotkey_success; + else if(current_hotkey == &replay_start_stop_hotkey) + hotkey_success = hotkey_result.replay_start_stop_hotkey_success; + else if(current_hotkey == &replay_save_hotkey) + hotkey_success = hotkey_result.replay_save_hotkey_success; + + if(hotkey_success) { + save_configs(); + } else { + *current_hotkey = prev_current_hotkey; + std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); + std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey"; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), *current_hotkey); + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + } + + current_hotkey = nullptr; + return GDK_FILTER_CONTINUE; + } + } + + return GDK_FILTER_CONTINUE; +} + +static gboolean on_hotkey_entry_click(GtkWidget *button, gpointer) { + hotkey_mode = HotkeyMode::NewHotkey; + + pressed_hotkey.hotkey_entry = nullptr; + pressed_hotkey.keysym = None; + pressed_hotkey.modkey_mask = 0; + latest_hotkey = pressed_hotkey; + + if(button == record_hotkey_button) { + current_hotkey = &record_hotkey; + } else if(button == streaming_hotkey_button) { + current_hotkey = &streaming_hotkey; + } else if(button == replay_start_stop_hotkey_button) { + current_hotkey = &replay_start_stop_hotkey; + } else if(button == replay_save_hotkey_button) { + current_hotkey = &replay_save_hotkey; + } else { + current_hotkey = nullptr; + } + + Display *display = gdk_x11_get_default_xdisplay(); + XErrorHandler prev_error_handler = XSetErrorHandler(xerror_dummy); + XGrabKeyboard(display, DefaultRootWindow(display), False, GrabModeAsync, GrabModeAsync, CurrentTime); + XSync(display, False); + XSetErrorHandler(prev_error_handler); + return true; +} + +static bool audio_inputs_contains(const std::vector &audio_inputs, const std::string &audio_input_name) { + for(auto &audio_input : audio_inputs) { + if(audio_input.name == audio_input_name) + return true; + } + return false; +} + static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *app) { GtkGrid *grid = GTK_GRID(gtk_grid_new()); gtk_stack_add_named(stack, GTK_WIDGET(grid), "common-settings"); @@ -1048,7 +1675,7 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a record_area_selection_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(record_area_selection_menu, "window", "Window"); gtk_combo_box_text_append(record_area_selection_menu, "focused", "Follow focused window"); - if(is_nv_fbc_installed()) { + if(nvfbc_installed) { gtk_combo_box_text_append(record_area_selection_menu, "screen", "All monitors"); gtk_combo_box_text_append(record_area_selection_menu, "screen-direct", "All monitors, direct mode (VRR workaround)"); for_each_active_monitor_output(gdk_x11_get_default_xdisplay(), [](const XRROutputInfo *output_info, const XRRCrtcInfo*, const XRRModeInfo *mode_info) { @@ -1070,7 +1697,7 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_combo_box_text_append(record_area_selection_menu, id, label.c_str()); }); } - gtk_combo_box_set_active(GTK_COMBO_BOX(record_area_selection_menu), 0); + gtk_combo_box_set_active(GTK_COMBO_BOX(record_area_selection_menu), nvfbc_installed ? 2 : 0); gtk_widget_set_hexpand(GTK_WIDGET(record_area_selection_menu), true); gtk_grid_attach(record_area_grid, GTK_WIDGET(record_area_selection_menu), 0, record_area_row++, 3, 1); @@ -1117,7 +1744,21 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_grid_attach(audio_grid, GTK_WIDGET(add_audio_grid), 0, audio_input_area_row++, 1, 1); audio_input_menu_todo = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); - for(const AudioInput &audio_input : get_pulseaudio_inputs()) { + const PulseAudioServerInfo pa_server_info = get_pulseaudio_default_inputs(); + const auto audio_inputs = get_pulseaudio_inputs(); + + if(!pa_server_info.default_sink_name.empty() && audio_inputs_contains(audio_inputs, pa_server_info.default_sink_name)) { + gtk_combo_box_text_append(audio_input_menu_todo, pa_server_info.default_sink_name.c_str(), "Default output"); + ++num_audio_inputs_addable; + } + + if(!pa_server_info.default_source_name.empty() && audio_inputs_contains(audio_inputs, pa_server_info.default_source_name)) { + gtk_combo_box_text_append(audio_input_menu_todo, pa_server_info.default_source_name.c_str(), "Default input"); + ++num_audio_inputs_addable; + } + + for(const AudioInput &audio_input : audio_inputs) { + std::string text = audio_input.description; gtk_combo_box_text_append(audio_input_menu_todo, audio_input.name.c_str(), audio_input.description.c_str()); ++num_audio_inputs_addable; } @@ -1165,6 +1806,11 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_widget_set_halign(selected_audio_inputs_label, GTK_ALIGN_START); gtk_grid_attach(add_audio_grid, selected_audio_inputs_label, 0, ++audio_input_area_row, 2, 1); + merge_audio_tracks_button = gtk_check_button_new_with_label("Merge audio tracks"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button), true); + gtk_widget_set_halign(merge_audio_tracks_button, GTK_ALIGN_START); + gtk_grid_attach(grid, merge_audio_tracks_button, 0, grid_row++, 2, 1); + GtkGrid *quality_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(quality_grid), 0, grid_row++, 2, 1); gtk_grid_attach(quality_grid, gtk_label_new("Video quality: "), 0, 0, 1, 1); @@ -1185,6 +1831,17 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_widget_set_hexpand(GTK_WIDGET(fps_entry), true); gtk_grid_attach(fps_grid, GTK_WIDGET(fps_entry), 1, 0, 1, 1); + GtkGrid *codec_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_attach(grid, GTK_WIDGET(codec_grid), 0, grid_row++, 2, 1); + gtk_grid_attach(codec_grid, gtk_label_new("Codec: "), 0, 0, 1, 1); + codec_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + gtk_combo_box_text_append(codec_input_menu, "auto", "Auto (Recommended)"); + gtk_combo_box_text_append(codec_input_menu, "h264", "H264"); + gtk_combo_box_text_append(codec_input_menu, "h265", "HEVC"); + gtk_widget_set_hexpand(GTK_WIDGET(codec_input_menu), true); + gtk_grid_attach(codec_grid, GTK_WIDGET(codec_input_menu), 1, 0, 1, 1); + gtk_combo_box_set_active(GTK_COMBO_BOX(codec_input_menu), 0); + GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, grid_row++, 2, 1); gtk_grid_set_column_spacing(start_button_grid, 10); @@ -1220,17 +1877,46 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); - GtkWidget *hotkey_label = gtk_label_new("Press Shift+Alt+F1 to start/stop the replay and Alt+F1 to save"); - gtk_grid_attach(grid, hotkey_label, 0, 0, 3, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 3, 1); + GtkWidget *a = gtk_label_new("Press"); + gtk_widget_set_halign(a, GTK_ALIGN_END); + + GtkWidget *b = gtk_label_new("to start/stop the replay and "); + gtk_widget_set_halign(b, GTK_ALIGN_START); + + GtkWidget *c = gtk_label_new("to save"); + gtk_widget_set_halign(c, GTK_ALIGN_START); + + replay_start_stop_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(replay_start_stop_hotkey_button), "Alt + F1"); + g_signal_connect(replay_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_start_stop_hotkey_button); + + replay_save_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey_button), "Alt + F2"); + gtk_widget_set_halign(replay_save_hotkey_button, GTK_ALIGN_START); + g_signal_connect(replay_save_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_save_hotkey_button); + + gtk_grid_attach(grid, a, 0, 0, 1, 1); + gtk_grid_attach(grid, replay_start_stop_hotkey_button, 1, 0, 1, 1); + gtk_grid_attach(grid, b, 2, 0, 1, 1); + gtk_grid_attach(grid, replay_save_hotkey_button, 3, 0, 1, 1); + gtk_grid_attach(grid, c, 4, 0, 1, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 5, 1); + + replay_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + replay_start_stop_hotkey.keysym = XK_F1; + replay_start_stop_hotkey.hotkey_entry = replay_start_stop_hotkey_button; + + replay_save_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + replay_save_hotkey.keysym = XK_F2; + replay_save_hotkey.hotkey_entry = replay_save_hotkey_button; GtkWidget *save_icon = gtk_image_new_from_icon_name("document-save", GTK_ICON_SIZE_BUTTON); GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, 2, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, 2, 5, 1); gtk_grid_set_column_spacing(file_chooser_grid, 10); GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the replays?"); - gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_label), 0, 0, 1, 1); + gtk_grid_attach(file_chooser_grid, file_chooser_label, 0, 0, 1, 1); replay_file_chooser_button = GTK_BUTTON(gtk_button_new_with_label(video_filepath.c_str())); gtk_button_set_image(replay_file_chooser_button, save_icon); gtk_button_set_always_show_image(replay_file_chooser_button, true); @@ -1240,7 +1926,7 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(file_chooser_grid, GTK_WIDGET(replay_file_chooser_button), 1, 0, 1, 1); GtkGrid *container_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, 3, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, 3, 5, 1); gtk_grid_attach(container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); replay_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); for(auto &supported_container : supported_containers) { @@ -1251,7 +1937,7 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_combo_box_set_active(GTK_COMBO_BOX(replay_container), 0); GtkGrid *replay_time_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(replay_time_grid), 0, 4, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(replay_time_grid), 0, 4, 5, 1); gtk_grid_attach(replay_time_grid, gtk_label_new("Replay time: "), 0, 0, 1, 1); replay_time_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 1200.0, 1.0)); gtk_spin_button_set_value(replay_time_entry, 30.0); @@ -1260,7 +1946,7 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 5, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 5, 5, 1); gtk_grid_set_column_spacing(start_button_grid, 10); replay_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); gtk_widget_set_hexpand(GTK_WIDGET(replay_back_button), true); @@ -1292,14 +1978,28 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); - GtkWidget *hotkey_label = gtk_label_new("Press Alt+F1 to start/stop recording"); - gtk_grid_attach(grid, hotkey_label, 0, 0, 2, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 2, 1); + GtkWidget *a = gtk_label_new("Press"); + gtk_widget_set_halign(a, GTK_ALIGN_END); + + GtkWidget *b = gtk_label_new("to start/stop recording"); + gtk_widget_set_halign(b, GTK_ALIGN_START); + + record_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(record_hotkey_button), "Alt + F1"); + g_signal_connect(record_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), record_hotkey_button); + gtk_grid_attach(grid, a, 0, 0, 1, 1); + gtk_grid_attach(grid, record_hotkey_button, 1, 0, 1, 1); + gtk_grid_attach(grid, b, 2, 0, 1, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 3, 1); + + record_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + record_hotkey.keysym = XK_F1; + record_hotkey.hotkey_entry = record_hotkey_button; GtkWidget *save_icon = gtk_image_new_from_icon_name("document-save", GTK_ICON_SIZE_BUTTON); GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, 2, 2, 1); + gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, 2, 3, 1); gtk_grid_set_column_spacing(file_chooser_grid, 10); GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the video?"); gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_label), 0, 0, 1, 1); @@ -1312,7 +2012,7 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(file_chooser_grid, GTK_WIDGET(record_file_chooser_button), 1, 0, 1, 1); GtkGrid *container_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, 3, 2, 1); + gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, 3, 3, 1); gtk_grid_attach(container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); record_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); for(auto &supported_container : supported_containers) { @@ -1323,7 +2023,7 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_combo_box_set_active(GTK_COMBO_BOX(record_container), 0); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 4, 2, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 4, 3, 1); gtk_grid_set_column_spacing(start_button_grid, 10); record_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); gtk_widget_set_hexpand(GTK_WIDGET(record_back_button), true); @@ -1345,12 +2045,26 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); - GtkWidget *hotkey_label = gtk_label_new("Press Alt+F1 to start/stop streaming"); - gtk_grid_attach(grid, hotkey_label, 0, 0, 2, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 2, 1); + GtkWidget *a = gtk_label_new("Press"); + gtk_widget_set_halign(a, GTK_ALIGN_END); + + GtkWidget *b = gtk_label_new("to start/stop streaming"); + gtk_widget_set_halign(b, GTK_ALIGN_START); + + streaming_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(streaming_hotkey_button), "Alt + F1"); + g_signal_connect(streaming_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), streaming_hotkey_button); + gtk_grid_attach(grid, a, 0, 0, 1, 1); + gtk_grid_attach(grid, streaming_hotkey_button, 1, 0, 1, 1); + gtk_grid_attach(grid, b, 2, 0, 1, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 3, 1); + + streaming_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + streaming_hotkey.keysym = XK_F1; + streaming_hotkey.hotkey_entry = streaming_hotkey_button; GtkGrid *stream_service_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, 2, 2, 1); + gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, 2, 3, 1); gtk_grid_attach(stream_service_grid, gtk_label_new("Stream service: "), 0, 0, 1, 1); stream_service_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(stream_service_input_menu, "twitch", "Twitch"); @@ -1362,7 +2076,7 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(stream_service_grid, GTK_WIDGET(stream_service_input_menu), 1, 0, 1, 1); GtkGrid *stream_id_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(stream_id_grid), 0, 3, 2, 1); + gtk_grid_attach(grid, GTK_WIDGET(stream_id_grid), 0, 3, 3, 1); stream_key_label = GTK_LABEL(gtk_label_new("Stream key: ")); gtk_grid_attach(stream_id_grid, GTK_WIDGET(stream_key_label), 0, 0, 1, 1); stream_id_entry = GTK_ENTRY(gtk_entry_new()); @@ -1375,7 +2089,7 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(stream_id_grid, GTK_WIDGET(stream_id_entry), 1, 0, 1, 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 4, 2, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 4, 3, 1); gtk_grid_set_column_spacing(start_button_grid, 10); stream_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); gtk_widget_set_hexpand(GTK_WIDGET(stream_back_button), true); @@ -1401,77 +2115,6 @@ static gboolean on_destroy_window(GtkWidget *widget, GdkEvent *event, gpointer d return true; } -typedef gboolean (*KeyPressHandler)(GtkButton *button, gpointer userdata); -static void keypress_toggle_recording(bool recording_state, GtkButton *record_button, KeyPressHandler keypress_handler, GtkApplication *app) { - if(!gtk_widget_get_sensitive(GTK_WIDGET(record_button))) - return; - - if(!recording_state) { - keypress_handler(record_button, app); - } else if(recording_state) { - keypress_handler(record_button, app); - } -} - -static bool hotkey_pressed = false; -static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent *event, gpointer userdata) { - PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; - XEvent *ev = (XEvent*)xevent; - - if((ev->type == KeyPress || ev->type == KeyRelease) && XLookupKeysym(&ev->xkey, 0) == XK_F1 && (ev->xkey.state & Mod1Mask)) { - if(ev->type == KeyPress) { - if(hotkey_pressed) - return GDK_FILTER_CONTINUE; - hotkey_pressed = true; - } else if(ev->type == KeyRelease) { - hotkey_pressed = false; - return GDK_FILTER_CONTINUE; - } - - GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata->stack); - if(visible_page == page_navigation_userdata->recording_page) { - keypress_toggle_recording(recording, start_recording_button, on_start_recording_button_click, page_navigation_userdata->app); - } else if(visible_page == page_navigation_userdata->streaming_page) { - keypress_toggle_recording(streaming, start_streaming_button, on_start_streaming_button_click, page_navigation_userdata->app); - } else if(visible_page == page_navigation_userdata->replay_page && (ev->xkey.state & ShiftMask)) { - keypress_toggle_recording(replaying, start_replay_button, on_start_replay_button_click, page_navigation_userdata->app); - } else if(visible_page == page_navigation_userdata->replay_page && replaying && gpu_screen_recorder_process != -1) { - on_replay_save_button_click(nullptr, page_navigation_userdata->app); - } - } - - return GDK_FILTER_CONTINUE; -} - -static int xerror_dummy(Display *dpy, XErrorEvent *ee) { - return 0; -} - -static void grabkeys(Display *display) { - unsigned int numlockmask = 0; - KeyCode numlock_keycode = XKeysymToKeycode(display, XK_Num_Lock); - XModifierKeymap *modmap = XGetModifierMapping(display); - for(int i = 0; i < 8; ++i) { - for(int j = 0; j < modmap->max_keypermod; ++j) { - if(modmap->modifiermap[i * modmap->max_keypermod + j] == numlock_keycode) - numlockmask = (1 << i); - } - } - XFreeModifiermap(modmap); - - XErrorHandler prev_error_handler = XSetErrorHandler(xerror_dummy); - - Window root_window = DefaultRootWindow(display); - unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; - for(int i = 0; i < 4; ++i) { - XGrabKey(display, XKeysymToKeycode(display, XK_F1), Mod1Mask|modifiers[i], root_window, False, GrabModeAsync, GrabModeAsync); - XGrabKey(display, XKeysymToKeycode(display, XK_F1), Mod1Mask|ShiftMask|modifiers[i], root_window, False, GrabModeAsync, GrabModeAsync); - } - - XSync(display, False); - XSetErrorHandler(prev_error_handler); -} - static gboolean handle_child_process_death(gpointer userdata) { if(gpu_screen_recorder_process != -1) { int status; @@ -1526,6 +2169,13 @@ static void load_config() { }); if(!found_monitor) + config.main_config.record_area_option.clear(); + } + + if(config.main_config.record_area_option.empty()) { + if(nvfbc_installed) + config.main_config.record_area_option = "screen"; + else config.main_config.record_area_option = "window"; } @@ -1547,6 +2197,9 @@ static void load_config() { if(config.main_config.quality != "medium" && config.main_config.quality != "high" && config.main_config.quality != "very_high" && config.main_config.quality != "ultra") config.main_config.quality = "very_high"; + if(config.main_config.codec != "auto" && config.main_config.codec != "h264" && config.main_config.quality != "h265") + config.main_config.codec = "auto"; + if(config.streaming_config.streaming_service != "twitch" && config.streaming_config.streaming_service != "youtube" && config.streaming_config.streaming_service != "custom") config.streaming_config.streaming_service = "twitch"; @@ -1568,26 +2221,122 @@ static void load_config() { gtk_spin_button_set_value(area_width_entry, config.main_config.record_area_width); gtk_spin_button_set_value(area_height_entry, config.main_config.record_area_height); gtk_spin_button_set_value(fps_entry, config.main_config.fps); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button), config.main_config.merge_audio_tracks); for(const std::string &audio_input : config.main_config.audio_input) { add_audio_input_track(audio_input.c_str()); } gtk_combo_box_set_active_id(GTK_COMBO_BOX(quality_input_menu), config.main_config.quality.c_str()); + gtk_combo_box_set_active_id(GTK_COMBO_BOX(codec_input_menu), config.main_config.codec.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(stream_service_input_menu), config.streaming_config.streaming_service.c_str()); gtk_entry_set_text(stream_id_entry, config.streaming_config.stream_key.c_str()); + if(config.streaming_config.start_recording_hotkey.keysym) { + streaming_hotkey.keysym = config.streaming_config.start_recording_hotkey.keysym; + streaming_hotkey.modkey_mask = config.streaming_config.start_recording_hotkey.modifiers; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(streaming_hotkey_button), streaming_hotkey); + } gtk_button_set_label(record_file_chooser_button, config.record_config.save_directory.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(record_container), config.record_config.container.c_str()); + if(config.record_config.start_recording_hotkey.keysym) { + record_hotkey.keysym = config.record_config.start_recording_hotkey.keysym; + record_hotkey.modkey_mask = config.record_config.start_recording_hotkey.modifiers; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(record_hotkey_button), record_hotkey); + } gtk_button_set_label(replay_file_chooser_button, config.replay_config.save_directory.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(replay_container), config.replay_config.container.c_str()); gtk_spin_button_set_value(replay_time_entry, config.replay_config.replay_time); + if(config.replay_config.start_recording_hotkey.keysym) { + replay_start_stop_hotkey.keysym = config.replay_config.start_recording_hotkey.keysym; + replay_start_stop_hotkey.modkey_mask = config.replay_config.start_recording_hotkey.modifiers; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(replay_start_stop_hotkey_button), replay_start_stop_hotkey); + } + if(config.replay_config.save_recording_hotkey.keysym) { + replay_save_hotkey.keysym = config.replay_config.save_recording_hotkey.keysym; + replay_save_hotkey.modkey_mask = config.replay_config.save_recording_hotkey.modifiers; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(replay_save_hotkey_button), replay_save_hotkey); + } enable_stream_record_button_if_info_filled(); } +typedef enum { + GPU_VENDOR_AMD, + GPU_VENDOR_INTEL, + GPU_VENDOR_NVIDIA +} gpu_vendor; + +typedef struct { + gpu_vendor vendor; + int gpu_version; /* 0 if unknown */ +} gpu_info; + +static bool gl_get_gpu_info(Display *dpy, gpu_info *info) { + gsr_egl gl; + if(!gsr_egl_load(&gl, dpy)) { + fprintf(stderr, "Error: failed to load opengl\n"); + return false; + } + + bool supported = true; + const unsigned char *gl_vendor = gl.glGetString(GL_VENDOR); + const unsigned char *gl_renderer = gl.glGetString(GL_RENDERER); + + info->gpu_version = 0; + + if(!gl_vendor) { + fprintf(stderr, "Error: failed to get gpu vendor\n"); + supported = false; + goto end; + } + + if(strstr((const char*)gl_vendor, "AMD")) + info->vendor = GPU_VENDOR_AMD; + else if(strstr((const char*)gl_vendor, "Intel")) + info->vendor = GPU_VENDOR_INTEL; + else if(strstr((const char*)gl_vendor, "NVIDIA")) + info->vendor = GPU_VENDOR_NVIDIA; + else { + fprintf(stderr, "Error: unknown gpu vendor: %s\n", gl_vendor); + supported = false; + goto end; + } + + if(gl_renderer) { + if(info->vendor == GPU_VENDOR_NVIDIA) + sscanf((const char*)gl_renderer, "%*s %*s %*s %d", &info->gpu_version); + } + + end: + gsr_egl_unload(&gl); + return supported; +} + static void activate(GtkApplication *app, gpointer userdata) { - GtkWidget *window = gtk_application_window_new(app); + nvfbc_installed = is_nv_fbc_installed(); + + gpu_info gpu_inf; + if(!gl_get_gpu_info(gdk_x11_get_default_xdisplay(), &gpu_inf)) { + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "Failed to get OpenGL information. Make sure your are using a nvidia gpu and graphics drivers installed"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + g_application_quit(G_APPLICATION(app)); + return; + } + + // TODO: Remove once gpu screen recorder supports amd and intel properly + if(gpu_inf.vendor != GPU_VENDOR_NVIDIA) { + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "GPU Screen Recorder does currently only support NVIDIA GPUs. You are using a laptop with a NVIDIA gpu then make sure you are running in NVIDIA performance mode, to make sure that everything runs on your NVIDIA GPU"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + g_application_quit(G_APPLICATION(app)); + return; + } + + window = gtk_application_window_new(app); g_signal_connect(window, "destroy", G_CALLBACK(on_destroy_window), nullptr); gtk_window_set_title(GTK_WINDOW(window), "GPU Screen Recorder"); gtk_window_set_resizable(GTK_WINDOW(window), false); @@ -1615,18 +2364,18 @@ static void activate(GtkApplication *app, gpointer userdata) { page_navigation_userdata.streaming_page = streaming_page; g_signal_connect(replay_button, "clicked", G_CALLBACK(on_start_replay_click), &page_navigation_userdata); - g_signal_connect(replay_back_button, "clicked", G_CALLBACK(on_streaming_recording_page_back_click), &page_navigation_userdata); + g_signal_connect(replay_back_button, "clicked", G_CALLBACK(on_streaming_recording_replay_page_back_click), &page_navigation_userdata); g_signal_connect(record_button, "clicked", G_CALLBACK(on_start_recording_click), &page_navigation_userdata); - g_signal_connect(record_back_button, "clicked", G_CALLBACK(on_streaming_recording_page_back_click), &page_navigation_userdata); + g_signal_connect(record_back_button, "clicked", G_CALLBACK(on_streaming_recording_replay_page_back_click), &page_navigation_userdata); g_signal_connect(stream_button, "clicked", G_CALLBACK(on_start_streaming_click), &page_navigation_userdata); - g_signal_connect(stream_back_button, "clicked", G_CALLBACK(on_streaming_recording_page_back_click), &page_navigation_userdata); + g_signal_connect(stream_back_button, "clicked", G_CALLBACK(on_streaming_recording_replay_page_back_click), &page_navigation_userdata); + + xim = XOpenIM(gdk_x11_get_default_xdisplay(), NULL, NULL, NULL); + xic = XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, NULL); - Display *display = gdk_x11_get_default_xdisplay(); - grabkeys(display); GdkWindow *root_window = gdk_get_default_root_window(); - //gdk_window_set_events(root_window, GDK_BUTTON_PRESS_MASK); gdk_window_add_filter(root_window, hotkey_filter_callback, &page_navigation_userdata); g_timeout_add(1000, handle_child_process_death, app); @@ -1637,7 +2386,7 @@ static void activate(GtkApplication *app, gpointer userdata) { int main(int argc, char **argv) { setlocale(LC_ALL, "C"); - GtkApplication *app = gtk_application_new("com.dec05eba.gpu_screen_recorder", G_APPLICATION_FLAGS_NONE); + GtkApplication *app = gtk_application_new("com.dec05eba.gpu_screen_recorder", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), nullptr); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); -- cgit v1.2.3