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 --- src/main.cpp | 997 +++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 873 insertions(+), 124 deletions(-) (limited to 'src/main.cpp') 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