#include "config.hpp" extern "C" { #include "global_shortcuts.h" } #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef GSR_VERSION #define GSR_VERSION "unknown" #endif // Start/stop recording also means start/stop streaming and start/stop replay #define SHORTCUT_ID_START_STOP_RECORDING "gpu_screen_recorder_start_stop_recording" #define SHORTCUT_ID_PAUSE_UNPAUSE_RECORDING "gpu_screen_recorder_pause_unpause_recording" #define SHORTCUT_ID_SAVE_REPLAY "gpu_screen_recorder_save_replay" typedef struct { Display *display; GtkApplication *app; GtkButton *select_window_button; Window selected_window; } SelectWindowUserdata; typedef struct { GtkApplication *app; GtkStack *stack; GtkWidget *common_settings_page; GtkWidget *replay_page; GtkWidget *recording_page; GtkWidget *streaming_page; } PageNavigationUserdata; static bool window_hidden = false; static GtkWidget *window; static SelectWindowUserdata select_window_userdata; static PageNavigationUserdata page_navigation_userdata; static Cursor crosshair_cursor; static GtkSpinButton *fps_entry; static GtkSpinButton *video_bitrate_entry; static GtkGrid *area_size_grid; static GtkWidget *change_video_resolution_button; static GtkGrid *video_resolution_grid; static GtkSpinButton *area_width_entry; static GtkSpinButton *area_height_entry; static GtkSpinButton *video_width_entry; static GtkSpinButton *video_height_entry; static GtkComboBox *record_area_selection_menu; static GtkTreeModel *record_area_selection_model; static GtkComboBoxText *quality_input_menu; static GtkComboBoxText *audio_codec_input_menu; static GtkComboBoxText *color_range_input_menu; static GtkComboBoxText *framerate_mode_input_menu; static GtkComboBoxText *stream_service_input_menu; static GtkComboBoxText *record_container; static GtkComboBoxText *replay_container; static GtkComboBoxText *custom_stream_container; static GtkComboBox *video_codec_selection_menu; static GtkTreeModel *video_codec_selection_model; static GtkLabel *stream_key_label; static GtkButton *record_file_chooser_button; static GtkButton *replay_file_chooser_button; static GtkButton *stream_button; static GtkButton *record_button; static GtkButton *replay_button; static GtkButton *replay_back_button; static GtkButton *record_back_button; static GtkButton *stream_back_button; static GtkButton *replay_save_button; static GtkButton *start_recording_button; static GtkButton *pause_recording_button; static GtkButton *start_replay_button; static GtkButton *start_streaming_button; static GtkEntry *youtube_stream_id_entry; static GtkEntry *twitch_stream_id_entry; static GtkEntry *custom_stream_url_entry; 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_start_stop_hotkey_button; static GtkWidget *pause_unpause_hotkey_button; static GtkWidget *replay_start_stop_hotkey_button; static GtkWidget *replay_save_hotkey_button; static GtkWidget *streaming_start_stop_hotkey_button; static GtkWidget *merge_audio_tracks_button; static GtkWidget *show_recording_started_notification_button; static GtkWidget *show_recording_stopped_notification_button; static GtkWidget *show_recording_saved_notification_button; static GtkWidget *record_cursor_button; static GtkWidget *restore_portal_session_button; static GtkGrid *video_codec_grid; static GtkGrid *audio_codec_grid; static GtkGrid *color_range_grid; static GtkGrid *framerate_mode_grid; static GtkGrid *video_bitrate_grid; static GtkComboBoxText *view_combo_box; static GtkGrid *overclock_grid; static GtkWidget *overclock_button; static GtkGrid *recording_bottom_panel_grid; static GtkWidget *recording_record_time_label; static GtkWidget *recording_record_icon; static GtkGrid *custom_stream_container_grid; static GtkGrid *streaming_bottom_panel_grid; static GtkWidget *streaming_record_time_label; static GtkGrid *replay_bottom_panel_grid; static GtkWidget *replay_record_time_label; static GtkWidget *show_hide_menu_item; static GtkWidget *recording_menu_separator; static GtkWidget *start_stop_streaming_menu_item; static GtkWidget *start_stop_recording_menu_item; static GtkWidget *pause_recording_menu_item; static GtkWidget *start_stop_replay_menu_item; static GtkWidget *save_replay_menu_item; static GtkWidget *hide_window_when_recording_menu_item; static GtkGrid *recording_hotkeys_grid; static GtkGrid *replay_hotkeys_grid; static GtkGrid *streaming_hotkeys_grid; static GtkWidget *recording_hotkeys_not_supported_label; static GtkWidget *replay_hotkeys_not_supported_label; static GtkWidget *streaming_hotkeys_not_supported_label; static double record_start_time_sec = 0.0; static double pause_start_sec = 0.0; static double paused_time_offset_sec = 0.0; static XIM xim; static XIC xic; static bool replaying = false; static bool recording = false; static bool paused = false; static bool streaming = false; static pid_t gpu_screen_recorder_process = -1; static int prev_exit_status = -1; static Config config; static std::string record_file_current_filename; static bool nvfbc_installed = false; static Display *dpy = NULL; static bool flatpak = false; static bool showing_notification = false; static double notification_timeout_seconds = 0.0; static double notification_start_seconds = 0.0; static AppIndicator *app_indicator; static gsr_global_shortcuts global_shortcuts; static bool global_shortcuts_initialized = false; struct AudioInput { std::string name; std::string description; }; static std::vector audio_inputs; enum class HotkeyMode { NoAction, NewHotkey, Record }; static HotkeyMode hotkey_mode = HotkeyMode::NoAction; typedef gboolean (*hotkey_trigger_handler)(GtkButton *button, gpointer userdata); struct Hotkey { uint32_t modkey_mask = 0; KeySym keysym = None; GtkWidget *hotkey_entry = nullptr; GtkWidget *hotkey_active_label = nullptr; ConfigHotkey *config = nullptr; bool grab_success = false; GtkWidget *page = nullptr; hotkey_trigger_handler trigger_handler = nullptr; GtkButton *associated_button = nullptr; const char *shortcut_id = nullptr; }; static Hotkey *current_hotkey = nullptr; static Hotkey pressed_hotkey; static Hotkey latest_hotkey; static Hotkey streaming_start_stop_hotkey; static Hotkey record_start_stop_hotkey; static Hotkey pause_unpause_hotkey; static Hotkey replay_start_stop_hotkey; static Hotkey replay_save_hotkey; static Hotkey *hotkeys[] = { &streaming_start_stop_hotkey, &record_start_stop_hotkey, &pause_unpause_hotkey, &replay_start_stop_hotkey, &replay_save_hotkey }; static int num_hotkeys = 5; struct SupportedVideoCodecs { bool h264 = false; bool h264_software = false; bool hevc = false; bool hevc_hdr = false; bool hevc_10bit = false; bool av1 = false; bool av1_hdr = false; bool av1_10bit = false; bool vp8 = false; bool vp9 = false; }; struct vec2i { int x = 0; int y = 0; }; struct GsrMonitor { std::string name; vec2i size; }; struct SupportedCaptureOptions { bool window = false; bool focused = false; bool screen = false; bool portal = false; std::vector monitors; }; enum class DisplayServer { UNKNOWN, X11, WAYLAND }; struct SystemInfo { DisplayServer display_server = DisplayServer::UNKNOWN; bool is_steam_deck = false; }; enum class GpuVendor { UNKNOWN, AMD, INTEL, NVIDIA }; struct GpuInfo { GpuVendor vendor = GpuVendor::UNKNOWN; }; struct GsrInfo { SystemInfo system_info; GpuInfo gpu_info; SupportedVideoCodecs supported_video_codecs; SupportedCaptureOptions supported_capture_options; }; static GsrInfo gsr_info; enum class GsrInfoExitStatus { OK, FAILED_TO_RUN_COMMAND, OPENGL_FAILED, NO_DRM_CARD }; static GsrInfoExitStatus gsr_info_exit_status; enum class WaylandCompositor { UNKNOWN, HYPRLAND, KDE // kwin }; static WaylandCompositor wayland_compositor = WaylandCompositor::UNKNOWN; struct Container { const char *container_name; const char *file_extension; }; static const Container supported_containers[] = { { "mp4", "mp4" }, { "flv", "flv" }, { "matroska", "mkv" }, // TODO: Default to this on amd/intel, add (Recommended on AMD/Intel) { "mov", "mov" }, { "mpegts", "ts" }, { "hls", "m3u8" } }; struct AudioRow { GtkWidget *row; GtkComboBoxText *input_list; }; // Dumb hacks below!! why dont these fking paths work outside flatpak.. except in kde. TODO: fix this! static const char* get_tray_idle_icon_name() { if(flatpak) return "com.dec05eba.gpu_screen_recorder.tray-idle"; else return "/usr/share/icons/hicolor/32x32/status/com.dec05eba.gpu_screen_recorder.tray-idle.png"; } static const char* get_tray_recording_icon_name() { if(flatpak) return "com.dec05eba.gpu_screen_recorder.tray-recording"; else return "/usr/share/icons/hicolor/32x32/status/com.dec05eba.gpu_screen_recorder.tray-recording.png"; } static const char* get_tray_paused_icon_name() { if(flatpak) return "com.dec05eba.gpu_screen_recorder.tray-paused"; else return "/usr/share/icons/hicolor/32x32/status/com.dec05eba.gpu_screen_recorder.tray-paused.png"; } static bool is_program_installed(const StringView program_name) { const char *path = getenv("PATH"); if(!path) return false; bool program_installed = false; char full_program_path[PATH_MAX]; string_split_char(path, ':', [&](StringView line) -> bool { snprintf(full_program_path, sizeof(full_program_path), "%.*s/%.*s", (int)line.size, line.str, (int)program_name.size, program_name.str); if(access(full_program_path, F_OK) == 0) { program_installed = true; return false; } return true; }); return program_installed; } static bool is_inside_flatpak(void) { return getenv("FLATPAK_ID") != NULL; } static bool is_pkexec_installed() { if(flatpak) return system("flatpak-spawn --host pkexec --version") == 0; else return is_program_installed({ "pkexec", 6 }); } static bool flatpak_is_installed_as_system(void) { static bool installed_as_system = false; static bool checked = false; if(!checked) { checked = true; installed_as_system = system("flatpak-spawn --host flatpak run --system --command=pwd com.dec05eba.gpu_screen_recorder") == 0; } return installed_as_system; } static double clock_get_monotonic_seconds(void) { struct timespec ts; ts.tv_sec = 0; ts.tv_nsec = 0; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001; } static void show_window() { gdk_window_show(gtk_widget_get_window(window)); gtk_menu_item_set_label(GTK_MENU_ITEM(show_hide_menu_item), "Hide window"); window_hidden = false; } static void hide_window() { gdk_window_hide(gtk_widget_get_window(window)); gtk_menu_item_set_label(GTK_MENU_ITEM(show_hide_menu_item), "Show window"); window_hidden = true; } static void systray_show_hide_callback(GtkMenuItem*, gpointer) { if(window_hidden) { show_window(); } else { hide_window(); } } static void hide_window_when_recording_systray_callback(GtkMenuItem*, gpointer) { config.main_config.hide_window_when_recording = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item)); } static void start_stop_streaming_menu_item_systray_callback(GtkMenuItem*, gpointer userdata); static void start_stop_recording_systray_callback(GtkMenuItem*, gpointer userdata); static void pause_recording_systray_callback(GtkMenuItem*, gpointer userdata); static void start_stop_replay_systray_callback(GtkMenuItem*, gpointer userdata); static void save_replay_systray_callback(GtkMenuItem*, gpointer userdata); static void systray_exit_callback(GtkMenuItem*, gpointer) { gtk_window_close(GTK_WINDOW(window)); } enum class SystrayPage { FRONT, STREAMING, RECORDING, REPLAY }; static GtkMenuShell* create_systray_menu(GtkApplication *app, SystrayPage systray_page) { GtkMenuShell *menu = GTK_MENU_SHELL(gtk_menu_new()); show_hide_menu_item = gtk_menu_item_new_with_label("Hide window"); g_signal_connect(show_hide_menu_item, "activate", G_CALLBACK(systray_show_hide_callback), app); gtk_menu_shell_append(menu, show_hide_menu_item); { GtkMenuShell *options_menu = GTK_MENU_SHELL(gtk_menu_new()); hide_window_when_recording_menu_item = gtk_check_menu_item_new_with_label("Hide window when recording starts"); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item), config.main_config.hide_window_when_recording); g_signal_connect(hide_window_when_recording_menu_item, "activate", G_CALLBACK(hide_window_when_recording_systray_callback), nullptr); gtk_menu_shell_append(options_menu, hide_window_when_recording_menu_item); GtkWidget *options_menu_item = gtk_menu_item_new_with_label("Options"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(options_menu_item), GTK_WIDGET(options_menu)); gtk_menu_shell_append(menu, options_menu_item); } recording_menu_separator = gtk_separator_menu_item_new(); gtk_menu_shell_append(menu, recording_menu_separator); start_stop_streaming_menu_item = gtk_menu_item_new_with_label("Start streaming"); g_signal_connect(start_stop_streaming_menu_item, "activate", G_CALLBACK(start_stop_streaming_menu_item_systray_callback), app); gtk_menu_shell_append(menu, start_stop_streaming_menu_item); start_stop_recording_menu_item = gtk_menu_item_new_with_label("Start recording"); g_signal_connect(start_stop_recording_menu_item, "activate", G_CALLBACK(start_stop_recording_systray_callback), app); gtk_menu_shell_append(menu, start_stop_recording_menu_item); pause_recording_menu_item = gtk_menu_item_new_with_label("Pause recording"); g_signal_connect(pause_recording_menu_item, "activate", G_CALLBACK(pause_recording_systray_callback), app); gtk_menu_shell_append(menu, pause_recording_menu_item); start_stop_replay_menu_item = gtk_menu_item_new_with_label("Start replay"); g_signal_connect(start_stop_replay_menu_item, "activate", G_CALLBACK(start_stop_replay_systray_callback), app); gtk_menu_shell_append(menu, start_stop_replay_menu_item); save_replay_menu_item = gtk_menu_item_new_with_label("Save replay"); g_signal_connect(save_replay_menu_item, "activate", G_CALLBACK(save_replay_systray_callback), app); gtk_menu_shell_append(menu, save_replay_menu_item); gtk_menu_shell_append(menu, gtk_separator_menu_item_new()); GtkWidget *exit_menu_item = gtk_menu_item_new_with_label("Exit"); g_signal_connect(exit_menu_item, "activate", G_CALLBACK(systray_exit_callback), nullptr); gtk_menu_shell_append(menu, exit_menu_item); gtk_widget_show_all(GTK_WIDGET(menu)); gtk_widget_set_visible(recording_menu_separator, false); gtk_widget_set_visible(start_stop_streaming_menu_item, false); gtk_widget_set_visible(start_stop_recording_menu_item, false); gtk_widget_set_visible(pause_recording_menu_item, false); gtk_widget_set_visible(start_stop_replay_menu_item, false); gtk_widget_set_visible(save_replay_menu_item, false); switch(systray_page) { case SystrayPage::FRONT: break; case SystrayPage::STREAMING: gtk_widget_set_visible(recording_menu_separator, true); gtk_widget_set_visible(start_stop_streaming_menu_item, true); break; case SystrayPage::RECORDING: gtk_widget_set_visible(recording_menu_separator, true); gtk_widget_set_visible(start_stop_recording_menu_item, true); gtk_widget_set_visible(pause_recording_menu_item, true); gtk_widget_set_sensitive(pause_recording_menu_item, false); break; case SystrayPage::REPLAY: gtk_widget_set_visible(recording_menu_separator, true); gtk_widget_set_visible(start_stop_replay_menu_item, true); gtk_widget_set_visible(save_replay_menu_item, true); gtk_widget_set_sensitive(save_replay_menu_item, false); break; } return menu; } static void setup_systray(GtkApplication *app) { app_indicator = app_indicator_new("com.dec05eba.gpu_screen_recorder", "", APP_INDICATOR_CATEGORY_APPLICATION_STATUS); app_indicator_set_icon_full(app_indicator, get_tray_idle_icon_name(), "Idle"); // This triggers Gdk assert: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed, // dont know why but it works anyways app_indicator_set_title(app_indicator, "GPU Screen Recorder"); app_indicator_set_status(app_indicator, APP_INDICATOR_STATUS_ACTIVE); app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(app, SystrayPage::FRONT))); } static AudioInput parse_audio_device_line(const std::string &line) { AudioInput audio_input; const size_t space_index = line.find('|'); if(space_index == std::string::npos) return audio_input; const StringView audio_input_name = {line.c_str(), space_index}; const StringView audio_input_description = {line.c_str() + space_index + 1, line.size() - (space_index + 1)}; audio_input.name.assign(audio_input_name.str, audio_input_name.size); audio_input.description.assign(audio_input_description.str, audio_input_description.size); return audio_input; } static std::vector get_audio_devices() { std::vector inputs; FILE *f = popen("gpu-screen-recorder --list-audio-devices", "r"); if(!f) { fprintf(stderr, "error: 'gpu-screen-recorder --list-audio-devices' failed\n"); return inputs; } char output[16384]; ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f); if(bytes_read < 0 || ferror(f)) { fprintf(stderr, "error: failed to read 'gpu-screen-recorder --list-audio-devices' output\n"); pclose(f); return inputs; } output[bytes_read] = '\0'; string_split_char(output, '\n', [&](StringView line) { const std::string line_str(line.str, line.size); inputs.push_back(parse_audio_device_line(line_str)); return true; }); return inputs; } static void used_audio_input_loop_callback(GtkWidget *row, gpointer userdata) { const AudioRow *audio_row = (AudioRow*)g_object_get_data(G_OBJECT(row), "audio-row"); std::function &callback = *(std::function*)userdata; callback(audio_row); } static void for_each_used_audio_input(GtkListBox *list_box, std::function callback) { gtk_container_foreach(GTK_CONTAINER(list_box), used_audio_input_loop_callback, &callback); } static void drag_begin (GtkWidget *widget, GdkDragContext *context, gpointer) { GtkAllocation alloc; int x, y; double sx, sy; GtkWidget *row = gtk_widget_get_ancestor(widget, GTK_TYPE_LIST_BOX_ROW); gtk_widget_get_allocation(row, &alloc); cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, alloc.width, alloc.height); cairo_t *cr = cairo_create(surface); gtk_style_context_add_class(gtk_widget_get_style_context (row), "drag-icon"); gtk_widget_draw(row, cr); gtk_style_context_remove_class(gtk_widget_get_style_context (row), "drag-icon"); gtk_widget_translate_coordinates(widget, row, 0, 0, &x, &y); cairo_surface_get_device_scale(surface, &sx, &sy); cairo_surface_set_device_offset(surface, -x * sx, -y * sy); gtk_drag_set_icon_surface(context, surface); cairo_destroy(cr); cairo_surface_destroy(surface); } static void drag_data_get(GtkWidget *widget, GdkDragContext*, GtkSelectionData *selection_data, guint, guint, gpointer) { gtk_selection_data_set(selection_data, gdk_atom_intern_static_string("GTK_LIST_BOX_ROW"), 32, (const guchar *)&widget, sizeof(gpointer)); } static void drag_data_received(GtkWidget *widget, GdkDragContext*, gint, gint, GtkSelectionData *selection_data, guint, guint32, gpointer) { GtkWidget *target = widget; int pos = gtk_list_box_row_get_index(GTK_LIST_BOX_ROW (target)); GtkWidget *row = *(GtkWidget**)gtk_selection_data_get_data(selection_data); GtkWidget *source = gtk_widget_get_ancestor(row, GTK_TYPE_LIST_BOX_ROW); if (source == target) return; GtkWidget *list_box = gtk_widget_get_parent(source); g_object_ref(source); gtk_container_remove(GTK_CONTAINER(list_box), source); gtk_list_box_insert(GTK_LIST_BOX(list_box), source, pos); g_object_unref(source); } static bool is_video_capture_option_enabled(const char *str) { bool enabled = true; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) enabled = strcmp(str, "window") != 0 && strcmp(str, "focused") != 0; if(strcmp(str, "portal") == 0 && !gsr_info.supported_capture_options.portal) enabled = false; return enabled; } static bool is_video_codec_enabled(const char *str) { bool enabled = true; if(strcmp(str, "h264") == 0 && !gsr_info.supported_video_codecs.h264) enabled = false; if(strcmp(str, "h264_software") == 0 && !gsr_info.supported_video_codecs.h264_software) enabled = false; if(strcmp(str, "hevc") == 0 && !gsr_info.supported_video_codecs.hevc) enabled = false; if(strcmp(str, "hevc_hdr") == 0 && !gsr_info.supported_video_codecs.hevc_hdr) enabled = false; if(strcmp(str, "hevc_10bit") == 0 && !gsr_info.supported_video_codecs.hevc_10bit) enabled = false; if(strcmp(str, "av1") == 0 && !gsr_info.supported_video_codecs.av1) enabled = false; if(strcmp(str, "av1_hdr") == 0 && !gsr_info.supported_video_codecs.av1_hdr) enabled = false; if(strcmp(str, "av1_10bit") == 0 && !gsr_info.supported_video_codecs.av1_10bit) enabled = false; if(strcmp(str, "vp8") == 0 && !gsr_info.supported_video_codecs.vp8) enabled = false; if(strcmp(str, "vp9") == 0 && !gsr_info.supported_video_codecs.vp9) enabled = false; return enabled; } static std::string record_area_selection_menu_get_active_id() { std::string id_str; GtkTreeIter iter; if(!gtk_combo_box_get_active_iter(record_area_selection_menu, &iter)) return id_str; gchar *id; gtk_tree_model_get(record_area_selection_model, &iter, 1, &id, -1); id_str = id; g_free(id); return id_str; } static void record_area_selection_menu_set_active_id(const gchar *id) { if(!is_video_capture_option_enabled(id)) return; GtkTreeIter iter; if(!gtk_tree_model_get_iter_first(record_area_selection_model, &iter)) return; do { gchar *row_id = nullptr; gtk_tree_model_get(record_area_selection_model, &iter, 1, &row_id, -1); const bool found_row = strcmp(row_id, id) == 0; g_free(row_id); if(found_row) { gtk_combo_box_set_active_iter(record_area_selection_menu, &iter); break; } } while(gtk_tree_model_iter_next(record_area_selection_model, &iter)); } static std::string video_codec_selection_menu_get_active_id() { std::string id_str; GtkTreeIter iter; if(!gtk_combo_box_get_active_iter(video_codec_selection_menu, &iter)) return id_str; gchar *id; gtk_tree_model_get(video_codec_selection_model, &iter, 1, &id, -1); id_str = id; g_free(id); return id_str; } static void video_codec_selection_menu_set_active_id(const gchar *id) { if(!is_video_codec_enabled(id)) return; GtkTreeIter iter; if(!gtk_tree_model_get_iter_first(video_codec_selection_model, &iter)) return; do { gchar *row_id = nullptr; gtk_tree_model_get(video_codec_selection_model, &iter, 1, &row_id, -1); const bool found_row = strcmp(row_id, id) == 0; g_free(row_id); if(found_row) { gtk_combo_box_set_active_iter(video_codec_selection_menu, &iter); break; } } while(gtk_tree_model_iter_next(video_codec_selection_model, &iter)); } static void enable_stream_record_button_if_info_filled() { if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { const std::string selected_window_area = record_area_selection_menu_get_active_id(); if(strcmp(selected_window_area.c_str(), "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; } } 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), true); } static GtkWidget* create_used_audio_input_row(void) { char entry_name[] = "GTK_LIST_BOX_ROW"; const GtkTargetEntry entries[] = { { entry_name, GTK_TARGET_SAME_APP, 0 } }; GtkWidget *row = gtk_list_box_row_new(); GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); gtk_container_add(GTK_CONTAINER(row), box); GtkWidget *handle = gtk_event_box_new(); GtkWidget *image = gtk_image_new_from_icon_name("open-menu-symbolic", GTK_ICON_SIZE_MENU); gtk_container_add(GTK_CONTAINER(handle), image); gtk_container_add(GTK_CONTAINER(box), handle); GtkComboBoxText *input_list = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); for(const auto &audio_input : audio_inputs) { gtk_combo_box_text_append(input_list, audio_input.name.c_str(), audio_input.description.c_str()); } gtk_widget_set_hexpand(GTK_WIDGET(input_list), true); gtk_combo_box_set_active(GTK_COMBO_BOX(input_list), 0); //gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo), id); gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(input_list)); GtkWidget *remove_button = gtk_button_new_with_label("Remove"); gtk_widget_set_halign(remove_button, GTK_ALIGN_END); gtk_container_add(GTK_CONTAINER(box), remove_button); gtk_drag_source_set(handle, GDK_BUTTON1_MASK, entries, 1, GDK_ACTION_MOVE); g_signal_connect(handle, "drag-begin", G_CALLBACK(drag_begin), NULL); g_signal_connect(handle, "drag-data-get", G_CALLBACK(drag_data_get), NULL); gtk_drag_dest_set(row, GTK_DEST_DEFAULT_ALL, entries, 1, GDK_ACTION_MOVE); g_signal_connect(row, "drag-data-received", G_CALLBACK(drag_data_received), NULL); AudioRow *audio_row = new AudioRow(); audio_row->row = row; audio_row->input_list = input_list; g_object_set_data(G_OBJECT(row), "audio-row", audio_row); g_signal_connect(remove_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer userdata){ AudioRow *_audio_row = (AudioRow*)userdata; gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(_audio_row->row)), _audio_row->row); delete _audio_row; return true; }), audio_row); return row; } // Return true from |callback_func| to continue to the next row static void for_each_item_in_combo_box(GtkComboBox *combo_box, std::function callback_func) { const int id_column = gtk_combo_box_get_id_column(GTK_COMBO_BOX(combo_box)); const int text_column = gtk_combo_box_get_entry_text_column(GTK_COMBO_BOX(combo_box)); GtkTreeModel *tree_model = gtk_combo_box_get_model(combo_box); GtkTreeIter tree_iter; if(!gtk_tree_model_get_iter_first(tree_model, &tree_iter)) return; gint row_index = 0; do { gchar *row_id = nullptr; gtk_tree_model_get(tree_model, &tree_iter, id_column, &row_id, -1); gchar *row_text = nullptr; gtk_tree_model_get(tree_model, &tree_iter, text_column, &row_text, -1); bool cont = true; if(row_id && row_text) cont = callback_func(row_index, row_id, row_text); g_free(row_id); g_free(row_text); if(!cont) break; ++row_index; } while(gtk_tree_model_iter_next(tree_model, &tree_iter)); } static gint combo_box_text_get_row_by_label(GtkComboBox *combo_box, const char *label, std::string &id) { gint found_index = -1; for_each_item_in_combo_box(combo_box, [&found_index, &label, &id](gint row_index, const gchar *row_id, const gchar *row_text) { if(strcmp(row_text, label) == 0) { id = row_id; found_index = row_index; return false; } return true; }); return found_index; } static bool is_directory(const char *filepath) { struct stat file_stat; memset(&file_stat, 0, sizeof(file_stat)); int ret = stat(filepath, &file_stat); if(ret == 0) return S_ISDIR(file_stat.st_mode); return false; } static std::string get_date_str() { char str[128]; time_t now = time(NULL); struct tm *t = localtime(&now); strftime(str, sizeof(str)-1, "%Y-%m-%d_%H-%M-%S", t); return str; } static void save_configs() { config.main_config.record_area_option = record_area_selection_menu_get_active_id(); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { 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.video_width = gtk_spin_button_get_value_as_int(video_width_entry); config.main_config.video_height = gtk_spin_button_get_value_as_int(video_height_entry); config.main_config.fps = gtk_spin_button_get_value_as_int(fps_entry); config.main_config.video_bitrate = gtk_spin_button_get_value_as_int(video_bitrate_entry); config.main_config.merge_audio_tracks = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button)); config.main_config.change_video_resolution = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(change_video_resolution_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_combo_box_text_get_active_text(audio_row->input_list)); }); config.main_config.color_range = gtk_combo_box_get_active_id(GTK_COMBO_BOX(color_range_input_menu)); config.main_config.quality = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); config.main_config.codec = video_codec_selection_menu_get_active_id(); config.main_config.audio_codec = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_codec_input_menu)); config.main_config.framerate_mode = gtk_combo_box_get_active_id(GTK_COMBO_BOX(framerate_mode_input_menu)); config.main_config.advanced_view = strcmp(gtk_combo_box_get_active_id(GTK_COMBO_BOX(view_combo_box)), "advanced") == 0; config.main_config.overclock = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(overclock_button)); config.main_config.show_recording_started_notifications = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button)); config.main_config.show_recording_stopped_notifications = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button)); config.main_config.show_recording_saved_notifications = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button)); config.main_config.record_cursor = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_cursor_button)); config.main_config.hide_window_when_recording = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item)); config.main_config.restore_portal_session = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(restore_portal_session_button)); config.streaming_config.streaming_service = gtk_combo_box_get_active_id(GTK_COMBO_BOX(stream_service_input_menu)); config.streaming_config.youtube.stream_key = gtk_entry_get_text(youtube_stream_id_entry); config.streaming_config.twitch.stream_key = gtk_entry_get_text(twitch_stream_id_entry); config.streaming_config.custom.url = gtk_entry_get_text(custom_stream_url_entry); config.streaming_config.custom.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(custom_stream_container)); 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.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); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { for(int i = 0; i < num_hotkeys; ++i) { // This can also happen if we run multiple instances of gpu screen recorder, in which case it will fail to grab keys for the other windows. // We dont want to overwrite hotkeys in that case. if(hotkeys[i]->grab_success) { hotkeys[i]->config->keysym = hotkeys[i]->keysym; hotkeys[i]->config->modifiers = hotkeys[i]->modkey_mask; } } } save_config(config); } static void show_notification(GtkApplication *app, const char *title, const char *body, GNotificationPriority priority) { if(priority < G_NOTIFICATION_PRIORITY_URGENT) { notification_timeout_seconds = 3.0; } else { notification_timeout_seconds = 10.0; } // KDE doesn't show notifications when using desktop portal capture unless either DoNotDisturb.WhenScreenSharing kde config // has been changed by the user or if the priority for the notification is set as urgent if((recording || replaying || streaming) && wayland_compositor == WaylandCompositor::KDE) priority = G_NOTIFICATION_PRIORITY_URGENT; fprintf(stderr, "Notification: title: %s, body: %s\n", title, body); GNotification *notification = g_notification_new(title); g_notification_set_body(notification, body); g_notification_set_priority(notification, priority); g_application_send_notification(&app->parent, "gpu-screen-recorder", notification); notification_start_seconds = clock_get_monotonic_seconds(); showing_notification = true; } static bool window_has_atom(Display *display, Window _window, Atom atom) { Atom type; unsigned long len, bytes_left; int format; unsigned char *properties = nullptr; if(XGetWindowProperty(display, _window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) return false; if(properties) XFree(properties); return type != None; } static Window window_get_target_window_child(Display *display, Window _window) { if(_window == None) return None; Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False); if(!wm_state_atom) return None; if(window_has_atom(display, _window, wm_state_atom)) return _window; Window root; Window parent; Window *children = nullptr; unsigned int num_children = 0; if(!XQueryTree(display, _window, &root, &parent, &children, &num_children) || !children) return None; Window found_window = None; for(int i = num_children - 1; i >= 0; --i) { if(children[i] && window_has_atom(display, children[i], wm_state_atom)) { found_window = children[i]; goto finished; } } for(int i = num_children - 1; i >= 0; --i) { if(children[i]) { Window win = window_get_target_window_child(display, children[i]); if(win) { found_window = win; goto finished; } } } finished: XFree(children); return found_window; } static GdkFilterReturn filter_callback(GdkXEvent *xevent, GdkEvent*, gpointer userdata) { SelectWindowUserdata *_select_window_userdata = (SelectWindowUserdata*)userdata; XEvent *ev = (XEvent*)xevent; //assert(ev->type == ButtonPress); if(ev->type != ButtonPress) return GDK_FILTER_CONTINUE; Window target_win = ev->xbutton.subwindow; Window new_window = window_get_target_window_child(_select_window_userdata->display, target_win); if(new_window) target_win = new_window; int status = XUngrabPointer(_select_window_userdata->display, CurrentTime); if(!status) { fprintf(stderr, "failed to ungrab pointer!\n"); show_notification(_select_window_userdata->app, "GPU Screen Recorder", "Failed to ungrab pointer!", G_NOTIFICATION_PRIORITY_URGENT); exit(1); } if(target_win == None) { show_notification(_select_window_userdata->app, "GPU Screen Recorder", "No window selected!", G_NOTIFICATION_PRIORITY_URGENT); GdkScreen *screen = gdk_screen_get_default(); GdkWindow *root_window = gdk_screen_get_root_window(screen); gdk_window_remove_filter(root_window, filter_callback, _select_window_userdata); return GDK_FILTER_REMOVE; } std::string window_name; XTextProperty wm_name_prop; if(XGetWMName(_select_window_userdata->display, target_win, &wm_name_prop) && wm_name_prop.nitems > 0) { char **list_return = NULL; int num_items = 0; int ret = XmbTextPropertyToTextList(_select_window_userdata->display, &wm_name_prop, &list_return, &num_items); if((ret == Success || ret > 0) && list_return) { for(int i = 0; i < num_items; ++i) { window_name += list_return[i]; } XFreeStringList(list_return); } else { window_name += (char*)wm_name_prop.value; } } else { window_name += "(no name)"; } fprintf(stderr, "window name: %s, window id: %ld\n", window_name.c_str(), target_win); gtk_button_set_label(_select_window_userdata->select_window_button, window_name.c_str()); _select_window_userdata->selected_window = target_win; GdkScreen *screen = gdk_screen_get_default(); GdkWindow *root_window = gdk_screen_get_root_window(screen); gdk_window_remove_filter(root_window, filter_callback, _select_window_userdata); enable_stream_record_button_if_info_filled(); return GDK_FILTER_REMOVE; } static gboolean on_select_window_button_click(GtkButton *button, gpointer) { Display *display = gdk_x11_get_default_xdisplay(); select_window_userdata.display = display; select_window_userdata.select_window_button = button; select_window_userdata.selected_window = None; GdkScreen *screen = gdk_screen_get_default(); GdkWindow *root_window = gdk_screen_get_root_window(screen); gdk_window_set_events(root_window, GDK_BUTTON_PRESS_MASK); gdk_window_add_filter(root_window, filter_callback, &select_window_userdata); Window root = GDK_WINDOW_XID(root_window); /* Grab the pointer using target cursor, letting it room all over */ int status = XGrabPointer(display, root, False, ButtonPressMask, GrabModeAsync, GrabModeAsync, root, crosshair_cursor, CurrentTime); if (status != GrabSuccess) { fprintf(stderr, "failed to grab pointer!\n"); //GNotification *notification = g_notification_new("Failed to grab pointer to window selection!"); //g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_HIGH); //g_application_send_notification(app, "select-window", notification); } return true; } static gboolean on_change_video_resolution_button_click(GtkButton *button, gpointer) { const bool clicked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); const std::string window_str = record_area_selection_menu_get_active_id(); gtk_widget_set_visible(GTK_WIDGET(video_resolution_grid), clicked && window_str != "focused"); 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 || key_sym == None) 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*, XErrorEvent*) { return 0; } static bool x_failed = false; static int xerror_grab_error(Display*, XErrorEvent*) { x_failed = true; return 0; } static void ungrab_keyboard(Display *display) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return; if(current_hotkey) { gtk_grab_remove(current_hotkey->hotkey_entry); gtk_widget_set_visible(current_hotkey->hotkey_active_label, false); } 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(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return true; 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) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return; for(int i = 0; i < num_hotkeys; ++i) { grab_ungrab_hotkey_combo(display, *hotkeys[i], 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()); } static bool replace_grabbed_keys_depending_on_active_page() { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return true; for(int i = 0; i < num_hotkeys; ++i) { hotkeys[i]->grab_success = false; } ungrab_keys(gdk_x11_get_default_xdisplay()); const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata.stack); bool keys_successfully_grabbed = true; for(int i = 0; i < num_hotkeys; ++i) { if(visible_page == hotkeys[i]->page) { hotkey_mode = HotkeyMode::Record; hotkeys[i]->grab_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), *hotkeys[i], true); if(hotkeys[i]->grab_success) { set_hotkey_text_from_hotkey_data(GTK_ENTRY(hotkeys[i]->hotkey_entry), *hotkeys[i]); } else { gtk_entry_set_text(GTK_ENTRY(hotkeys[i]->hotkey_entry), ""); hotkeys[i]->keysym = 0; hotkeys[i]->modkey_mask = 0; keys_successfully_grabbed = false; } } } return keys_successfully_grabbed; } static bool is_monitor_capture_drm() { return gsr_info.system_info.display_server == DisplayServer::WAYLAND || gsr_info.gpu_info.vendor != GpuVendor::NVIDIA; } static bool show_pkexec_flatpak_error_if_needed() { const std::string window_str = record_area_selection_menu_get_active_id(); if(is_monitor_capture_drm() && window_str != "window" && window_str != "focused" && window_str != "portal") { if(!is_pkexec_installed()) { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "pkexec needs to be installed to record a monitor with an AMD/Intel GPU. Please install and run polkit. Alternatively, record a single window or use portal option which doesn't require root access."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); return true; } if(flatpak && !flatpak_is_installed_as_system()) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "GPU Screen Recorder needs to be installed system-wide to record your monitor on Wayland when not using the portal option. To install GPU Screen recorder system-wide, you can run this command:\n" "flatpak install --system com.dec05eba.gpu_screen_recorder\n"); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); } else { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "GPU Screen Recorder needs to be installed system-wide to record your monitor on AMD/Intel when not using the portal option. To install GPU Screen recorder system-wide, you can run this command:\n" "flatpak install --system com.dec05eba.gpu_screen_recorder\n" "Alternatively, record a single window which doesn't have this restriction."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); } return true; } } return false; } static void show_bugged_driver_warning() { if(gsr_info.gpu_info.vendor != GpuVendor::AMD) return; const std::string video_codec = video_codec_selection_menu_get_active_id(); if((video_codec == "hevc" || video_codec == "hevc_10bit" || video_codec == "hevc_hdr") && !config.main_config.hevc_amd_bug_warning_shown) { GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, "There is an AMD driver bug and FFmpeg bug that causes black bars to appear on the sides of the video at certain resolutions when using HEVC codec.\n" "Select H264 video codec instead if this is an issue for you."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); config.main_config.hevc_amd_bug_warning_shown = true; } if((video_codec == "av1" || video_codec == "av1_10bit" || video_codec == "av1_hdr") && !config.main_config.av1_amd_bug_warning_shown) { GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, "There is an AMD hardware bug that causes black bars to appear on the sides of the video at certain resolutions when using AV1 codec.\n" "Select H264 video codec instead if this is an issue for you."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); config.main_config.av1_amd_bug_warning_shown = true; } } static gboolean on_start_replay_click(GtkButton*, gpointer userdata) { if(show_pkexec_flatpak_error_if_needed()) return true; show_bugged_driver_warning(); PageNavigationUserdata *_page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(_page_navigation_userdata->stack, _page_navigation_userdata->replay_page); app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(_page_navigation_userdata->app, SystrayPage::REPLAY))); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) replace_grabbed_keys_depending_on_active_page(); return true; } static gboolean on_start_recording_click(GtkButton*, gpointer userdata) { if(show_pkexec_flatpak_error_if_needed()) return true; show_bugged_driver_warning(); PageNavigationUserdata *_page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(_page_navigation_userdata->stack, _page_navigation_userdata->recording_page); app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(_page_navigation_userdata->app, SystrayPage::RECORDING))); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) replace_grabbed_keys_depending_on_active_page(); return true; } void on_stream_key_icon_click(GtkWidget *widget, gpointer) { gboolean visible = gtk_entry_get_visibility(GTK_ENTRY(widget)); gtk_entry_set_visibility(GTK_ENTRY(widget), !visible); gtk_entry_set_icon_from_icon_name(GTK_ENTRY(widget), GTK_ENTRY_ICON_SECONDARY, visible ? "view-reveal-symbolic" : "view-conceal-symbolic"); } static gboolean on_start_streaming_click(GtkButton*, gpointer userdata) { if(show_pkexec_flatpak_error_if_needed()) return true; show_bugged_driver_warning(); 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 && !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button))) { 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 or select to merge audio tracks."); 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); app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(_page_navigation_userdata->app, SystrayPage::STREAMING))); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) replace_grabbed_keys_depending_on_active_page(); return true; } static gboolean on_streaming_recording_replay_page_back_click(GtkButton*, 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; app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(_page_navigation_userdata->app, SystrayPage::FRONT))); return true; } static gboolean file_choose_button_click_handler(GtkButton *button, const char *title, GtkFileChooserAction file_action, std::string container_file_ext) { GtkWidget *file_chooser_dialog = gtk_file_chooser_dialog_new(title, nullptr, file_action, "Cancel", GTK_RESPONSE_CANCEL, "Save", GTK_RESPONSE_ACCEPT, nullptr); if(file_action != GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER) { const std::string name = "Video_" + get_date_str() + "." + container_file_ext; gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser_dialog), name.c_str()); } int res = gtk_dialog_run(GTK_DIALOG(file_chooser_dialog)); if(res == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(file_chooser_dialog)); gtk_button_set_label(button, filename); g_free(filename); } gtk_widget_destroy(file_chooser_dialog); return true; } static gboolean on_record_file_choose_button_click(GtkButton *button, gpointer) { gboolean res = file_choose_button_click_handler(button, "Where do you want to save the video?", GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, gtk_combo_box_text_get_active_text(record_container)); config.record_config.save_directory = gtk_button_get_label(button); return res; } static gboolean on_replay_file_chooser_button_click(GtkButton *button, gpointer) { gboolean res = file_choose_button_click_handler(button, "Where do you want to save the replays?", GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, gtk_combo_box_text_get_active_text(replay_container)); config.replay_config.save_directory = gtk_button_get_label(button); return res; } static bool kill_gpu_screen_recorder_get_result(bool *already_dead) { *already_dead = true; bool exit_success = true; if(gpu_screen_recorder_process != -1) { *already_dead = false; int status; int wait_result = waitpid(gpu_screen_recorder_process, &status, WNOHANG); if(wait_result == -1) { perror("waitpid failed"); exit_success = false; } else if(wait_result == 0) { // the process is still running kill(gpu_screen_recorder_process, SIGINT); if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) { perror("waitpid failed"); exit_success = false; } else { exit_success = WIFEXITED(status) && WEXITSTATUS(status) == 0; } } else { exit_success = WIFEXITED(status) && WEXITSTATUS(status) == 0; } gpu_screen_recorder_process = -1; } return exit_success; } static void add_audio_command_line_args(std::vector &args, 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 += gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_row->input_list)); }); 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", gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_row->input_list)) }); }); } } static void change_container_if_codec_not_supported(const std::string &video_codec, const gchar **container_str) { if(strcmp(video_codec.c_str(), "vp8") == 0 || strcmp(video_codec.c_str(), "vp9") == 0) { if(strcmp(*container_str, "webm") != 0 && strcmp(*container_str, "matroska") != 0) { fprintf(stderr, "Warning: container '%s' is not compatible with video codec '%s', using webm container instead\n", *container_str, video_codec.c_str()); *container_str = "webm"; } } else if(strcmp(*container_str, "webm") == 0) { fprintf(stderr, "Warning: container webm is not compatible with video codec '%s', using mp4 container instead\n", video_codec.c_str()); *container_str = "mp4"; } } static bool switch_video_codec_to_usable_hardware_encoder(std::string &video_codec) { if(gsr_info.supported_video_codecs.h264) { video_codec = "h264"; return true; } else if(gsr_info.supported_video_codecs.hevc) { video_codec = "hevc"; return true; } else if(gsr_info.supported_video_codecs.av1) { video_codec = "av1"; return true; } else if(gsr_info.supported_video_codecs.vp8) { video_codec = "vp8"; return true; } else if(gsr_info.supported_video_codecs.vp9) { video_codec = "vp9"; return true; } return false; } static void add_quality_command_line_args(std::vector &args, const char *quality_input_str, const char *video_bitrate_str) { if(strcmp(quality_input_str, "custom") == 0) { args.push_back("-bm"); args.push_back("cbr"); args.push_back("-q"); args.push_back(video_bitrate_str); } else { args.push_back("-q"); args.push_back(quality_input_str); } } static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdata) { GtkApplication *app = (GtkApplication*)userdata; const gchar *dir = gtk_button_get_label(replay_file_chooser_button); int exit_status = prev_exit_status; prev_exit_status = -1; if(replaying) { bool already_dead = true; bool exit_success = kill_gpu_screen_recorder_get_result(&already_dead); gtk_button_set_label(button, "Start replay"); replaying = false; gtk_widget_set_sensitive(GTK_WIDGET(replay_back_button), true); gtk_widget_set_sensitive(GTK_WIDGET(replay_save_button), false); gtk_widget_set_opacity(GTK_WIDGET(replay_bottom_panel_grid), 0.5); gtk_label_set_text(GTK_LABEL(replay_record_time_label), "00:00:00"); gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_replay_menu_item), "Start replay"); gtk_widget_set_sensitive(save_replay_menu_item, false); app_indicator_set_icon_full(app_indicator, get_tray_idle_icon_name(), "Idle"); if(exit_status == 10) { show_notification(app, "GPU Screen Recorder", "You need to have pkexec installed and a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_status == 50) { show_notification(app, "GPU Screen Recorder", "Desktop portal capture failed. Either you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_status == 60) { // Canceled by the user } else if(!exit_success || (already_dead && exit_status != 0)) { show_notification(app, "GPU Screen Recorder", "Failed to start replay. Either your graphics card doesn't support GPU Screen Recorder with the settings you used or you don't have enough disk space to record a video", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_success) { if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button))) show_notification(app, "GPU Screen Recorder", "Stopped replay", G_NOTIFICATION_PRIORITY_NORMAL); } return true; } save_configs(); const int fps = gtk_spin_button_get_value_as_int(fps_entry); const int replay_time = gtk_spin_button_get_value_as_int(replay_time_entry); char dir_tmp[PATH_MAX]; strcpy(dir_tmp, dir); if(create_directory_recursive(dir_tmp) != 0) { std::string notification_body = std::string("Failed to start replay. Failed to create ") + dir_tmp; show_notification(app, "GPU Screen Recorder", notification_body.c_str(), G_NOTIFICATION_PRIORITY_URGENT); return true; } bool follow_focused = false; std::string window_str = record_area_selection_menu_get_active_id(); if(window_str == "window") { if(select_window_userdata.selected_window == None) { fprintf(stderr, "No window selected!\n"); return true; } window_str = std::to_string(select_window_userdata.selected_window); } else if(window_str == "focused") { follow_focused = true; } const std::string fps_str = std::to_string(fps); const std::string replay_time_str = std::to_string(replay_time); const bool change_video_resolution = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(change_video_resolution_button)); const gchar* container_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(replay_container)); const gchar* color_range_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(color_range_input_menu)); const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); const gchar* audio_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_codec_input_menu)); const gchar* framerate_mode_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(framerate_mode_input_menu)); const bool record_cursor = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_cursor_button)); const bool restore_portal_session = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(restore_portal_session_button)); const std::string video_bitrate_str = std::to_string(gtk_spin_button_get_value_as_int(video_bitrate_entry)); const int record_width = follow_focused ? gtk_spin_button_get_value_as_int(area_width_entry) : gtk_spin_button_get_value_as_int(video_width_entry); const int record_height = follow_focused ? gtk_spin_button_get_value_as_int(area_height_entry) : gtk_spin_button_get_value_as_int(video_height_entry); const char *encoder = "gpu"; std::string video_codec_input_str = video_codec_selection_menu_get_active_id(); if(video_codec_input_str == "h264_software") { video_codec_input_str = "h264"; encoder = "cpu"; } else if(video_codec_input_str == "auto") { if(!switch_video_codec_to_usable_hardware_encoder(video_codec_input_str)) { video_codec_input_str = "h264"; encoder = "cpu"; } } change_container_if_codec_not_supported(video_codec_input_str, &container_str); 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, "-k", video_codec_input_str.c_str(), "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-restore-portal-session", restore_portal_session ? "yes" : "no", "-cr", color_range_input_str, "-r", replay_time_str.c_str(), "-encoder", encoder, "-o", dir }; add_quality_command_line_args(args, quality_input_str, video_bitrate_str.c_str()); if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(overclock_button))) args.insert(args.end(), { "-oc", "yes" }); if(strcmp(framerate_mode_input_str, "auto") != 0) args.insert(args.end(), { "-fm", framerate_mode_input_str }); std::string merge_audio_tracks_arg_value; add_audio_command_line_args(args, merge_audio_tracks_arg_value); if(follow_focused || change_video_resolution) args.insert(args.end(), { "-s", area }); args.push_back(NULL); if(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item))) { hide_window(); } pid_t parent_pid = getpid(); pid_t pid = fork(); if(pid == -1) { perror("failed to fork"); show_notification(app, "GPU Screen Recorder", "Failed to start replay (failed to fork)", G_NOTIFICATION_PRIORITY_URGENT); return true; } else if(pid == 0) { /* child process */ if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) { perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed"); _exit(3); } if(getppid() != parent_pid) _exit(3); execvp(args[0], (char* const*)args.data()); perror("failed to launch gpu-screen-recorder"); _exit(127); } else { /* parent process */ gpu_screen_recorder_process = pid; } replaying = true; gtk_button_set_label(button, "Stop replay"); gtk_widget_set_sensitive(GTK_WIDGET(replay_back_button), false); gtk_widget_set_sensitive(GTK_WIDGET(replay_save_button), true); gtk_widget_set_opacity(GTK_WIDGET(replay_bottom_panel_grid), 1.0); gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_replay_menu_item), "Stop replay"); gtk_widget_set_sensitive(save_replay_menu_item, true); app_indicator_set_icon_full(app_indicator, get_tray_recording_icon_name(), "Recording"); if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button))) show_notification(app, "GPU Screen Recorder", "Started replay", G_NOTIFICATION_PRIORITY_NORMAL); record_start_time_sec = clock_get_monotonic_seconds(); return true; } static gboolean on_replay_save_button_click(GtkButton*, gpointer userdata) { if(gpu_screen_recorder_process == -1) return true; GtkApplication *app = (GtkApplication*)userdata; kill(gpu_screen_recorder_process, SIGUSR1); if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button))) show_notification(app, "GPU Screen Recorder", "Saved replay", G_NOTIFICATION_PRIORITY_NORMAL); return true; } static gboolean on_pause_unpause_button_click(GtkButton*, gpointer) { if(!recording) return true; if(gpu_screen_recorder_process == -1) return true; kill(gpu_screen_recorder_process, SIGUSR2); paused = !paused; if(paused) { gtk_button_set_label(pause_recording_button, "Unpause recording"); gtk_image_set_from_icon_name(GTK_IMAGE(recording_record_icon), "media-playback-pause", GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_menu_item_set_label(GTK_MENU_ITEM(pause_recording_menu_item), "Unpause recording"); app_indicator_set_icon_full(app_indicator, get_tray_paused_icon_name(), "Paused"); pause_start_sec = clock_get_monotonic_seconds(); } else { gtk_button_set_label(pause_recording_button, "Pause recording"); gtk_image_set_from_icon_name(GTK_IMAGE(recording_record_icon), "media-record", GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_menu_item_set_label(GTK_MENU_ITEM(pause_recording_menu_item), "Pause recording"); app_indicator_set_icon_full(app_indicator, get_tray_recording_icon_name(), "Recording"); paused_time_offset_sec += (clock_get_monotonic_seconds() - pause_start_sec); } return true; } static gboolean on_start_recording_button_click(GtkButton *button, gpointer userdata) { GtkApplication *app = (GtkApplication*)userdata; const gchar *dir = gtk_button_get_label(record_file_chooser_button); int exit_status = prev_exit_status; prev_exit_status = -1; if(recording) { bool already_dead = true; bool exit_success = kill_gpu_screen_recorder_get_result(&already_dead); gtk_button_set_label(button, "Start recording"); recording = false; gtk_widget_set_sensitive(GTK_WIDGET(record_back_button), true); paused = false; gtk_widget_set_sensitive(GTK_WIDGET(pause_recording_button), false); gtk_button_set_label(pause_recording_button, "Pause recording"); gtk_widget_set_opacity(GTK_WIDGET(recording_bottom_panel_grid), 0.5); gtk_image_set_from_icon_name(GTK_IMAGE(recording_record_icon), "media-record", GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_label_set_text(GTK_LABEL(recording_record_time_label), "00:00:00"); gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_recording_menu_item), "Start recording"); gtk_menu_item_set_label(GTK_MENU_ITEM(pause_recording_menu_item), "Pause recording"); gtk_widget_set_sensitive(pause_recording_menu_item, false); app_indicator_set_icon_full(app_indicator, get_tray_idle_icon_name(), "Idle"); if(exit_status == 10) { show_notification(app, "GPU Screen Recorder", "You need to have pkexec installed and a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_status == 50) { show_notification(app, "GPU Screen Recorder", "Desktop portal capture failed. Either you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_status == 60) { // Canceled by the user } else if(!exit_success || (already_dead && exit_status != 0)) { show_notification(app, "GPU Screen Recorder", "Failed to save video. Either your graphics card doesn't support GPU Screen Recorder with the settings you used or you don't have enough disk space to record a video. Start this GPU Screen Recorder GUI application from the terminal to see more information when this failure happens", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_success) { if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button))) { const 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); } } return true; } save_configs(); const int fps = gtk_spin_button_get_value_as_int(fps_entry); bool follow_focused = false; std::string window_str = record_area_selection_menu_get_active_id(); if(window_str == "window") { if(select_window_userdata.selected_window == None) { fprintf(stderr, "No window selected!\n"); return true; } window_str = std::to_string(select_window_userdata.selected_window); } else if(window_str == "focused") { follow_focused = true; } const std::string fps_str = std::to_string(fps); const bool change_video_resolution = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(change_video_resolution_button)); const gchar* container_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(record_container)); const gchar* container_name = gtk_combo_box_text_get_active_text(record_container); const gchar* color_range_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(color_range_input_menu)); const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); const gchar* audio_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_codec_input_menu)); const gchar* framerate_mode_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(framerate_mode_input_menu)); const bool record_cursor = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_cursor_button)); const bool restore_portal_session = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(restore_portal_session_button)); const std::string video_bitrate_str = std::to_string(gtk_spin_button_get_value_as_int(video_bitrate_entry)); const int record_width = follow_focused ? gtk_spin_button_get_value_as_int(area_width_entry) : gtk_spin_button_get_value_as_int(video_width_entry); const int record_height = follow_focused ? gtk_spin_button_get_value_as_int(area_height_entry) : gtk_spin_button_get_value_as_int(video_height_entry); const char *encoder = "gpu"; std::string video_codec_input_str = video_codec_selection_menu_get_active_id(); if(video_codec_input_str == "h264_software") { video_codec_input_str = "h264"; encoder = "cpu"; } else if(video_codec_input_str == "auto") { if(!switch_video_codec_to_usable_hardware_encoder(video_codec_input_str)) { video_codec_input_str = "h264"; encoder = "cpu"; } } change_container_if_codec_not_supported(video_codec_input_str, &container_str); char dir_tmp[PATH_MAX]; strcpy(dir_tmp, dir); if(create_directory_recursive(dir_tmp) != 0) { std::string notification_body = std::string("Failed to start recording. Failed to create ") + dir_tmp; show_notification(app, "GPU Screen Recorder", notification_body.c_str(), G_NOTIFICATION_PRIORITY_URGENT); return true; } record_file_current_filename = std::string(dir_tmp) + "/Video_" + get_date_str() + "." + container_name; 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, "-k", video_codec_input_str.c_str(), "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-restore-portal-session", restore_portal_session ? "yes" : "no", "-cr", color_range_input_str, "-encoder", encoder, "-o", record_file_current_filename.c_str() }; add_quality_command_line_args(args, quality_input_str, video_bitrate_str.c_str()); if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(overclock_button))) args.insert(args.end(), { "-oc", "yes" }); if(strcmp(framerate_mode_input_str, "auto") != 0) args.insert(args.end(), { "-fm", framerate_mode_input_str }); std::string merge_audio_tracks_arg_value; add_audio_command_line_args(args, merge_audio_tracks_arg_value); if(follow_focused || change_video_resolution) args.insert(args.end(), { "-s", area }); args.push_back(NULL); if(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item))) { hide_window(); } pid_t parent_pid = getpid(); pid_t pid = fork(); if(pid == -1) { perror("failed to fork"); show_notification(app, "GPU Screen Recorder", "Failed to start recording (failed to fork)", G_NOTIFICATION_PRIORITY_URGENT); return true; } else if(pid == 0) { /* child process */ if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) { perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed"); _exit(3); } if(getppid() != parent_pid) _exit(3); execvp(args[0], (char* const*)args.data()); perror("failed to launch gpu-screen-recorder"); _exit(127); } else { /* parent process */ gpu_screen_recorder_process = pid; } recording = true; gtk_button_set_label(button, "Stop recording"); gtk_widget_set_sensitive(GTK_WIDGET(record_back_button), false); gtk_widget_set_sensitive(GTK_WIDGET(pause_recording_button), true); gtk_widget_set_opacity(GTK_WIDGET(recording_bottom_panel_grid), 1.0); gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_recording_menu_item), "Stop recording"); gtk_widget_set_sensitive(pause_recording_menu_item, true); app_indicator_set_icon_full(app_indicator, get_tray_recording_icon_name(), "Recording"); if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button))) show_notification(app, "GPU Screen Recorder", "Started recording", G_NOTIFICATION_PRIORITY_NORMAL); record_start_time_sec = clock_get_monotonic_seconds(); paused_time_offset_sec = 0.0; return true; } static gboolean on_start_streaming_button_click(GtkButton *button, gpointer userdata) { GtkApplication *app = (GtkApplication*)userdata; int exit_status = prev_exit_status; prev_exit_status = -1; if(streaming) { bool already_dead = true; bool exit_success = kill_gpu_screen_recorder_get_result(&already_dead); gtk_button_set_label(button, "Start streaming"); streaming = false; gtk_widget_set_sensitive(GTK_WIDGET(stream_back_button), true); gtk_widget_set_opacity(GTK_WIDGET(streaming_bottom_panel_grid), 0.5); gtk_label_set_text(GTK_LABEL(streaming_record_time_label), "00:00:00"); gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_streaming_menu_item), "Start streaming"); app_indicator_set_icon_full(app_indicator, get_tray_idle_icon_name(), "Idle"); if(exit_status == 10) { show_notification(app, "GPU Screen Recorder", "You need to have pkexec installed and a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_status == 50) { show_notification(app, "GPU Screen Recorder", "Desktop portal capture failed. Either you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_status == 60) { // Canceled by the user } else if(!exit_success || (already_dead && exit_status != 0)) { show_notification(app, "GPU Screen Recorder", "Failed to stream video. There is either an error in your streaming config or your graphics card doesn't support GPU Screen Recorder with the settings you used", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_success) { if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button))) show_notification(app, "GPU Screen Recorder", "Stopped streaming", G_NOTIFICATION_PRIORITY_NORMAL); } return true; } save_configs(); const int fps = gtk_spin_button_get_value_as_int(fps_entry); bool follow_focused = false; std::string window_str = record_area_selection_menu_get_active_id(); if(window_str == "window") { if(select_window_userdata.selected_window == None) { fprintf(stderr, "No window selected!\n"); return true; } window_str = std::to_string(select_window_userdata.selected_window); } else if(window_str == "focused") { follow_focused = true; } const std::string fps_str = std::to_string(fps); const bool change_video_resolution = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(change_video_resolution_button)); std::string stream_url; const gchar *container_str = "flv"; const gchar *stream_service = gtk_combo_box_get_active_id(GTK_COMBO_BOX(stream_service_input_menu)); if(strcmp(stream_service, "twitch") == 0) { stream_url = "rtmp://live.twitch.tv/app/"; stream_url += gtk_entry_get_text(twitch_stream_id_entry); } else if(strcmp(stream_service, "youtube") == 0) { stream_url = "rtmp://a.rtmp.youtube.com/live2/"; stream_url += gtk_entry_get_text(youtube_stream_id_entry); } else if(strcmp(stream_service, "custom") == 0) { stream_url = gtk_entry_get_text(custom_stream_url_entry); container_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(custom_stream_container)); if(stream_url.size() >= 7 && strncmp(stream_url.c_str(), "rtmp://", 7) == 0) {} else if(stream_url.size() >= 8 && strncmp(stream_url.c_str(), "rtmps://", 8) == 0) {} else if(stream_url.size() >= 7 && strncmp(stream_url.c_str(), "rtsp://", 7) == 0) {} else if(stream_url.size() >= 6 && strncmp(stream_url.c_str(), "srt://", 6) == 0) {} else if(stream_url.size() >= 7 && strncmp(stream_url.c_str(), "http://", 7) == 0) {} else if(stream_url.size() >= 8 && strncmp(stream_url.c_str(), "https://", 8) == 0) {} else if(stream_url.size() >= 6 && strncmp(stream_url.c_str(), "tcp://", 6) == 0) {} else if(stream_url.size() >= 6 && strncmp(stream_url.c_str(), "udp://", 6) == 0) {} else stream_url = "rtmp://" + stream_url; } const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); const gchar* color_range_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(color_range_input_menu)); const gchar* audio_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_codec_input_menu)); const gchar* framerate_mode_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(framerate_mode_input_menu)); const bool record_cursor = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_cursor_button)); const bool restore_portal_session = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(restore_portal_session_button)); const std::string video_bitrate_str = std::to_string(gtk_spin_button_get_value_as_int(video_bitrate_entry)); const int record_width = follow_focused ? gtk_spin_button_get_value_as_int(area_width_entry) : gtk_spin_button_get_value_as_int(video_width_entry); const int record_height = follow_focused ? gtk_spin_button_get_value_as_int(area_height_entry) : gtk_spin_button_get_value_as_int(video_height_entry); const char *encoder = "gpu"; std::string video_codec_input_str = video_codec_selection_menu_get_active_id(); if(video_codec_input_str == "h264_software") { video_codec_input_str = "h264"; encoder = "cpu"; } else if(video_codec_input_str == "auto") { if(!switch_video_codec_to_usable_hardware_encoder(video_codec_input_str)) { video_codec_input_str = "h264"; encoder = "cpu"; } } change_container_if_codec_not_supported(video_codec_input_str, &container_str); 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, "-k", video_codec_input_str.c_str(), "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-restore-portal-session", restore_portal_session ? "yes" : "no", "-cr", color_range_input_str, "-encoder", encoder, "-o", stream_url.c_str() }; add_quality_command_line_args(args, quality_input_str, video_bitrate_str.c_str()); if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(overclock_button))) args.insert(args.end(), { "-oc", "yes" }); if(strcmp(framerate_mode_input_str, "auto") != 0) args.insert(args.end(), { "-fm", framerate_mode_input_str }); std::string merge_audio_tracks_arg_value; add_audio_command_line_args(args, merge_audio_tracks_arg_value); if(follow_focused || change_video_resolution) args.insert(args.end(), { "-s", area }); args.push_back(NULL); if(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item))) { hide_window(); } pid_t parent_pid = getpid(); pid_t pid = fork(); if(pid == -1) { perror("failed to fork"); show_notification(app, "GPU Screen Recorder", "Failed to start streaming (failed to fork)", G_NOTIFICATION_PRIORITY_URGENT); return true; } else if(pid == 0) { /* child process */ if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) { perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed"); _exit(3); } if(getppid() != parent_pid) _exit(3); execvp(args[0], (char* const*)args.data()); perror("failed to launch gpu-screen-recorder"); _exit(127); } else { /* parent process */ gpu_screen_recorder_process = pid; } streaming = true; gtk_button_set_label(button, "Stop streaming"); gtk_widget_set_sensitive(GTK_WIDGET(stream_back_button), false); gtk_widget_set_opacity(GTK_WIDGET(streaming_bottom_panel_grid), 1.0); gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_streaming_menu_item), "Stop streaming"); app_indicator_set_icon_full(app_indicator, get_tray_recording_icon_name(), "Recording"); if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button))) show_notification(app, "GPU Screen Recorder", "Started streaming", G_NOTIFICATION_PRIORITY_NORMAL); record_start_time_sec = clock_get_monotonic_seconds(); return true; } static void start_stop_streaming_menu_item_systray_callback(GtkMenuItem*, gpointer userdata) { on_start_streaming_button_click(start_streaming_button, userdata); } static void start_stop_recording_systray_callback(GtkMenuItem*, gpointer userdata) { on_start_recording_button_click(start_recording_button, userdata); } static void pause_recording_systray_callback(GtkMenuItem*, gpointer userdata) { on_pause_unpause_button_click(pause_recording_button, userdata); } static void start_stop_replay_systray_callback(GtkMenuItem*, gpointer userdata) { on_start_replay_button_click(start_replay_button, userdata); } static void save_replay_systray_callback(GtkMenuItem*, gpointer userdata) { on_replay_save_button_click(replay_save_button, userdata); } static void gtk_widget_set_margin(GtkWidget *widget, int top, int bottom, int left, int right) { gtk_widget_set_margin_top(widget, top); gtk_widget_set_margin_bottom(widget, bottom); gtk_widget_set_margin_start(widget, left); gtk_widget_set_margin_end(widget, right); } static void record_area_item_change_callback(GtkComboBox *widget, gpointer userdata) { (void)widget; (void)userdata; const std::string selected_window_area = record_area_selection_menu_get_active_id(); gtk_widget_set_visible(GTK_WIDGET(select_window_button), strcmp(selected_window_area.c_str(), "window") == 0); gtk_widget_set_visible(GTK_WIDGET(area_size_grid), strcmp(selected_window_area.c_str(), "focused") == 0); gtk_widget_set_visible(GTK_WIDGET(restore_portal_session_button), strcmp(selected_window_area.c_str(), "portal") == 0); gtk_widget_set_visible(GTK_WIDGET(change_video_resolution_button), strcmp(selected_window_area.c_str(), "focused") != 0); on_change_video_resolution_button_click(GTK_BUTTON(change_video_resolution_button), nullptr); enable_stream_record_button_if_info_filled(); } static void view_combo_box_change_callback(GtkComboBox *widget, gpointer userdata) { (void)userdata; const gchar *selected_view = gtk_combo_box_get_active_id(widget); const bool advanced_view = strcmp(selected_view, "advanced") == 0; gtk_widget_set_visible(GTK_WIDGET(color_range_grid), advanced_view); gtk_widget_set_visible(GTK_WIDGET(video_codec_grid), advanced_view); gtk_widget_set_visible(GTK_WIDGET(audio_codec_grid), advanced_view); gtk_widget_set_visible(GTK_WIDGET(framerate_mode_grid), advanced_view); gtk_widget_set_visible(GTK_WIDGET(overclock_grid), advanced_view && gsr_info.gpu_info.vendor == GpuVendor::NVIDIA && gsr_info.system_info.display_server != DisplayServer::WAYLAND); gtk_widget_set_visible(GTK_WIDGET(show_recording_started_notification_button), advanced_view); gtk_widget_set_visible(GTK_WIDGET(show_recording_stopped_notification_button), advanced_view); gtk_widget_set_visible(GTK_WIDGET(show_recording_saved_notification_button), advanced_view); } static void quality_combo_box_change_callback(GtkComboBox *widget, gpointer userdata) { (void)userdata; const gchar *selected_view = gtk_combo_box_get_active_id(widget); const bool custom_selected = strcmp(selected_view, "custom") == 0; gtk_widget_set_visible(GTK_WIDGET(video_bitrate_grid), custom_selected); } static void stream_service_item_change_callback(GtkComboBox *widget, gpointer userdata) { (void)userdata; GtkEntry *stream_id_entries[3] = { youtube_stream_id_entry, twitch_stream_id_entry, custom_stream_url_entry }; for(int i = 0; i < 3; ++i) { gtk_widget_set_visible(GTK_WIDGET(stream_id_entries[i]), false); } const gchar *selected_stream_service = gtk_combo_box_get_active_id(widget); if(strcmp(selected_stream_service, "youtube") == 0) { gtk_label_set_text(stream_key_label, "Stream key: "); gtk_widget_set_visible(GTK_WIDGET(youtube_stream_id_entry), true); gtk_widget_set_visible(GTK_WIDGET(custom_stream_container_grid), false); } else if(strcmp(selected_stream_service, "twitch") == 0) { gtk_label_set_text(stream_key_label, "Stream key: "); gtk_widget_set_visible(GTK_WIDGET(twitch_stream_id_entry), true); gtk_widget_set_visible(GTK_WIDGET(custom_stream_container_grid), false); } else if(strcmp(selected_stream_service, "custom") == 0) { gtk_label_set_text(stream_key_label, "Url: "); gtk_widget_set_visible(GTK_WIDGET(custom_stream_url_entry), true); gtk_widget_set_visible(GTK_WIDGET(custom_stream_container_grid), true); } } static bool is_nv_fbc_installed() { void *lib = dlopen("libnvidia-fbc.so.1", RTLD_LAZY); if(lib) dlclose(lib); return lib != nullptr; } static bool is_nvenc_installed() { void *lib = dlopen("libnvidia-encode.so.1", RTLD_LAZY); if(lib) dlclose(lib); return lib != nullptr; } static bool is_cuda_installed() { void *lib = dlopen("libcuda.so.1", RTLD_LAZY); if(!lib) lib = dlopen("libcuda.so", RTLD_LAZY); if(lib) dlclose(lib); return lib != nullptr; } static bool is_hotkey_already_bound_to_another_action_on_same_page(const Hotkey *_current_hotkey, const Hotkey new_hotkey, GtkWidget *page) { for(int i = 0; i < num_hotkeys; ++i) { if(hotkeys[i] != _current_hotkey && hotkeys[i]->page == page && hotkeys[i]->keysym == new_hotkey.keysym && hotkeys[i]->modkey_mask == new_hotkey.modkey_mask) return true; } return false; } static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpointer userdata) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return GDK_FILTER_CONTINUE; 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); for(int i = 0; i < num_hotkeys; ++i) { if(visible_page != hotkeys[i]->page) continue; if(key_sym == hotkeys[i]->keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(hotkeys[i]->modkey_mask)) hotkeys[i]->trigger_handler(hotkeys[i]->associated_button, _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 && key_sym == XK_BackSpace) { if(pressed_hotkey.modkey_mask == None && pressed_hotkey.keysym == None) { ungrab_keyboard(display); grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), *current_hotkey, false); gtk_entry_set_text(GTK_ENTRY(current_hotkey->hotkey_entry), ""); current_hotkey->grab_success = true; current_hotkey->keysym = None; current_hotkey->modkey_mask = 0; current_hotkey = nullptr; hotkey_mode = HotkeyMode::Record; save_configs(); } 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); if(is_hotkey_already_bound_to_another_action_on_same_page(current_hotkey, latest_hotkey, gtk_stack_get_visible_child(_page_navigation_userdata->stack))) { const std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); const 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; hotkey_mode = HotkeyMode::Record; return GDK_FILTER_CONTINUE; } const Hotkey prev_current_hotkey = *current_hotkey; current_hotkey->keysym = latest_hotkey.keysym; current_hotkey->modkey_mask = latest_hotkey.modkey_mask; const std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); if(replace_grabbed_keys_depending_on_active_page()) { save_configs(); current_hotkey = nullptr; hotkey_mode = HotkeyMode::Record; return GDK_FILTER_CONTINUE; } else { const std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey."; current_hotkey->keysym = prev_current_hotkey.keysym; current_hotkey->modkey_mask = prev_current_hotkey.modkey_mask; 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); return GDK_FILTER_CONTINUE; } } } return GDK_FILTER_CONTINUE; } static gboolean on_hotkey_entry_click(GtkWidget *button, gpointer) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return true; pressed_hotkey.hotkey_entry = nullptr; pressed_hotkey.hotkey_active_label = nullptr; pressed_hotkey.keysym = None; pressed_hotkey.modkey_mask = 0; latest_hotkey = pressed_hotkey; current_hotkey = nullptr; for(int i = 0; i < num_hotkeys; ++i) { if(button == hotkeys[i]->hotkey_entry) { current_hotkey = hotkeys[i]; break; } } if(!current_hotkey) return true; hotkey_mode = HotkeyMode::NewHotkey; gtk_grab_add(current_hotkey->hotkey_entry); gtk_widget_set_visible(current_hotkey->hotkey_active_label, true); 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 void parse_system_info_line(GsrInfo *_gsr_info, const std::string &line) { const size_t space_index = line.find('|'); if(space_index == std::string::npos) return; const StringView attribute_name = {line.c_str(), space_index}; const StringView attribute_value = {line.c_str() + space_index + 1, line.size() - (space_index + 1)}; if(attribute_name == "display_server") { if(attribute_value == "x11") _gsr_info->system_info.display_server = DisplayServer::X11; else if(attribute_value == "wayland") _gsr_info->system_info.display_server = DisplayServer::WAYLAND; } else if(attribute_name == "is_steam_deck") { _gsr_info->system_info.is_steam_deck = attribute_value == "yes"; } } static void parse_gpu_info_line(GsrInfo *_gsr_info, const std::string &line) { const size_t space_index = line.find('|'); if(space_index == std::string::npos) return; const StringView attribute_name = {line.c_str(), space_index}; const StringView attribute_value = {line.c_str() + space_index + 1, line.size() - (space_index + 1)}; if(attribute_name == "vendor") { if(attribute_value == "amd") _gsr_info->gpu_info.vendor = GpuVendor::AMD; else if(attribute_value == "intel") _gsr_info->gpu_info.vendor = GpuVendor::INTEL; else if(attribute_value == "nvidia") _gsr_info->gpu_info.vendor = GpuVendor::NVIDIA; } } static void parse_video_codecs_line(GsrInfo *_gsr_info, const std::string &line) { if(line == "h264") _gsr_info->supported_video_codecs.h264 = true; else if(line == "h264_software") _gsr_info->supported_video_codecs.h264_software = true; else if(line == "hevc") _gsr_info->supported_video_codecs.hevc = true; else if(line == "hevc_hdr") _gsr_info->supported_video_codecs.hevc_hdr = true; else if(line == "hevc_10bit") _gsr_info->supported_video_codecs.hevc_10bit = true; else if(line == "av1") _gsr_info->supported_video_codecs.av1 = true; else if(line == "av1_hdr") _gsr_info->supported_video_codecs.av1_hdr = true; else if(line == "av1_10bit") _gsr_info->supported_video_codecs.av1_10bit = true; else if(line == "vp8") _gsr_info->supported_video_codecs.vp8 = true; else if(line == "vp9") _gsr_info->supported_video_codecs.vp9 = true; } static GsrMonitor capture_option_line_to_monitor(const std::string &line) { size_t space_index = line.find('|'); if(space_index == std::string::npos) return { line, {0, 0} }; vec2i size = {0, 0}; if(sscanf(line.c_str() + space_index + 1, "%dx%d", &size.x, &size.y) != 2) size = {0, 0}; return { line.substr(0, space_index), size }; } static void parse_capture_options_line(GsrInfo *_gsr_info, const std::string &line) { if(line == "window") _gsr_info->supported_capture_options.window = true; else if(line == "focused") _gsr_info->supported_capture_options.focused = true; else if(line == "screen") _gsr_info->supported_capture_options.screen = true; else if(line == "portal") _gsr_info->supported_capture_options.portal = true; else _gsr_info->supported_capture_options.monitors.push_back(capture_option_line_to_monitor(line)); } enum class GsrInfoSection { UNKNOWN, SYSTEM_INFO, GPU_INFO, VIDEO_CODECS, CAPTURE_OPTIONS }; static bool starts_with(const std::string &str, const char *substr) { size_t len = strlen(substr); return str.size() >= len && memcmp(str.data(), substr, len) == 0; } static GsrInfoExitStatus get_gpu_screen_recorder_info(GsrInfo *_gsr_info) { *_gsr_info = GsrInfo{}; FILE *f = popen("gpu-screen-recorder --info", "r"); if(!f) { fprintf(stderr, "error: 'gpu-screen-recorder --info' failed\n"); return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; } char output[8192]; ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f); if(bytes_read < 0 || ferror(f)) { fprintf(stderr, "error: failed to read 'gpu-screen-recorder --info' output\n"); pclose(f); return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; } output[bytes_read] = '\0'; GsrInfoSection section = GsrInfoSection::UNKNOWN; string_split_char(output, '\n', [&](StringView line) { const std::string line_str(line.str, line.size); if(starts_with(line_str, "section=")) { const char *section_name = line_str.c_str() + 8; if(strcmp(section_name, "system_info") == 0) section = GsrInfoSection::SYSTEM_INFO; else if(strcmp(section_name, "gpu_info") == 0) section = GsrInfoSection::GPU_INFO; else if(strcmp(section_name, "video_codecs") == 0) section = GsrInfoSection::VIDEO_CODECS; else if(strcmp(section_name, "capture_options") == 0) section = GsrInfoSection::CAPTURE_OPTIONS; else section = GsrInfoSection::UNKNOWN; return true; } switch(section) { case GsrInfoSection::UNKNOWN: { break; } case GsrInfoSection::SYSTEM_INFO: { parse_system_info_line(_gsr_info, line_str); break; } case GsrInfoSection::GPU_INFO: { parse_gpu_info_line(_gsr_info, line_str); break; } case GsrInfoSection::VIDEO_CODECS: { parse_video_codecs_line(_gsr_info, line_str); break; } case GsrInfoSection::CAPTURE_OPTIONS: { parse_capture_options_line(_gsr_info, line_str); break; } } return true; }); int status = pclose(f); if(WIFEXITED(status)) { switch(WEXITSTATUS(status)) { case 0: return GsrInfoExitStatus::OK; case 22: return GsrInfoExitStatus::OPENGL_FAILED; case 23: return GsrInfoExitStatus::NO_DRM_CARD; default: return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; } } return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; } static void record_area_set_sensitive(GtkCellLayout *cell_layout, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data) { (void)cell_layout; (void)data; gchar *id; gtk_tree_model_get(tree_model, iter, 1, &id, -1); g_object_set(cell, "sensitive", is_video_capture_option_enabled(id), NULL); g_free(id); } static void video_codec_set_sensitive(GtkCellLayout *cell_layout, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data) { (void)cell_layout; (void)data; gchar *id; gtk_tree_model_get(tree_model, iter, 1, &id, -1); g_object_set(cell, "sensitive", is_video_codec_enabled(id), NULL); g_free(id); } 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"); gtk_widget_set_vexpand(GTK_WIDGET(grid), true); gtk_widget_set_hexpand(GTK_WIDGET(grid), true); gtk_grid_set_row_spacing(grid, 10); gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); int grid_row = 0; int record_area_row = 0; int audio_input_area_row = 0; GtkGrid *simple_advanced_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(simple_advanced_grid), 0, grid_row++, 2, 1); gtk_grid_attach(simple_advanced_grid, gtk_label_new("View: "), 0, 0, 1, 1); view_combo_box = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(view_combo_box, "simple", "Simple"); gtk_combo_box_text_append(view_combo_box, "advanced", "Advanced"); gtk_widget_set_hexpand(GTK_WIDGET(view_combo_box), true); gtk_grid_attach(simple_advanced_grid, GTK_WIDGET(view_combo_box), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(view_combo_box), 0); g_signal_connect(view_combo_box, "changed", G_CALLBACK(view_combo_box_change_callback), view_combo_box); GtkFrame *record_area_frame = GTK_FRAME(gtk_frame_new("Record area")); gtk_grid_attach(grid, GTK_WIDGET(record_area_frame), 0, grid_row++, 2, 1); GtkGrid *record_area_grid = GTK_GRID(gtk_grid_new()); gtk_widget_set_vexpand(GTK_WIDGET(record_area_grid), false); gtk_widget_set_hexpand(GTK_WIDGET(record_area_grid), true); gtk_grid_set_row_spacing(record_area_grid, 10); gtk_grid_set_column_spacing(record_area_grid, 10); gtk_widget_set_margin(GTK_WIDGET(record_area_grid), 10, 10, 10, 10); gtk_container_add(GTK_CONTAINER(record_area_frame), GTK_WIDGET(record_area_grid)); GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); GtkTreeIter iter; record_area_selection_model = GTK_TREE_MODEL(store); if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "Window (Not available on Wayland)", -1); gtk_list_store_set(store, &iter, 1, "window", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "Follow focused window (Not available on Wayland)", -1); gtk_list_store_set(store, &iter, 1, "focused", -1); } else { gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "Window", -1); gtk_list_store_set(store, &iter, 1, "window", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "Follow focused window", -1); gtk_list_store_set(store, &iter, 1, "focused", -1); } const bool allow_screen_capture = is_monitor_capture_drm() || nvfbc_installed; if(allow_screen_capture) { if(gsr_info.supported_capture_options.screen) { gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "All monitors", -1); gtk_list_store_set(store, &iter, 1, "screen", -1); } for(const auto &monitor : gsr_info.supported_capture_options.monitors) { std::string label = "Monitor "; label += monitor.name; label += " ("; label += std::to_string(monitor.size.x); label += "x"; label += std::to_string(monitor.size.y); if(flatpak && is_monitor_capture_drm()) { label += ", requires root access"; } label += ")"; // Leak on purpose, what are you gonna do? stab me? char *id = (char*)malloc(monitor.name.size() + 1); memcpy(id, monitor.name.c_str(), monitor.name.size()); id[monitor.name.size()] = '\0'; gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, label.c_str(), -1); gtk_list_store_set(store, &iter, 1, id, -1); } if(gsr_info.supported_capture_options.monitors.empty() && gsr_info.system_info.display_server == DisplayServer::WAYLAND && !gsr_info.supported_capture_options.portal) { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "No monitors to record found. Make sure GPU Screen Recorder is running on the same GPU device that is displaying graphics on the screen."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(select_window_userdata.app)); return GTK_WIDGET(grid); } } if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_capture_options.portal ? "Desktop portal (HDR not supported)" : "Desktop portal (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "portal", -1); } else { gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "Desktop portal (Not available on X11)", -1); gtk_list_store_set(store, &iter, 1, "portal", -1); } record_area_selection_menu = GTK_COMBO_BOX(gtk_combo_box_new_with_model(record_area_selection_model)); GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(record_area_selection_menu), renderer, TRUE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(record_area_selection_menu), renderer, "text", 0, NULL); gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(record_area_selection_menu), renderer, record_area_set_sensitive, NULL, NULL); gtk_combo_box_set_active(record_area_selection_menu, (allow_screen_capture || gsr_info.supported_capture_options.portal) ? 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); g_signal_connect(record_area_selection_menu, "changed", G_CALLBACK(record_area_item_change_callback), NULL); select_window_button = GTK_BUTTON(gtk_button_new_with_label("Select window...")); gtk_widget_set_hexpand(GTK_WIDGET(select_window_button), true); g_signal_connect(select_window_button, "clicked", G_CALLBACK(on_select_window_button_click), app); gtk_grid_attach(record_area_grid, GTK_WIDGET(select_window_button), 0, record_area_row++, 3, 1); change_video_resolution_button = gtk_check_button_new_with_label("Change video resolution"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(change_video_resolution_button), false); gtk_widget_set_halign(change_video_resolution_button, GTK_ALIGN_START); g_signal_connect(change_video_resolution_button, "clicked", G_CALLBACK(on_change_video_resolution_button_click), app); gtk_grid_attach(record_area_grid, change_video_resolution_button, 0, record_area_row++, 3, 1); { area_size_grid = GTK_GRID(gtk_grid_new()); gtk_grid_set_row_spacing(area_size_grid, 10); gtk_grid_attach(record_area_grid, GTK_WIDGET(area_size_grid), 0, record_area_row++, 3, 1); GtkLabel *video_resolution_label = GTK_LABEL(gtk_label_new("Video resolution limit: ")); gtk_label_set_xalign(video_resolution_label, 0.0f); gtk_grid_attach(area_size_grid, GTK_WIDGET(video_resolution_label), 0, 0, 3, 1); area_width_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 10000.0, 1.0)); gtk_spin_button_set_value(area_width_entry, 1920.0); gtk_widget_set_hexpand(GTK_WIDGET(area_width_entry), true); gtk_grid_attach(area_size_grid, GTK_WIDGET(area_width_entry), 0, 1, 1, 1); gtk_grid_attach(area_size_grid, gtk_label_new("x"), 1, 1, 1, 1); area_height_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 10000.0, 1.0)); gtk_spin_button_set_value(area_height_entry, 1080.0); gtk_widget_set_hexpand(GTK_WIDGET(area_height_entry), true); gtk_grid_attach(area_size_grid, GTK_WIDGET(area_height_entry), 2, 1, 1, 1); } { video_resolution_grid = GTK_GRID(gtk_grid_new()); gtk_grid_set_row_spacing(video_resolution_grid, 10); gtk_grid_attach(record_area_grid, GTK_WIDGET(video_resolution_grid), 0, record_area_row++, 3, 1); GtkLabel *video_resolution_label = GTK_LABEL(gtk_label_new("Video resolution: ")); gtk_label_set_xalign(video_resolution_label, 0.0f); gtk_grid_attach(video_resolution_grid, GTK_WIDGET(video_resolution_label), 0, 0, 3, 1); video_width_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 10000.0, 1.0)); gtk_spin_button_set_value(video_width_entry, 1920.0); gtk_widget_set_hexpand(GTK_WIDGET(video_width_entry), true); gtk_grid_attach(video_resolution_grid, GTK_WIDGET(video_width_entry), 0, 1, 1, 1); gtk_grid_attach(video_resolution_grid, gtk_label_new("x"), 1, 1, 1, 1); video_height_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 10000.0, 1.0)); gtk_spin_button_set_value(video_height_entry, 1080.0); gtk_widget_set_hexpand(GTK_WIDGET(video_height_entry), true); gtk_grid_attach(video_resolution_grid, GTK_WIDGET(video_height_entry), 2, 1, 1, 1); } restore_portal_session_button = gtk_check_button_new_with_label("Restore portal session"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(restore_portal_session_button), true); gtk_widget_set_halign(restore_portal_session_button, GTK_ALIGN_START); gtk_grid_attach(record_area_grid, restore_portal_session_button, 0, record_area_row++, 3, 1); GtkFrame *audio_input_frame = GTK_FRAME(gtk_frame_new("Audio")); gtk_grid_attach(grid, GTK_WIDGET(audio_input_frame), 0, grid_row++, 2, 1); GtkGrid *audio_grid = GTK_GRID(gtk_grid_new()); gtk_widget_set_vexpand(GTK_WIDGET(audio_grid), false); gtk_widget_set_hexpand(GTK_WIDGET(audio_grid), true); gtk_grid_set_row_spacing(audio_grid, 10); gtk_grid_set_column_spacing(audio_grid, 10); gtk_widget_set_margin(GTK_WIDGET(audio_grid), 10, 10, 10, 10); gtk_container_add(GTK_CONTAINER(audio_input_frame), GTK_WIDGET(audio_grid)); GtkGrid *add_audio_grid = GTK_GRID(gtk_grid_new()); gtk_grid_set_row_spacing(add_audio_grid, 10); gtk_grid_set_column_spacing(add_audio_grid, 10); gtk_grid_attach(audio_grid, GTK_WIDGET(add_audio_grid), 0, audio_input_area_row++, 1, 1); add_audio_input_button = gtk_button_new_with_label("Add audio track"); gtk_grid_attach(add_audio_grid, add_audio_input_button, 0, 0, 1, 1); g_signal_connect(add_audio_input_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer){ GtkWidget *row = create_used_audio_input_row(); gtk_widget_show_all(row); gtk_list_box_insert(GTK_LIST_BOX(audio_input_used_list), row, -1); return true; }), nullptr); audio_input_used_list = gtk_list_box_new(); gtk_widget_set_hexpand (audio_input_used_list, TRUE); gtk_list_box_set_selection_mode (GTK_LIST_BOX (audio_input_used_list), GTK_SELECTION_NONE); gtk_grid_attach(audio_grid, audio_input_used_list, 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(audio_grid, merge_audio_tracks_button, 0, audio_input_area_row++, 2, 1); GtkGrid *fps_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(fps_grid), 0, grid_row++, 2, 1); gtk_grid_attach(fps_grid, gtk_label_new("Frame rate: "), 0, 0, 1, 1); fps_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(1.0, 5000.0, 1.0)); gtk_spin_button_set_value(fps_entry, 60.0); gtk_widget_set_hexpand(GTK_WIDGET(fps_entry), true); gtk_grid_attach(fps_grid, GTK_WIDGET(fps_entry), 1, 0, 1, 1); color_range_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(color_range_grid), 0, grid_row++, 2, 1); gtk_grid_attach(color_range_grid, gtk_label_new("Color range: "), 0, 0, 1, 1); color_range_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(color_range_input_menu, "limited", "Limited"); gtk_combo_box_text_append(color_range_input_menu, "full", "Full"); gtk_widget_set_hexpand(GTK_WIDGET(color_range_input_menu), true); gtk_grid_attach(color_range_grid, GTK_WIDGET(color_range_input_menu), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(color_range_input_menu), 0); GtkGrid *video_quality_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(video_quality_grid), 0, grid_row++, 2, 1); gtk_grid_attach(video_quality_grid, gtk_label_new("Video quality: "), 0, 0, 1, 1); quality_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(quality_input_menu, "custom", "Constant bitrate (Recommended for live streaming and replay)"); gtk_combo_box_text_append(quality_input_menu, "medium", "Medium"); gtk_combo_box_text_append(quality_input_menu, "high", "High"); gtk_combo_box_text_append(quality_input_menu, "very_high", "Very High (Recommended for recording)"); gtk_combo_box_text_append(quality_input_menu, "ultra", "Ultra"); gtk_widget_set_hexpand(GTK_WIDGET(quality_input_menu), true); gtk_grid_attach(video_quality_grid, GTK_WIDGET(quality_input_menu), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(quality_input_menu), 0); g_signal_connect(quality_input_menu, "changed", G_CALLBACK(quality_combo_box_change_callback), quality_input_menu); video_bitrate_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(video_bitrate_grid), 0, grid_row++, 2, 1); gtk_grid_attach(video_bitrate_grid, gtk_label_new("Video bitrate (kbps): "), 0, 0, 1, 1); video_bitrate_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(1.0, 500000.0, 1.0)); gtk_spin_button_set_value(video_bitrate_entry, 15000.0); gtk_widget_set_hexpand(GTK_WIDGET(video_bitrate_entry), true); gtk_grid_attach(video_bitrate_grid, GTK_WIDGET(video_bitrate_entry), 1, 0, 1, 1); video_codec_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(video_codec_grid), 0, grid_row++, 2, 1); gtk_grid_attach(video_codec_grid, gtk_label_new("Video codec: "), 0, 0, 1, 1); { store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); video_codec_selection_model = GTK_TREE_MODEL(store); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "Auto (Recommended, H264)", -1); gtk_list_store_set(store, &iter, 1, "auto", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.h264 ? "H264 (Largest file size, best software compatibility)" : "H264 (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "h264", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC" : "HEVC (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "hevc", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1 (Smallest file size, worst software compatibility)" : "AV1 (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "av1", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.vp8 ? "VP8" : "VP8 (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "vp8", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.vp9 ? "VP9" : "VP9 (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "vp9", -1); if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC (HDR)" : "HEVC (HDR, not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "hevc_hdr", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1 (HDR)" : "AV1 (HDR, not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "av1_hdr", -1); } else { gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "HEVC (HDR, not available on X11)", -1); gtk_list_store_set(store, &iter, 1, "hevc_hdr", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, "AV1 (HDR, not available on X11)", -1); gtk_list_store_set(store, &iter, 1, "av1_hdr", -1); } gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC (10 bit, reduces banding)" : "HEVC (10 bit, not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "hevc_10bit", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1 (10 bit, reduces banding)" : "AV1 (10 bit, not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "av1_10bit", -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.h264_software ? "H264 Software Encoder (Slow, not recommeded)" : "H264 Software Encoder (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "h264_software", -1); video_codec_selection_menu = GTK_COMBO_BOX(gtk_combo_box_new_with_model(video_codec_selection_model)); renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(video_codec_selection_menu), renderer, TRUE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(video_codec_selection_menu), renderer, "text", 0, NULL); gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(video_codec_selection_menu), renderer, video_codec_set_sensitive, NULL, NULL); gtk_combo_box_set_active(video_codec_selection_menu, 0); gtk_widget_set_hexpand(GTK_WIDGET(video_codec_selection_menu), true); gtk_grid_attach(video_codec_grid, GTK_WIDGET(video_codec_selection_menu), 1, 0, 1, 1); } audio_codec_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(audio_codec_grid), 0, grid_row++, 2, 1); gtk_grid_attach(audio_codec_grid, gtk_label_new("Audio codec: "), 0, 0, 1, 1); audio_codec_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(audio_codec_input_menu, "opus", "Opus (Recommended)"); gtk_combo_box_text_append(audio_codec_input_menu, "aac", "AAC"); gtk_widget_set_hexpand(GTK_WIDGET(audio_codec_input_menu), true); gtk_grid_attach(audio_codec_grid, GTK_WIDGET(audio_codec_input_menu), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(audio_codec_input_menu), 0); framerate_mode_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(framerate_mode_grid), 0, grid_row++, 2, 1); gtk_grid_attach(framerate_mode_grid, gtk_label_new("Frame rate mode: "), 0, 0, 1, 1); framerate_mode_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(framerate_mode_input_menu, "auto", "Auto (Recommended)"); gtk_combo_box_text_append(framerate_mode_input_menu, "cfr", "Constant"); gtk_combo_box_text_append(framerate_mode_input_menu, "vfr", "Variable"); gtk_widget_set_hexpand(GTK_WIDGET(framerate_mode_input_menu), true); gtk_grid_attach(framerate_mode_grid, GTK_WIDGET(framerate_mode_input_menu), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(framerate_mode_input_menu), 0); overclock_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(overclock_grid), 0, grid_row++, 2, 1); overclock_button = gtk_check_button_new_with_label("Overclock memory transfer rate to workaround NVIDIA driver performance bug"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(overclock_button), false); gtk_widget_set_halign(overclock_button, GTK_ALIGN_START); gtk_grid_attach(overclock_grid, overclock_button, 0, 0, 1, 1); GtkButton *overclock_info_button = GTK_BUTTON(gtk_button_new_with_label("?")); gtk_grid_attach(overclock_grid, GTK_WIDGET(overclock_info_button), 1, 0, 1, 1); g_signal_connect(overclock_info_button, "clicked", G_CALLBACK(+[](GtkButton *button, gpointer userdata){ (void)button; (void)userdata; GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "NVIDIA driver has a \"feature\" (read: bug) where it will downclock memory transfer rate when a program uses CUDA, such as GPU Screen Recorder.\n" "To work around this bug, GPU Screen Recorder can overclock your GPU memory transfer rate to it's normal optimal level. To enable overclocking for optimal performance enable the \"Overclock memory transfer rate to workaround NVIDIA driver performance bug\" option.\n" "You also need to have \"Coolbits\" NVIDIA X setting set to \"12\" to enable overclocking.\n" "You can set coolbits by running \"sudo nvidia-xconfig --cool-bits=12\" and then rebooting.\n" "\n" "Note that this only works when Xorg server is running as root.\n" "\n" "Note! use at your own risk!"); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); return true; }), nullptr); record_cursor_button = gtk_check_button_new_with_label("Record cursor"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record_cursor_button), true); gtk_widget_set_halign(record_cursor_button, GTK_ALIGN_START); gtk_grid_attach(grid, record_cursor_button, 0, grid_row++, 2, 1); show_recording_started_notification_button = gtk_check_button_new_with_label("Show recording/streaming/replay started notification"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button), false); gtk_widget_set_halign(show_recording_started_notification_button, GTK_ALIGN_START); gtk_grid_attach(grid, show_recording_started_notification_button, 0, grid_row++, 2, 1); show_recording_stopped_notification_button = gtk_check_button_new_with_label("Show streaming/replay stopped notification"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button), false); gtk_widget_set_halign(show_recording_stopped_notification_button, GTK_ALIGN_START); gtk_grid_attach(grid, show_recording_stopped_notification_button, 0, grid_row++, 2, 1); show_recording_saved_notification_button = gtk_check_button_new_with_label("Show video saved notification"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button), true); gtk_widget_set_halign(show_recording_saved_notification_button, GTK_ALIGN_START); gtk_grid_attach(grid, show_recording_saved_notification_button, 0, grid_row++, 2, 1); 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); stream_button = GTK_BUTTON(gtk_button_new_with_label("Stream")); GtkWidget *go_next_stream = gtk_image_new_from_icon_name("go-next", GTK_ICON_SIZE_BUTTON); gtk_button_set_image(stream_button, go_next_stream); gtk_button_set_always_show_image(stream_button, true); gtk_button_set_image_position(stream_button, GTK_POS_RIGHT); gtk_widget_set_hexpand(GTK_WIDGET(stream_button), true); gtk_grid_attach(start_button_grid, GTK_WIDGET(stream_button), 0, 0, 1, 1); record_button = GTK_BUTTON(gtk_button_new_with_label("Record")); GtkWidget *go_next_record = gtk_image_new_from_icon_name("go-next", GTK_ICON_SIZE_BUTTON); gtk_button_set_image(record_button, go_next_record); gtk_button_set_always_show_image(record_button, true); gtk_button_set_image_position(record_button, GTK_POS_RIGHT); gtk_widget_set_hexpand(GTK_WIDGET(record_button), true); gtk_grid_attach(start_button_grid, GTK_WIDGET(record_button), 1, 0, 1, 1); replay_button = GTK_BUTTON(gtk_button_new_with_label("Replay")); GtkWidget *go_next_replay = gtk_image_new_from_icon_name("go-next", GTK_ICON_SIZE_BUTTON); gtk_button_set_image(replay_button, go_next_replay); gtk_button_set_always_show_image(replay_button, true); gtk_button_set_image_position(replay_button, GTK_POS_RIGHT); gtk_widget_set_hexpand(GTK_WIDGET(replay_button), true); gtk_grid_attach(start_button_grid, GTK_WIDGET(replay_button), 2, 0, 1, 1); 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 GTK_WIDGET(grid); } static void replace_meta_with_super(std::string &str) { size_t index = str.find("meta"); if(index != std::string::npos) str.replace(index, 4, "Super"); index = str.find("Meta"); if(index != std::string::npos) str.replace(index, 4, "Super"); } static void shortcut_changed_callback(gsr_shortcut shortcut, void *userdata) { (void)userdata; std::string trigger = shortcut.trigger_description; replace_meta_with_super(trigger); for(int i = 0; i < num_hotkeys; ++i) { if(strcmp(shortcut.id, hotkeys[i]->shortcut_id) == 0) { gtk_entry_set_text(GTK_ENTRY(hotkeys[i]->hotkey_entry), trigger.c_str()); } } } static void deactivated_callback(const char *description, void *userdata) { (void)userdata; const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata.stack); for(int i = 0; i < num_hotkeys; ++i) { if(visible_page != hotkeys[i]->page) continue; if(strcmp(description, hotkeys[i]->shortcut_id) == 0) hotkeys[i]->trigger_handler(hotkeys[i]->associated_button, page_navigation_userdata.app); } } static gboolean on_register_hotkeys_button_clicked(GtkButton *button, gpointer userdata) { (void)button; (void)userdata; /* Modifier key names are defined here: https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-names.h. Remove the XKB_MOD_NAME_ prefix from the name and use the remaining part. Key names are defined here: https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-keysyms.h. Remove the XKB_KEY_ (or XKB_KEY_KP_) prefix from the name and user the remaining part. */ /* LOGO = Super key */ /* Unfortunately global shortcuts cant handle same key for different shortcuts, even though GPU Screen Recorder has page specific hotkeys */ const gsr_bind_shortcut shortcuts[3] = { { "Start/stop recording/replay/streaming", { SHORTCUT_ID_START_STOP_RECORDING, "LOGO+f1" } }, { "Pause/unpause recording", { SHORTCUT_ID_PAUSE_UNPAUSE_RECORDING, "LOGO+f2" } }, { "Save replay", { SHORTCUT_ID_SAVE_REPLAY, "LOGO+f3" } } }; if(global_shortcuts_initialized) { if(!gsr_global_shortcuts_bind_shortcuts(&global_shortcuts, shortcuts, 3, shortcut_changed_callback, NULL)) { fprintf(stderr, "gsr error: failed to bind shortcuts\n"); } } return true; } static void add_wayland_global_hotkeys_ui(GtkGrid *grid, int &row, int width) { GtkGrid *aa_grid = GTK_GRID(gtk_grid_new()); gtk_widget_set_halign(GTK_WIDGET(aa_grid), GTK_ALIGN_CENTER); gtk_grid_attach(grid, GTK_WIDGET(aa_grid), 0, row++, width, 1); gtk_grid_set_column_spacing(aa_grid, 10); gtk_grid_attach(aa_grid, gtk_label_new("On Wayland hotkeys are managed externally by the Wayland compositor, click here to change hotkeys:"), 0, 0, 1, 1); GtkButton *register_hotkeys_button = GTK_BUTTON(gtk_button_new_with_label("Change hotkeys")); gtk_widget_set_hexpand(GTK_WIDGET(register_hotkeys_button), true); //gtk_widget_set_halign(GTK_WIDGET(register_hotkeys_button), GTK_ALIGN_START); g_signal_connect(register_hotkeys_button, "clicked", G_CALLBACK(on_register_hotkeys_button_clicked), nullptr); gtk_grid_attach(aa_grid, GTK_WIDGET(register_hotkeys_button), 1, 0, 1, 1); row++; gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, width, 1); row++; } static void create_replay_hotkey_items(GtkGrid *parent_grid, int row, int num_columns) { replay_hotkeys_grid = GTK_GRID(gtk_grid_new()); gtk_widget_set_halign(GTK_WIDGET(replay_hotkeys_grid), GTK_ALIGN_START); gtk_grid_set_row_spacing(replay_hotkeys_grid, 10); gtk_grid_set_column_spacing(replay_hotkeys_grid, 10); gtk_grid_attach(parent_grid, GTK_WIDGET(replay_hotkeys_grid), 0, row, num_columns, 1); int hotkeys_row = 0; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) add_wayland_global_hotkeys_ui(replay_hotkeys_grid, hotkeys_row, num_columns); { gtk_grid_attach(replay_hotkeys_grid, gtk_label_new("Press"), 0, hotkeys_row, 1, 1); replay_start_stop_hotkey_button = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(replay_start_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F1"); g_signal_connect(replay_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_start_stop_hotkey_button); gtk_grid_attach(replay_hotkeys_grid, replay_start_stop_hotkey_button, 1, hotkeys_row, 1, 1); gtk_grid_attach(replay_hotkeys_grid, gtk_label_new("to start/stop the replay and"), 2, hotkeys_row, 1, 1); replay_save_hotkey_button = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F2"); g_signal_connect(replay_save_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_save_hotkey_button); gtk_grid_attach(replay_hotkeys_grid, replay_save_hotkey_button, 3, hotkeys_row, 1, 1); GtkWidget *save_replay_label = gtk_label_new("to save the replay"); gtk_widget_set_halign(save_replay_label, GTK_ALIGN_START); gtk_widget_set_hexpand(save_replay_label, true); gtk_grid_attach(replay_hotkeys_grid, save_replay_label, 4, hotkeys_row, 1, 1); ++hotkeys_row; } } static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { int row = 0; const int num_columns = 5; std::string video_filepath = get_videos_dir(); GtkGrid *grid = GTK_GRID(gtk_grid_new()); gtk_stack_add_named(stack, GTK_WIDGET(grid), "replay"); gtk_widget_set_vexpand(GTK_WIDGET(grid), true); gtk_widget_set_hexpand(GTK_WIDGET(grid), true); gtk_grid_set_row_spacing(grid, 10); gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); GtkWidget *hotkey_active_label = nullptr; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { replay_hotkeys_not_supported_label = gtk_label_new("Your Wayland compositor doesn't support global hotkeys. Use X11 or KDE Plasma on Wayland if you want to use hotkeys."); gtk_grid_attach(grid, replay_hotkeys_not_supported_label, 0, row++, num_columns, 1); } else { hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel."); gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); } create_replay_hotkey_items(grid, row, num_columns); ++row; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_grid), false); gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_not_supported_label), false); } gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); 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, row++, num_columns, 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, 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); gtk_button_set_image_position(replay_file_chooser_button, GTK_POS_RIGHT); gtk_widget_set_hexpand(GTK_WIDGET(replay_file_chooser_button), true); g_signal_connect(replay_file_chooser_button, "clicked", G_CALLBACK(on_replay_file_chooser_button_click), nullptr); 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, row++, num_columns, 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) { gtk_combo_box_text_append(replay_container, supported_container.container_name, supported_container.file_extension); } if(gsr_info.supported_video_codecs.vp8 || gsr_info.supported_video_codecs.vp9) { gtk_combo_box_text_append(replay_container, "webm", "webm"); } gtk_widget_set_hexpand(GTK_WIDGET(replay_container), true); gtk_grid_attach(container_grid, GTK_WIDGET(replay_container), 1, 0, 1, 1); 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, row++, num_columns, 1); gtk_grid_attach(replay_time_grid, gtk_label_new("Replay time in seconds: "), 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); gtk_widget_set_hexpand(GTK_WIDGET(replay_time_entry), true); gtk_grid_attach(replay_time_grid, GTK_WIDGET(replay_time_entry), 1, 0, 1, 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); replay_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); GtkWidget *go_previous = gtk_image_new_from_icon_name("go-previous", GTK_ICON_SIZE_BUTTON); gtk_button_set_image(replay_back_button, go_previous); gtk_button_set_always_show_image(replay_back_button, true); gtk_button_set_image_position(replay_back_button, GTK_POS_LEFT); gtk_widget_set_hexpand(GTK_WIDGET(replay_back_button), true); gtk_grid_attach(start_button_grid, GTK_WIDGET(replay_back_button), 0, 0, 1, 1); start_replay_button = GTK_BUTTON(gtk_button_new_with_label("Start replay")); gtk_widget_set_hexpand(GTK_WIDGET(start_replay_button), true); g_signal_connect(start_replay_button, "clicked", G_CALLBACK(on_start_replay_button_click), app); gtk_grid_attach(start_button_grid, GTK_WIDGET(start_replay_button), 1, 0, 1, 1); replay_save_button = GTK_BUTTON(gtk_button_new_with_label("Save replay")); gtk_widget_set_hexpand(GTK_WIDGET(replay_save_button), true); g_signal_connect(replay_save_button, "clicked", G_CALLBACK(on_replay_save_button_click), app); gtk_widget_set_sensitive(GTK_WIDGET(replay_save_button), false); gtk_grid_attach(start_button_grid, GTK_WIDGET(replay_save_button), 2, 0, 1, 1); gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); replay_bottom_panel_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(replay_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(replay_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(replay_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(replay_bottom_panel_grid), GTK_ALIGN_END); GtkWidget *record_icon = gtk_image_new_from_icon_name("media-record", GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_widget_set_valign(record_icon, GTK_ALIGN_CENTER); gtk_grid_attach(replay_bottom_panel_grid, record_icon, 0, 0, 1, 1); replay_record_time_label = gtk_label_new("00:00:00"); gtk_widget_set_valign(replay_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(replay_bottom_panel_grid, replay_record_time_label, 1, 0, 1, 1); replay_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); replay_start_stop_hotkey.keysym = XK_F1; replay_start_stop_hotkey.hotkey_entry = replay_start_stop_hotkey_button; replay_start_stop_hotkey.hotkey_active_label = hotkey_active_label; replay_start_stop_hotkey.config = &config.replay_config.start_stop_recording_hotkey; replay_start_stop_hotkey.page = GTK_WIDGET(grid); replay_start_stop_hotkey.trigger_handler = on_start_replay_button_click; replay_start_stop_hotkey.associated_button = start_replay_button; replay_start_stop_hotkey.shortcut_id = SHORTCUT_ID_START_STOP_RECORDING; replay_save_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); replay_save_hotkey.keysym = XK_F2; replay_save_hotkey.hotkey_entry = replay_save_hotkey_button; replay_save_hotkey.hotkey_active_label = hotkey_active_label; replay_save_hotkey.config = &config.replay_config.save_recording_hotkey; replay_save_hotkey.page = GTK_WIDGET(grid); replay_save_hotkey.trigger_handler = on_replay_save_button_click; replay_save_hotkey.associated_button = replay_save_button; replay_save_hotkey.shortcut_id = SHORTCUT_ID_SAVE_REPLAY; return GTK_WIDGET(grid); } static void create_recording_hotkey_items(GtkGrid *parent_grid, int row, int num_columns) { recording_hotkeys_grid = GTK_GRID(gtk_grid_new()); gtk_widget_set_halign(GTK_WIDGET(recording_hotkeys_grid), GTK_ALIGN_START); gtk_grid_set_row_spacing(recording_hotkeys_grid, 10); gtk_grid_set_column_spacing(recording_hotkeys_grid, 10); gtk_grid_attach(parent_grid, GTK_WIDGET(recording_hotkeys_grid), 0, row, num_columns, 1); int hotkeys_row = 0; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) add_wayland_global_hotkeys_ui(recording_hotkeys_grid, hotkeys_row, num_columns); { gtk_grid_attach(recording_hotkeys_grid, gtk_label_new("Press"), 0, hotkeys_row, 1, 1); record_start_stop_hotkey_button = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(record_start_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F1"); g_signal_connect(record_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), record_start_stop_hotkey_button); gtk_grid_attach(recording_hotkeys_grid, record_start_stop_hotkey_button, 1, hotkeys_row, 1, 1); gtk_grid_attach(recording_hotkeys_grid, gtk_label_new("to start/stop recording and"), 2, hotkeys_row, 1, 1); pause_unpause_hotkey_button = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(pause_unpause_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F2"); g_signal_connect(pause_unpause_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), pause_unpause_hotkey_button); gtk_grid_attach(recording_hotkeys_grid, pause_unpause_hotkey_button, 3, hotkeys_row, 1, 1); GtkWidget *pause_unpause_recording_label = gtk_label_new("to pause/unpause recording"); gtk_widget_set_halign(pause_unpause_recording_label, GTK_ALIGN_START); gtk_widget_set_hexpand(pause_unpause_recording_label, true); gtk_grid_attach(recording_hotkeys_grid, pause_unpause_recording_label, 4, hotkeys_row, 1, 1); ++hotkeys_row; } } static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { int row = 0; const int num_columns = 5; std::string video_filepath = get_videos_dir(); GtkGrid *grid = GTK_GRID(gtk_grid_new()); gtk_stack_add_named(stack, GTK_WIDGET(grid), "recording"); gtk_widget_set_vexpand(GTK_WIDGET(grid), true); gtk_widget_set_hexpand(GTK_WIDGET(grid), true); gtk_grid_set_row_spacing(grid, 10); gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); GtkWidget *hotkey_active_label = nullptr; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { recording_hotkeys_not_supported_label = gtk_label_new("Your Wayland compositor doesn't support global hotkeys. Use X11 or KDE Plasma on Wayland if you want to use hotkeys."); gtk_grid_attach(grid, recording_hotkeys_not_supported_label, 0, row++, num_columns, 1); } else { hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel."); gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); } create_recording_hotkey_items(grid, row, num_columns); ++row; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_grid), false); gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_not_supported_label), false); } gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); 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, row++, num_columns, 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); record_file_chooser_button = GTK_BUTTON(gtk_button_new_with_label(video_filepath.c_str())); gtk_button_set_image(record_file_chooser_button, save_icon); gtk_button_set_always_show_image(record_file_chooser_button, true); gtk_button_set_image_position(record_file_chooser_button, GTK_POS_RIGHT); gtk_widget_set_hexpand(GTK_WIDGET(record_file_chooser_button), true); g_signal_connect(record_file_chooser_button, "clicked", G_CALLBACK(on_record_file_choose_button_click), nullptr); 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, row++, num_columns, 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) { gtk_combo_box_text_append(record_container, supported_container.container_name, supported_container.file_extension); } if(gsr_info.supported_video_codecs.vp8 || gsr_info.supported_video_codecs.vp9) { gtk_combo_box_text_append(record_container, "webm", "webm"); } gtk_widget_set_hexpand(GTK_WIDGET(record_container), true); gtk_grid_attach(container_grid, GTK_WIDGET(record_container), 1, 0, 1, 1); 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, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); record_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); GtkWidget *go_previous = gtk_image_new_from_icon_name("go-previous", GTK_ICON_SIZE_BUTTON); gtk_button_set_image(record_back_button, go_previous); gtk_button_set_always_show_image(record_back_button, true); gtk_button_set_image_position(record_back_button, GTK_POS_LEFT); gtk_widget_set_hexpand(GTK_WIDGET(record_back_button), true); gtk_grid_attach(start_button_grid, GTK_WIDGET(record_back_button), 0, 0, 1, 1); start_recording_button = GTK_BUTTON(gtk_button_new_with_label("Start recording")); gtk_widget_set_hexpand(GTK_WIDGET(start_recording_button), true); g_signal_connect(start_recording_button, "clicked", G_CALLBACK(on_start_recording_button_click), app); gtk_grid_attach(start_button_grid, GTK_WIDGET(start_recording_button), 1, 0, 1, 1); pause_recording_button = GTK_BUTTON(gtk_button_new_with_label("Pause recording")); gtk_widget_set_hexpand(GTK_WIDGET(pause_recording_button), true); g_signal_connect(pause_recording_button, "clicked", G_CALLBACK(on_pause_unpause_button_click), app); gtk_widget_set_sensitive(GTK_WIDGET(pause_recording_button), false); gtk_grid_attach(start_button_grid, GTK_WIDGET(pause_recording_button), 2, 0, 1, 1); gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); recording_bottom_panel_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(recording_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(recording_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(recording_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(recording_bottom_panel_grid), GTK_ALIGN_END); recording_record_icon = gtk_image_new_from_icon_name("media-record", GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_widget_set_valign(recording_record_icon, GTK_ALIGN_CENTER); gtk_grid_attach(recording_bottom_panel_grid, recording_record_icon, 0, 0, 1, 1); recording_record_time_label = gtk_label_new("00:00:00"); gtk_widget_set_valign(recording_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(recording_bottom_panel_grid, recording_record_time_label, 1, 0, 1, 1); record_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); record_start_stop_hotkey.keysym = XK_F1; record_start_stop_hotkey.hotkey_entry = record_start_stop_hotkey_button; record_start_stop_hotkey.hotkey_active_label = hotkey_active_label; record_start_stop_hotkey.config = &config.record_config.start_stop_recording_hotkey; record_start_stop_hotkey.page = GTK_WIDGET(grid); record_start_stop_hotkey.trigger_handler = on_start_recording_button_click; record_start_stop_hotkey.associated_button = start_recording_button; record_start_stop_hotkey.shortcut_id = SHORTCUT_ID_START_STOP_RECORDING; pause_unpause_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); pause_unpause_hotkey.keysym = XK_F2; pause_unpause_hotkey.hotkey_entry = pause_unpause_hotkey_button; pause_unpause_hotkey.hotkey_active_label = hotkey_active_label; pause_unpause_hotkey.config = &config.record_config.pause_unpause_recording_hotkey; pause_unpause_hotkey.page = GTK_WIDGET(grid); pause_unpause_hotkey.trigger_handler = on_pause_unpause_button_click; pause_unpause_hotkey.associated_button = pause_recording_button; pause_unpause_hotkey.shortcut_id = SHORTCUT_ID_PAUSE_UNPAUSE_RECORDING; return GTK_WIDGET(grid); } static void create_streaming_hotkey_items(GtkGrid *parent_grid, int row, int num_columns) { streaming_hotkeys_grid = GTK_GRID(gtk_grid_new()); gtk_widget_set_halign(GTK_WIDGET(streaming_hotkeys_grid), GTK_ALIGN_START); gtk_grid_set_row_spacing(streaming_hotkeys_grid, 10); gtk_grid_set_column_spacing(streaming_hotkeys_grid, 10); gtk_grid_attach(parent_grid, GTK_WIDGET(streaming_hotkeys_grid), 0, row, num_columns, 1); int hotkeys_row = 0; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) add_wayland_global_hotkeys_ui(streaming_hotkeys_grid, hotkeys_row, num_columns); { gtk_grid_attach(streaming_hotkeys_grid, gtk_label_new("Press"), 0, hotkeys_row, 1, 1); streaming_start_stop_hotkey_button = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(streaming_start_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F1"); g_signal_connect(streaming_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), streaming_start_stop_hotkey_button); gtk_grid_attach(streaming_hotkeys_grid, streaming_start_stop_hotkey_button, 1, hotkeys_row, 1, 1); GtkWidget *start_stop_streaming_label = gtk_label_new("to start/stop streaming"); gtk_widget_set_halign(start_stop_streaming_label, GTK_ALIGN_START); gtk_widget_set_hexpand(start_stop_streaming_label, true); gtk_grid_attach(streaming_hotkeys_grid, start_stop_streaming_label, 2, hotkeys_row, 1, 1); ++hotkeys_row; } } static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { int row = 0; const int num_columns = 3; GtkGrid *grid = GTK_GRID(gtk_grid_new()); gtk_stack_add_named(stack, GTK_WIDGET(grid), "streaming"); gtk_widget_set_vexpand(GTK_WIDGET(grid), true); gtk_widget_set_hexpand(GTK_WIDGET(grid), true); gtk_grid_set_row_spacing(grid, 10); gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); GtkWidget *hotkey_active_label = nullptr; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { streaming_hotkeys_not_supported_label = gtk_label_new("Your Wayland compositor doesn't support global hotkeys. Use X11 or KDE Plasma on Wayland if you want to use hotkeys."); gtk_grid_attach(grid, streaming_hotkeys_not_supported_label, 0, row++, num_columns, 1); } else { hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel."); gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); } create_streaming_hotkey_items(grid, row, num_columns); ++row; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_grid), false); gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_not_supported_label), false); } gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); GtkGrid *stream_service_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, row++, num_columns, 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"); gtk_combo_box_text_append(stream_service_input_menu, "youtube", "Youtube"); gtk_combo_box_text_append(stream_service_input_menu, "custom", "Custom"); gtk_combo_box_set_active(GTK_COMBO_BOX(stream_service_input_menu), 0); gtk_widget_set_hexpand(GTK_WIDGET(stream_service_input_menu), true); g_signal_connect(stream_service_input_menu, "changed", G_CALLBACK(stream_service_item_change_callback), NULL); 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, row++, num_columns, 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); GtkEntry **stream_id_entries[3] = { &youtube_stream_id_entry, &twitch_stream_id_entry, &custom_stream_url_entry }; for(int i = 0; i < 3; ++i) { *stream_id_entries[i] = GTK_ENTRY(gtk_entry_new()); gtk_entry_set_visibility(*stream_id_entries[i], FALSE); gtk_entry_set_input_purpose(*stream_id_entries[i], GTK_INPUT_PURPOSE_PASSWORD); gtk_entry_set_icon_from_icon_name(*stream_id_entries[i], GTK_ENTRY_ICON_SECONDARY, "view-reveal-symbolic"); gtk_entry_set_icon_activatable(*stream_id_entries[i], GTK_ENTRY_ICON_SECONDARY, true); g_signal_connect(*stream_id_entries[i], "icon-press", G_CALLBACK(on_stream_key_icon_click), nullptr); gtk_widget_set_hexpand(GTK_WIDGET(*stream_id_entries[i]), true); gtk_grid_attach(stream_id_grid, GTK_WIDGET(*stream_id_entries[i]), 1, 0, 1, 1); gtk_widget_set_visible(GTK_WIDGET(*stream_id_entries[i]), false); } custom_stream_container_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(custom_stream_container_grid), 0, row++, num_columns, 1); gtk_grid_attach(custom_stream_container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); custom_stream_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); for(auto &supported_container : supported_containers) { gtk_combo_box_text_append(custom_stream_container, supported_container.container_name, supported_container.file_extension); } if(gsr_info.supported_video_codecs.vp8 || gsr_info.supported_video_codecs.vp9) { gtk_combo_box_text_append(custom_stream_container, "webm", "webm"); } gtk_widget_set_hexpand(GTK_WIDGET(custom_stream_container), true); gtk_grid_attach(custom_stream_container_grid, GTK_WIDGET(custom_stream_container), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(custom_stream_container), 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); stream_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); GtkWidget *go_previous = gtk_image_new_from_icon_name("go-previous", GTK_ICON_SIZE_BUTTON); gtk_button_set_image(stream_back_button, go_previous); gtk_button_set_always_show_image(stream_back_button, true); gtk_button_set_image_position(stream_back_button, GTK_POS_LEFT); gtk_widget_set_hexpand(GTK_WIDGET(stream_back_button), true); gtk_grid_attach(start_button_grid, GTK_WIDGET(stream_back_button), 0, 0, 1, 1); start_streaming_button = GTK_BUTTON(gtk_button_new_with_label("Start streaming")); gtk_widget_set_hexpand(GTK_WIDGET(start_streaming_button), true); g_signal_connect(start_streaming_button, "clicked", G_CALLBACK(on_start_streaming_button_click), app); gtk_grid_attach(start_button_grid, GTK_WIDGET(start_streaming_button), 1, 0, 1, 1); gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); streaming_bottom_panel_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(streaming_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(streaming_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(streaming_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(streaming_bottom_panel_grid), GTK_ALIGN_END); GtkWidget *record_icon = gtk_image_new_from_icon_name("media-record", GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_widget_set_valign(record_icon, GTK_ALIGN_CENTER); gtk_grid_attach(streaming_bottom_panel_grid, record_icon, 0, 0, 1, 1); streaming_record_time_label = gtk_label_new("00:00:00"); gtk_widget_set_valign(streaming_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(streaming_bottom_panel_grid, streaming_record_time_label, 1, 0, 1, 1); streaming_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); streaming_start_stop_hotkey.keysym = XK_F1; streaming_start_stop_hotkey.hotkey_entry = streaming_start_stop_hotkey_button; streaming_start_stop_hotkey.hotkey_active_label = hotkey_active_label; streaming_start_stop_hotkey.config = &config.streaming_config.start_stop_recording_hotkey; streaming_start_stop_hotkey.page = GTK_WIDGET(grid); streaming_start_stop_hotkey.trigger_handler = on_start_streaming_button_click; streaming_start_stop_hotkey.associated_button = start_streaming_button; streaming_start_stop_hotkey.shortcut_id = SHORTCUT_ID_START_STOP_RECORDING; return GTK_WIDGET(grid); } static gboolean on_destroy_window(GtkWidget*, GdkEvent*, gpointer) { if(gpu_screen_recorder_process != -1) { kill(gpu_screen_recorder_process, SIGINT); int status; if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) { perror("waitpid failed"); /* Ignore... */ } } save_configs(); return true; } static void handle_child_process_death(gpointer userdata) { if(gpu_screen_recorder_process == -1) return; int status = 0; if(waitpid(gpu_screen_recorder_process, &status, WNOHANG) == 0) return; gpu_screen_recorder_process = -1; prev_exit_status = -1; if(WIFEXITED(status)) prev_exit_status = WEXITSTATUS(status); if(replaying) { on_start_replay_button_click(start_replay_button, userdata); } else if(recording) { on_start_recording_button_click(start_recording_button, userdata); } else if(streaming) { on_start_streaming_button_click(start_streaming_button, userdata); } } static void seconds_to_record_time_format(int seconds, char *buffer, size_t buffer_size) { const int hours = seconds / 60 / 60; seconds -= (hours * 60 * 60); const int mins = seconds / 60; seconds -= (mins * 60); snprintf(buffer, buffer_size, "%02d:%02d:%02d", hours, mins, seconds); } static void handle_record_timer() { if(streaming) { const double recording_time_passed_seconds = clock_get_monotonic_seconds() - record_start_time_sec; char record_time_str[32]; seconds_to_record_time_format(recording_time_passed_seconds, record_time_str, sizeof(record_time_str)); gtk_label_set_text(GTK_LABEL(streaming_record_time_label), record_time_str); } if(recording && !paused) { const double recording_time_passed_seconds = (clock_get_monotonic_seconds() - record_start_time_sec) - paused_time_offset_sec; char record_time_str[32]; seconds_to_record_time_format(recording_time_passed_seconds, record_time_str, sizeof(record_time_str)); gtk_label_set_text(GTK_LABEL(recording_record_time_label), record_time_str); } if(replaying) { const double recording_time_passed_seconds = clock_get_monotonic_seconds() - record_start_time_sec; char record_time_str[32]; seconds_to_record_time_format(recording_time_passed_seconds, record_time_str, sizeof(record_time_str)); gtk_label_set_text(GTK_LABEL(replay_record_time_label), record_time_str); } } static void handle_notification_timer(GtkApplication *app) { if(!showing_notification) return; const double now = clock_get_monotonic_seconds(); if(now - notification_start_seconds >= notification_timeout_seconds) { g_application_withdraw_notification(&app->parent, "gpu-screen-recorder"); showing_notification = false; } } static gboolean timer_timeout_handler(gpointer userdata) { GtkApplication *app = (GtkApplication*)userdata; handle_notification_timer(app); handle_child_process_death(userdata); handle_record_timer(); return G_SOURCE_CONTINUE; } static void add_audio_input_track(const char *name) { GtkWidget *row = create_used_audio_input_row(); const AudioRow *audio_row = (AudioRow*)g_object_get_data(G_OBJECT(row), "audio-row"); std::string audio_id; gint target_combo_box_index = combo_box_text_get_row_by_label(GTK_COMBO_BOX(audio_row->input_list), name, audio_id); if(target_combo_box_index != -1) gtk_combo_box_set_active(GTK_COMBO_BOX(audio_row->input_list), target_combo_box_index); gtk_widget_show_all(row); gtk_list_box_insert (GTK_LIST_BOX(audio_input_used_list), row, -1); } static void load_config() { bool config_empty = false; config = read_config(config_empty); std::string first_monitor; if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && strcmp(config.main_config.record_area_option.c_str(), "window") == 0) { // } else if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && strcmp(config.main_config.record_area_option.c_str(), "focused") == 0) { // } else if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && gsr_info.gpu_info.vendor == GpuVendor::NVIDIA && strcmp(config.main_config.record_area_option.c_str(), "screen") == 0) { // } else if(config.main_config.record_area_option == "portal" && gsr_info.supported_capture_options.portal && gsr_info.system_info.display_server == DisplayServer::WAYLAND) { // } else { bool found_monitor = false; for(const auto &monitor : gsr_info.supported_capture_options.monitors) { if(first_monitor.empty()) first_monitor = monitor.name; if(config.main_config.record_area_option == monitor.name) found_monitor = true; } if(!found_monitor) config.main_config.record_area_option.clear(); } if(config.main_config.record_area_option.empty()) { const bool allow_screen_capture = is_monitor_capture_drm() || nvfbc_installed; if(allow_screen_capture) { config.main_config.record_area_option = first_monitor; } else { config.main_config.record_area_option = "window"; } } gtk_widget_set_visible(GTK_WIDGET(select_window_button), strcmp(config.main_config.record_area_option.c_str(), "window") == 0); //gtk_widget_set_visible(GTK_WIDGET(area_size_grid), strcmp(config.main_config.record_area_option.c_str(), "focused") == 0); gtk_widget_set_visible(GTK_WIDGET(restore_portal_session_button), strcmp(config.main_config.record_area_option.c_str(), "portal") == 0); if(config.main_config.record_area_width == 0) config.main_config.record_area_width = 1920; if(config.main_config.record_area_height == 0) config.main_config.record_area_height = 1080; if(config.main_config.video_width == 0) config.main_config.video_width = 1920; if(config.main_config.video_height == 0) config.main_config.video_height = 1080; if(config.main_config.fps == 0) config.main_config.fps = 60; else if(config.main_config.fps < 1) config.main_config.fps = 1; else if(config.main_config.fps > 5000) config.main_config.fps = 5000; if(config.main_config.color_range != "limited" && config.main_config.color_range != "full") config.main_config.color_range = "limited"; if(config.main_config.quality != "custom" && 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.audio_codec != "opus" && config.main_config.audio_codec != "aac") config.main_config.audio_codec = "opus"; if(config.main_config.framerate_mode != "auto" && config.main_config.framerate_mode != "cfr" && config.main_config.framerate_mode != "vfr") config.main_config.framerate_mode = "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"; if(config.record_config.save_directory.empty() || !is_directory(config.record_config.save_directory.c_str())) config.record_config.save_directory = get_videos_dir(); if(config.replay_config.save_directory.empty() || !is_directory(config.replay_config.save_directory.c_str())) config.replay_config.save_directory = get_videos_dir(); if(config.replay_config.replay_time < 5) config.replay_config.replay_time = 5; else if(config.replay_config.replay_time > 1200) config.replay_config.replay_time = 1200; record_area_selection_menu_set_active_id(config.main_config.record_area_option.c_str()); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { 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(video_width_entry, config.main_config.video_width); gtk_spin_button_set_value(video_height_entry, config.main_config.video_height); gtk_spin_button_set_value(fps_entry, config.main_config.fps); gtk_spin_button_set_value(video_bitrate_entry, config.main_config.video_bitrate); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button), config.main_config.merge_audio_tracks); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(change_video_resolution_button), config.main_config.change_video_resolution); for(const std::string &audio_input : config.main_config.audio_input) { add_audio_input_track(audio_input.c_str()); } if(config_empty && config.main_config.audio_input.empty()) add_audio_input_track("Default output"); gtk_combo_box_set_active_id(GTK_COMBO_BOX(color_range_input_menu), config.main_config.color_range.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(quality_input_menu), config.main_config.quality.c_str()); video_codec_selection_menu_set_active_id("auto"); video_codec_selection_menu_set_active_id(config.main_config.codec.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(audio_codec_input_menu), config.main_config.audio_codec.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(framerate_mode_input_menu), config.main_config.framerate_mode.c_str()); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(overclock_button), config.main_config.overclock); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button), config.main_config.show_recording_started_notifications); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button), config.main_config.show_recording_stopped_notifications); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button), config.main_config.show_recording_saved_notifications); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record_cursor_button), config.main_config.record_cursor); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item), config.main_config.hide_window_when_recording); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(restore_portal_session_button), config.main_config.restore_portal_session); gtk_combo_box_set_active_id(GTK_COMBO_BOX(stream_service_input_menu), config.streaming_config.streaming_service.c_str()); gtk_entry_set_text(youtube_stream_id_entry, config.streaming_config.youtube.stream_key.c_str()); gtk_entry_set_text(twitch_stream_id_entry, config.streaming_config.twitch.stream_key.c_str()); gtk_entry_set_text(custom_stream_url_entry, config.streaming_config.custom.url.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(custom_stream_container), config.streaming_config.custom.container.c_str()); 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()); 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); gtk_combo_box_set_active_id(GTK_COMBO_BOX(view_combo_box), config.main_config.advanced_view ? "advanced" : "simple"); view_combo_box_change_callback(GTK_COMBO_BOX(view_combo_box), view_combo_box); quality_combo_box_change_callback(GTK_COMBO_BOX(quality_input_menu), quality_input_menu); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { if(!config_empty) { for(int i = 0; i < num_hotkeys; ++i) { hotkeys[i]->keysym = hotkeys[i]->config->keysym; hotkeys[i]->modkey_mask = hotkeys[i]->config->modifiers; set_hotkey_text_from_hotkey_data(GTK_ENTRY(hotkeys[i]->hotkey_entry), *hotkeys[i]); } } gtk_widget_set_visible(record_start_stop_hotkey.hotkey_active_label, false); gtk_widget_set_visible(streaming_start_stop_hotkey.hotkey_active_label, false); gtk_widget_set_visible(replay_start_stop_hotkey.hotkey_active_label, false); } record_area_item_change_callback(nullptr, nullptr); stream_service_item_change_callback(GTK_COMBO_BOX(stream_service_input_menu), nullptr); on_change_video_resolution_button_click(GTK_BUTTON(change_video_resolution_button), nullptr); std::string dummy; if(!config.main_config.software_encoding_warning_shown && !switch_video_codec_to_usable_hardware_encoder(dummy)) { GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, "Unable to find a hardware video encoder on your system, using software video encoder instead (slow!). If you know that your system supports H264/HEVC hardware video encoding and " "you are using the flatpak version of GPU Screen Recorder then try installing mesa-extra freedesktop runtime by running this command:\n" "flatpak install --system org.freedesktop.Platform.GL.default//23.08-extra\n" "and then restart GPU Screen Recorder. If that doesn't work then you may have to install another mesa package for your distro.\n" "If you are using a distro such as manjaro which disables hardware accelerated video encoding then you can also try the flatpak version of GPU Screen Recorder instead which doesn't have this issue."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); config.main_config.software_encoding_warning_shown = true; video_codec_selection_menu_set_active_id("h264_software"); config.main_config.advanced_view = true; gtk_combo_box_set_active_id(GTK_COMBO_BOX(view_combo_box), "advanced"); } if(gsr_info.system_info.is_steam_deck && !config.main_config.steam_deck_warning_shown) { GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, "Steam deck has multiple driver bugs, some which have been introduced in the last few months. For example one of them has been reported here: " "https://github.com/ValveSoftware/SteamOS/issues/1609.\n" "If you have issues with GPU Screen Recorder on steam deck but not on a desktop computer then report the issue to Valve and/or AMD."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); config.main_config.steam_deck_warning_shown = true; } } static void init_shortcuts_callback(bool success, void *userdata) { (void)userdata; global_shortcuts_initialized = success; if(success) { gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_grid), true); gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_grid), true); gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_grid), true); gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_not_supported_label), false); gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_not_supported_label), false); gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_not_supported_label), false); if(!gsr_global_shortcuts_list_shortcuts(&global_shortcuts, shortcut_changed_callback, NULL)) { fprintf(stderr, "gsr error: failed to list shortcuts\n"); } gsr_global_shortcuts_subscribe_activated_signal(&global_shortcuts, deactivated_callback, shortcut_changed_callback, NULL); } else { gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_grid), false); gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_grid), false); gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_grid), false); gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_not_supported_label), true); gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_not_supported_label), true); gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_not_supported_label), true); } } static const char* gpu_vendor_to_name(GpuVendor vendor) { switch(vendor) { case GpuVendor::UNKNOWN: return "Unknown"; case GpuVendor::AMD: return "AMD"; case GpuVendor::INTEL: return "Intel"; case GpuVendor::NVIDIA: return "NVIDIA"; } return ""; } static void activate(GtkApplication *app, gpointer) { flatpak = is_inside_flatpak(); nvfbc_installed = gsr_info.system_info.display_server != DisplayServer::WAYLAND && is_nv_fbc_installed(); page_navigation_userdata.app = app; if(gsr_info_exit_status == GsrInfoExitStatus::FAILED_TO_RUN_COMMAND) { const char *cmd = flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder -w screen -f 60 -o video.mp4" : "gpu-screen-recorder -w screen -f 60 -o video.mp4"; GtkWidget *dialog = gtk_message_dialog_new_with_markup(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Failed to run 'gpu-screen-recorder' command. If you are using gpu-screen-recorder flatpak then this is a bug. Otherwise you need to make sure gpu-screen-recorder is installed on your system and working properly (install necessary depedencies depending on your GPU, such as libva-mesa-driver, libva-intel-driver, intel-media-driver and linux-firmware). Run:\n" "%s\n" "in a terminal to see more information about the issue.", cmd); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(app)); return; } if(gsr_info_exit_status == GsrInfoExitStatus::OPENGL_FAILED) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Failed to get OpenGL information. Make sure your GPU drivers are properly installed. " "If you are using nvidia then make sure to run \"flatpak update\" to make sure that your flatpak nvidia driver version matches your distros nvidia driver version. If this doesn't work then you might need to manually install a flatpak nvidia driver version that matches your distros nvidia driver version."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(app)); return; } if(gsr_info_exit_status == GsrInfoExitStatus::NO_DRM_CARD) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Failed to find a valid DRM card. If you are running GPU Screen Recorder with prime-run then try running without it."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(app)); return; } if(gsr_info.system_info.display_server == DisplayServer::UNKNOWN) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Neither X11 nor Wayland is running."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(app)); return; } if(gsr_info.system_info.display_server == DisplayServer::X11 && !dpy) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Failed to connect to X11 server"); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(app)); return; } if(gsr_info.gpu_info.vendor == GpuVendor::NVIDIA) { if(!is_cuda_installed()) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "CUDA is not installed on your system. GPU Screen Recorder requires CUDA to be installed to work with a NVIDIA GPU."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(app)); return; } if(!is_nvenc_installed()) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "NVENC is not installed on your system. GPU Screen Recorder requires NVENC to be installed to work with a NVIDIA GPU."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(app)); return; } } std::string window_title = "GPU Screen Recorder v" + std::string(GSR_VERSION) + " | Running on "; window_title += gpu_vendor_to_name(gsr_info.gpu_info.vendor); window = gtk_application_window_new(app); g_signal_connect(window, "destroy", G_CALLBACK(on_destroy_window), nullptr); gtk_window_set_title(GTK_WINDOW(window), window_title.c_str()); gtk_window_set_resizable(GTK_WINDOW(window), false); GtkIconTheme *icon_theme = gtk_icon_theme_get_default(); #ifdef GSR_ICONS_PATH const char *icon_path = GSR_ICONS_PATH; #else const char *icon_path = "/usr/share/icons"; #endif gtk_icon_theme_set_search_path(icon_theme, &icon_path, 1); const char *icon_name = "com.dec05eba.gpu_screen_recorder"; if(!gtk_icon_theme_has_icon(icon_theme, icon_name)) fprintf(stderr, "Error: failed to find icon %s in %s\n", icon_name, icon_path); gtk_window_set_default_icon_name(icon_name); gtk_window_set_icon_name(GTK_WINDOW(window), icon_name); select_window_userdata.app = app; audio_inputs = get_audio_devices(); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) crosshair_cursor = XCreateFontCursor(gdk_x11_get_default_xdisplay(), XC_crosshair); GtkStack *stack = GTK_STACK(gtk_stack_new()); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(stack)); gtk_stack_set_transition_type(stack, GTK_STACK_TRANSITION_TYPE_NONE); gtk_stack_set_transition_duration(stack, 0); gtk_stack_set_homogeneous(stack, false); GtkWidget *common_settings_page = create_common_settings_page(stack, app); GtkWidget *replay_page = create_replay_page(app, stack); GtkWidget *recording_page = create_recording_page(app, stack); GtkWidget *streaming_page = create_streaming_page(app, stack); gtk_stack_set_visible_child(stack, common_settings_page); page_navigation_userdata.stack = stack; page_navigation_userdata.common_settings_page = common_settings_page; page_navigation_userdata.replay_page = replay_page; page_navigation_userdata.recording_page = recording_page; 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_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_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_replay_page_back_click), &page_navigation_userdata); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { xim = XOpenIM(gdk_x11_get_default_xdisplay(), NULL, NULL, NULL); xic = XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, NULL); GdkWindow *root_window = gdk_get_default_root_window(); gdk_window_add_filter(root_window, hotkey_filter_callback, &page_navigation_userdata); } setup_systray(app); g_timeout_add(500, timer_timeout_handler, app); gtk_widget_show_all(window); load_config(); if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { if(gdk_wayland_display_query_registry(gdk_display_get_default(), "hyprland_global_shortcuts_manager_v1")) { wayland_compositor = WaylandCompositor::HYPRLAND; } else if(gdk_wayland_display_query_registry(gdk_display_get_default(), "org_kde_plasma_shell")) { wayland_compositor = WaylandCompositor::KDE; } init_shortcuts_callback(false, nullptr); // TODO: // Disable global hotkeys on Hyprland for now. It crashes the hyprland desktop portal. // When it's re-enabled on Hyprland it will need special handing where it does BindShortcuts immediately on startup // instead of having a "register hotkeys" button. This needed because Hyprland doesn't remember registered hotkeys after // the desktop portal is restarted (when the computer is restarted for example). if(wayland_compositor == WaylandCompositor::HYPRLAND) { const char *hotkeys_not_supported_text = "Global hotkeys have been disabled on your system because of a Hyprland bug.\nUse X11 or KDE Plasma on Wayland if you want to use hotkeys."; gtk_label_set_text(GTK_LABEL(recording_hotkeys_not_supported_label), hotkeys_not_supported_text); gtk_label_set_text(GTK_LABEL(replay_hotkeys_not_supported_label), hotkeys_not_supported_text); gtk_label_set_text(GTK_LABEL(streaming_hotkeys_not_supported_label), hotkeys_not_supported_text); } else { if(!gsr_global_shortcuts_init(&global_shortcuts, init_shortcuts_callback, NULL)) { fprintf(stderr, "gsr error: failed to initialize global shortcuts\n"); } } } } int main(int argc, char **argv) { setlocale(LC_ALL, "C"); dpy = XOpenDisplay(NULL); gsr_info_exit_status = get_gpu_screen_recorder_info(&gsr_info); if(gsr_info_exit_status == GsrInfoExitStatus::OK) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { setenv("GDK_BACKEND", "wayland", true); } else { setenv("GDK_BACKEND", "x11", true); } } GtkApplication *app = gtk_application_new("com.dec05eba.gpu_screen_recorder", G_APPLICATION_NON_UNIQUE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }