#include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { Display *display; 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 SelectWindowUserdata select_window_userdata; static PageNavigationUserdata page_navigation_userdata; static GtkWidget *save_icon; static Cursor crosshair_cursor; static GtkSpinButton *fps_entry; static GtkComboBoxText *audio_input_menu; static GtkComboBoxText *stream_service_input_menu; static GtkButton *file_chooser_button; static GtkButton *directory_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 *start_recording_button; static GtkButton *start_replay_button; static GtkButton *start_streaming_button; static GtkEntry *stream_id_entry; static GtkSpinButton *replay_time_entry; static bool replaying = false; static bool recording = false; static bool streaming = false; static std::string replay_output_path; static pid_t gpu_screen_recorder_process = -1; static pid_t ffmpeg_process = -1; static const char* get_home_dir() { const char *home_dir = getenv("HOME"); if(!home_dir) { passwd *pw = getpwuid(getuid()); home_dir = pw->pw_dir; } return home_dir; } 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 enable_stream_record_button_if_info_filled() { if(gtk_combo_box_get_active(GTK_COMBO_BOX(audio_input_menu)) == -1) return; if(select_window_userdata.selected_window == None) 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); } /* TODO: Look at xwininfo source to figure out how to make this work for different types of window managers */ static GdkFilterReturn filter_callback(GdkXEvent *xevent, GdkEvent *event, 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; if(target_win == None) { fprintf(stderr, "Selected the root window! (no window selected)\n"); return GDK_FILTER_CONTINUE; } int status = XUngrabPointer(select_window_userdata->display, CurrentTime); if(!status) { fprintf(stderr, "failed to ungrab pointer!\n"); exit(1); } std::string window_name = "Window ID: " + std::to_string(target_win) + " | 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\n", window_name.c_str()); 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 userdata) { GtkApplication *app = (GtkApplication*)userdata; 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_start_replay_click(GtkButton *button, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->replay_page); return true; } static gboolean on_start_recording_click(GtkButton *button, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->recording_page); std::string video_filepath = get_home_dir(); video_filepath += "/Videos/Video_" + get_date_str() + ".mp4"; gtk_button_set_label(file_chooser_button, video_filepath.c_str()); return true; } static gboolean on_start_streaming_click(GtkButton *button, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->streaming_page); return true; } static gboolean on_streaming_recording_page_back_click(GtkButton *button, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->common_settings_page); return true; } static gboolean on_file_chooser_button_click(GtkButton *button, gpointer userdata) { GtkWidget *file_chooser_dialog = gtk_file_chooser_dialog_new("Where do you want to save the video?", nullptr, GTK_FILE_CHOOSER_ACTION_SAVE, "Cancel", GTK_RESPONSE_CANCEL, "Save", GTK_RESPONSE_ACCEPT, nullptr); gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser_dialog), "video.mp4"); 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)); printf("filename: %s\n", filename); gtk_button_set_label(button, filename); g_free(filename); } gtk_widget_destroy(file_chooser_dialog); return true; } static gboolean on_directory_chooser_button_click(GtkButton *button, gpointer userdata) { GtkWidget *file_chooser_dialog = gtk_file_chooser_dialog_new("Where do you want to save the replays?", nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "Cancel", GTK_RESPONSE_CANCEL, "Save", GTK_RESPONSE_ACCEPT, nullptr); //gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser_dialog), "video.mp4"); 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)); printf("directory: %s\n", filename); gtk_button_set_label(button, filename); g_free(filename); } gtk_widget_destroy(file_chooser_dialog); return true; } /* TODO: XSetErrorHandler to prevent program crash, show notifications or inline error message instead of fprintf(stderr), more validations... */ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdata) { GtkApplication *app = (GtkApplication*)userdata; if(replaying) { 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... */ } } gtk_button_set_label(button, "Start replay"); replaying = false; gpu_screen_recorder_process = -1; gtk_widget_set_sensitive(GTK_WIDGET(replay_back_button), true); GNotification *notification = g_notification_new("GPU Screen Recorder"); std::string notification_body = "Replay saved to " + replay_output_path; g_notification_set_body(notification, notification_body.c_str()); g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_LOW); g_application_send_notification(&app->parent, "gpu-screen-recorder-replay", notification); return true; } const gchar *dir = gtk_button_get_label(directory_chooser_button); int fps = gtk_spin_button_get_value_as_int(fps_entry); int replay_time = gtk_spin_button_get_value_as_int(replay_time_entry); gchar* audio_input_str = gtk_combo_box_text_get_active_text(audio_input_menu); if(select_window_userdata.selected_window == None) { fprintf(stderr, "No window selected!\n"); return true; } replaying = true; gtk_widget_set_sensitive(GTK_WIDGET(replay_back_button), false); std::string window_str = std::to_string(select_window_userdata.selected_window); std::string fps_str = std::to_string(fps); std::string replay_time_str = std::to_string(replay_time); replay_output_path = dir; replay_output_path += "/Video_" + get_date_str() + ".mp4"; pid_t parent_pid = getpid(); pid_t pid = fork(); if(pid == -1) { perror("failed to fork"); exit(3); } 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); if(audio_input_str && strcmp(audio_input_str, "None") != 0) { const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "mp4", "-f", fps_str.c_str(), "-a", audio_input_str, "-r", replay_time_str.c_str(), "-o", replay_output_path.c_str(), NULL }; execvp(args[0], (char* const*)args); } else { const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "mp4", "-f", fps_str.c_str(), "-r", replay_time_str.c_str(), "-o", replay_output_path.c_str(), NULL }; execvp(args[0], (char* const*)args); } perror("failed to launch gpu-screen-recorder"); } else { /* parent process */ gpu_screen_recorder_process = pid; gtk_button_set_label(button, "Stop replaying and save"); } g_free(audio_input_str); return true; } static gboolean on_start_recording_button_click(GtkButton *button, gpointer userdata) { if(recording) { 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... */ } } gtk_button_set_label(button, "Start recording"); recording = false; gpu_screen_recorder_process = -1; gtk_widget_set_sensitive(GTK_WIDGET(record_back_button), true); std::string video_filepath = get_home_dir(); video_filepath += "/Videos/Video_" + get_date_str() + ".mp4"; gtk_button_set_label(file_chooser_button, video_filepath.c_str()); return true; } const gchar *filename = gtk_button_get_label(file_chooser_button); int fps = gtk_spin_button_get_value_as_int(fps_entry); gchar* audio_input_str = gtk_combo_box_text_get_active_text(audio_input_menu); if(select_window_userdata.selected_window == None) { fprintf(stderr, "No window selected!\n"); return true; } recording = true; gtk_widget_set_sensitive(GTK_WIDGET(record_back_button), false); std::string window_str = std::to_string(select_window_userdata.selected_window); std::string fps_str = std::to_string(fps); pid_t parent_pid = getpid(); pid_t pid = fork(); if(pid == -1) { perror("failed to fork"); exit(3); } 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); if(audio_input_str && strcmp(audio_input_str, "None") != 0) { const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "mp4", "-f", fps_str.c_str(), "-a", audio_input_str, "-o", filename, NULL }; execvp(args[0], (char* const*)args); } else { const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "mp4", "-f", fps_str.c_str(), "-o", filename, NULL }; execvp(args[0], (char* const*)args); } perror("failed to launch gpu-screen-recorder"); } else { /* parent process */ gpu_screen_recorder_process = pid; gtk_button_set_label(button, "Stop recording"); } g_free(audio_input_str); return true; } #define PIPE_READ_END 0 #define PIPE_WRITE_END 1 static pid_t launch_ffmpeg_rtmp_process(const char *url, int *pipe_write_end) { int pipes[2]; if(pipe(pipes) == -1) { perror("failed to create pipe"); exit(3); } pid_t parent_pid = getpid(); pid_t pid = fork(); if(pid == -1) { perror("failed to fork"); exit(3); } 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); dup2(pipes[PIPE_READ_END], STDIN_FILENO); close(pipes[PIPE_WRITE_END]); const char *args[] = { "ffmpeg", "-i", "pipe:0", "-c:v", "copy", "-f", "flv", "--", url, NULL }; execvp(args[0], (char* const*)args); perror("failed to launch ffmpeg"); exit(127); } else { /* parent process */ *pipe_write_end = pipes[PIPE_WRITE_END]; close(pipes[PIPE_READ_END]); } return pid; } static gboolean on_start_streaming_button_click(GtkButton *button, gpointer userdata) { if(streaming) { 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... */ } } if(ffmpeg_process != -1) kill(ffmpeg_process, SIGKILL); gtk_button_set_label(button, "Start streaming"); streaming = false; gpu_screen_recorder_process = -1; ffmpeg_process = -1; gtk_widget_set_sensitive(GTK_WIDGET(stream_back_button), true); return true; } const char *stream_id_str = gtk_entry_get_text(stream_id_entry); int fps = gtk_spin_button_get_value_as_int(fps_entry); gchar* audio_input_str = gtk_combo_box_text_get_active_text(audio_input_menu); if(select_window_userdata.selected_window == None) { fprintf(stderr, "No window selected!\n"); return true; } streaming = true; gtk_widget_set_sensitive(GTK_WIDGET(stream_back_button), false); std::string window_str = std::to_string(select_window_userdata.selected_window); std::string fps_str = std::to_string(fps); int pipe_write_end; std::string stream_url = "rtmp://live.twitch.tv/app/"; stream_url += stream_id_str; ffmpeg_process = launch_ffmpeg_rtmp_process(stream_url.c_str(), &pipe_write_end); pid_t parent_pid = getpid(); pid_t pid = fork(); if(pid == -1) { perror("failed to fork"); exit(3); } 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); // Redirect stdout to output_file dup2(pipe_write_end, STDOUT_FILENO); if(audio_input_str && strcmp(audio_input_str, "None") != 0) { const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "flv", "-f", fps_str.c_str(), "-a", audio_input_str, NULL }; execvp(args[0], (char* const*)args); } else { const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "flv", "-f", fps_str.c_str(), NULL }; execvp(args[0], (char* const*)args); } perror("failed to launch gpu-screen-recorder"); exit(127); } else { /* parent process */ gpu_screen_recorder_process = pid; close(pipe_write_end); gtk_button_set_label(button, "Stop streaming"); } g_free(audio_input_str); return true; } 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 pa_state_cb(pa_context *c, void *userdata) { pa_context_state state = pa_context_get_state(c); int *pa_ready = (int*)userdata; switch(state) { case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: default: break; case PA_CONTEXT_FAILED: case PA_CONTEXT_TERMINATED: *pa_ready = 2; break; case PA_CONTEXT_READY: *pa_ready = 1; break; } } static void pa_sourcelist_cb(pa_context *ctx, const pa_source_info *source_info, int eol, void *userdata) { if(eol > 0) return; gtk_combo_box_text_append_text(audio_input_menu, source_info->name); } static void populate_audio_input_menu_with_pulseaudio_monitors() { pa_mainloop *main_loop = pa_mainloop_new(); pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder-gtk"); pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL); int state = 0; int pa_ready = 0; pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); pa_operation *pa_op = NULL; for(;;) { // Not ready if(pa_ready == 0) { pa_mainloop_iterate(main_loop, 1, NULL); continue; } switch(state) { case 0: { pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, nullptr); ++state; break; } } // Couldn't get connection to the server if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) { if(pa_op) pa_operation_unref(pa_op); pa_context_disconnect(ctx); pa_context_unref(ctx); pa_mainloop_free(main_loop); return; } pa_mainloop_iterate(main_loop, 1, NULL); } pa_mainloop_free(main_loop); } static void audio_input_change_callback(GtkComboBox *widget, gpointer userdata) { enable_stream_record_button_if_info_filled(); } 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); GtkGrid *select_window_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(select_window_grid), 0, 0, 2, 1); gtk_grid_attach(select_window_grid, gtk_label_new("Window: "), 0, 0, 1, 1); GtkButton *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(select_window_grid, GTK_WIDGET(select_window_button), 1, 0, 1, 1); GtkGrid *fps_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(fps_grid), 0, 1, 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(10.0, 250.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); GtkGrid *audio_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(audio_grid), 0, 2, 2, 1); gtk_grid_attach(audio_grid, gtk_label_new("Audio input: "), 0, 0, 1, 1); audio_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append_text(audio_input_menu, "None"); populate_audio_input_menu_with_pulseaudio_monitors(); g_signal_connect(audio_input_menu, "changed", G_CALLBACK(audio_input_change_callback), nullptr); gtk_widget_set_hexpand(GTK_WIDGET(audio_input_menu), true); gtk_grid_attach(audio_grid, GTK_WIDGET(audio_input_menu), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(audio_input_menu), 0); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 3, 2, 1); gtk_grid_set_column_spacing(start_button_grid, 10); stream_button = GTK_BUTTON(gtk_button_new_with_label("Stream")); 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")); 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")); 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 GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { std::string video_filepath = get_home_dir(); video_filepath += "/Videos"; 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_label = gtk_label_new("Press Alt+F1 to start/stop replay"); gtk_grid_attach(grid, hotkey_label, 0, 0, 2, 1); gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 2, 1); GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, 2, 2, 1); gtk_grid_set_column_spacing(file_chooser_grid, 10); GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the replays?"); gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_label), 0, 0, 1, 1); directory_chooser_button = GTK_BUTTON(gtk_button_new_with_label(video_filepath.c_str())); gtk_button_set_image(directory_chooser_button, save_icon); gtk_button_set_always_show_image(directory_chooser_button, true); gtk_button_set_image_position(directory_chooser_button, GTK_POS_RIGHT); gtk_widget_set_hexpand(GTK_WIDGET(directory_chooser_button), true); g_signal_connect(directory_chooser_button, "clicked", G_CALLBACK(on_directory_chooser_button_click), nullptr); gtk_grid_attach(file_chooser_grid, GTK_WIDGET(directory_chooser_button), 1, 0, 1, 1); GtkGrid *replay_time_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(replay_time_grid), 0, 3, 2, 1); gtk_grid_attach(replay_time_grid, gtk_label_new("Replay time: "), 0, 0, 1, 1); replay_time_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(10.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, 4, 2, 1); gtk_grid_set_column_spacing(start_button_grid, 10); replay_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); gtk_widget_set_hexpand(GTK_WIDGET(replay_back_button), true); 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); return GTK_WIDGET(grid); } static GtkWidget* create_recording_page(GtkStack *stack) { 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_label = gtk_label_new("Press Alt+F1 to start/stop recording"); gtk_grid_attach(grid, hotkey_label, 0, 0, 2, 1); gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 2, 1); GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, 2, 2, 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); file_chooser_button = GTK_BUTTON(gtk_button_new_with_label("")); gtk_button_set_image(file_chooser_button, save_icon); gtk_button_set_always_show_image(file_chooser_button, true); gtk_button_set_image_position(file_chooser_button, GTK_POS_RIGHT); gtk_widget_set_hexpand(GTK_WIDGET(file_chooser_button), true); g_signal_connect(file_chooser_button, "clicked", G_CALLBACK(on_file_chooser_button_click), nullptr); gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_button), 1, 0, 1, 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 3, 2, 1); gtk_grid_set_column_spacing(start_button_grid, 10); record_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); gtk_widget_set_hexpand(GTK_WIDGET(record_back_button), true); 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), nullptr); gtk_grid_attach(start_button_grid, GTK_WIDGET(start_recording_button), 1, 0, 1, 1); return GTK_WIDGET(grid); } static void stream_service_change_callback(GtkComboBox *widget, gpointer userdata) { enable_stream_record_button_if_info_filled(); } static GtkWidget* create_streaming_page(GtkStack *stack) { 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_label = gtk_label_new("Press Alt+F1 to start/stop streaming"); gtk_grid_attach(grid, hotkey_label, 0, 0, 2, 1); gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 2, 1); GtkGrid *stream_service_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, 2, 2, 1); gtk_grid_attach(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_text(stream_service_input_menu, "Twitch"); //gtk_combo_box_text_append_text(stream_service_input_menu, "Custom"); g_signal_connect(stream_service_input_menu, "changed", G_CALLBACK(stream_service_change_callback), nullptr); gtk_combo_box_set_active(GTK_COMBO_BOX(stream_service_input_menu), 0); gtk_widget_set_hexpand(GTK_WIDGET(stream_service_input_menu), true); gtk_grid_attach(stream_service_grid, GTK_WIDGET(stream_service_input_menu), 1, 0, 1, 1); GtkGrid *stream_id_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(stream_id_grid), 0, 3, 2, 1); gtk_grid_attach(stream_id_grid, gtk_label_new("Stream id: "), 0, 0, 1, 1); stream_id_entry = GTK_ENTRY(gtk_entry_new()); gtk_widget_set_hexpand(GTK_WIDGET(stream_id_entry), true); gtk_grid_attach(stream_id_grid, GTK_WIDGET(stream_id_entry), 1, 0, 1, 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 4, 2, 1); gtk_grid_set_column_spacing(start_button_grid, 10); stream_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); gtk_widget_set_hexpand(GTK_WIDGET(stream_back_button), true); 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), nullptr); gtk_grid_attach(start_button_grid, GTK_WIDGET(start_streaming_button), 1, 0, 1, 1); return GTK_WIDGET(grid); } static gboolean on_destroy_window(GtkWidget *widget, GdkEvent *event, gpointer data) { 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... */ } } if(ffmpeg_process != -1) kill(ffmpeg_process, SIGKILL); return true; } typedef gboolean (*KeyPressHandler)(GtkButton *button, gpointer userdata); static void keypress_toggle_recording(bool recording_state, GtkButton *record_button, KeyPressHandler keypress_handler, GtkApplication *app) { if(!gtk_widget_get_sensitive(GTK_WIDGET(record_button))) return; if(!recording_state) { keypress_handler(record_button, app); } else if(recording_state) { keypress_handler(record_button, app); } } static bool hotkey_pressed = false; static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent *event, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; XEvent *ev = (XEvent*)xevent; Display *display = gdk_x11_get_default_xdisplay(); if((ev->type == KeyPress || ev->type == KeyRelease) && XKeycodeToKeysym(display, ev->xkey.keycode, 0) == XK_F1 && (ev->xkey.state & Mod1Mask)) { if(ev->type == KeyPress) { if(hotkey_pressed) return GDK_FILTER_CONTINUE; hotkey_pressed = true; } else if(ev->type == KeyRelease) { hotkey_pressed = false; return GDK_FILTER_CONTINUE; } GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata->stack); if(visible_page == page_navigation_userdata->recording_page) { keypress_toggle_recording(recording, start_recording_button, on_start_recording_button_click, page_navigation_userdata->app); } else if(visible_page == page_navigation_userdata->streaming_page) { keypress_toggle_recording(streaming, start_streaming_button, on_start_streaming_button_click, page_navigation_userdata->app); } else if(visible_page == page_navigation_userdata->replay_page) { keypress_toggle_recording(replaying, start_replay_button, on_start_replay_button_click, page_navigation_userdata->app); } } return GDK_FILTER_CONTINUE; } static int xerror_dummy(Display *dpy, XErrorEvent *ee) { return 0; } static void grabkeys(Display *display) { unsigned int numlockmask = 0; KeyCode numlock_keycode = XKeysymToKeycode(display, XK_Num_Lock); XModifierKeymap *modmap = XGetModifierMapping(display); for(int i = 0; i < 8; ++i) { for(int j = 0; j < modmap->max_keypermod; ++j) { if(modmap->modifiermap[i * modmap->max_keypermod + j] == numlock_keycode) numlockmask = (1 << i); } } XFreeModifiermap(modmap); XErrorHandler prev_error_handler = XSetErrorHandler(xerror_dummy); Window root_window = DefaultRootWindow(display); unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; for(int i = 0; i < 4; ++i) XGrabKey(display, XKeysymToKeycode(display, XK_F1), Mod1Mask|modifiers[i], root_window, False, GrabModeAsync, GrabModeAsync); XSetErrorHandler(prev_error_handler); } static void activate(GtkApplication *app, gpointer userdata) { GtkWidget *window = gtk_application_window_new(app); g_signal_connect(window, "destroy", G_CALLBACK(on_destroy_window), nullptr); gtk_window_set_title(GTK_WINDOW(window), "GPU Screen Recorder"); gtk_window_set_resizable(GTK_WINDOW(window), false); crosshair_cursor = XCreateFontCursor(gdk_x11_get_default_xdisplay(), XC_crosshair); save_icon = gtk_image_new_from_stock("gtk-save", GTK_ICON_SIZE_BUTTON); 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(stack); GtkWidget *streaming_page = create_streaming_page(stack); gtk_stack_set_visible_child(stack, common_settings_page); page_navigation_userdata.app = app; 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_page_back_click), &page_navigation_userdata); g_signal_connect(record_button, "clicked", G_CALLBACK(on_start_recording_click), &page_navigation_userdata); g_signal_connect(record_back_button, "clicked", G_CALLBACK(on_streaming_recording_page_back_click), &page_navigation_userdata); g_signal_connect(stream_button, "clicked", G_CALLBACK(on_start_streaming_click), &page_navigation_userdata); g_signal_connect(stream_back_button, "clicked", G_CALLBACK(on_streaming_recording_page_back_click), &page_navigation_userdata); Display *display = gdk_x11_get_default_xdisplay(); grabkeys(display); GdkWindow *root_window = gdk_get_default_root_window(); //gdk_window_set_events(root_window, GDK_BUTTON_PRESS_MASK); gdk_window_add_filter(root_window, hotkey_filter_callback, &page_navigation_userdata); Bool sup = False; XkbSetDetectableAutoRepeat(display, True, &sup); gtk_widget_show_all(window); } int main(int argc, char **argv) { GtkApplication *app = gtk_application_new("org.dec05eba.gpu-screen-recorder", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), nullptr); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }