diff --git a/include/Config.hpp b/include/Config.hpp index 2dffce9..09172b3 100644 --- a/include/Config.hpp +++ b/include/Config.hpp @@ -14,7 +14,7 @@ namespace gsr { enum class ReplayStartupMode { DONT_TURN_ON_AUTOMATICALLY, TURN_ON_AT_SYSTEM_STARTUP, - TURN_ON_AT_FULLSCREEN, + TURN_ON_AT_GAME_LAUNCH, TURN_ON_AT_POWER_SUPPLY_CONNECTED }; @@ -46,10 +46,7 @@ namespace gsr { int32_t video_height = 0; int32_t fps = 60; int32_t video_bitrate = 8000; - bool merge_audio_tracks = true; // TODO: Remove in the future - bool application_audio_invert = false; // TODO: Remove in the future bool change_video_resolution = false; - std::vector audio_tracks; // ids, TODO: Remove in the future std::vector audio_tracks_list; std::string color_range = "limited"; std::string video_quality = "very_high"; diff --git a/include/Overlay.hpp b/include/Overlay.hpp index 3599d84..4ecc344 100644 --- a/include/Overlay.hpp +++ b/include/Overlay.hpp @@ -123,19 +123,20 @@ namespace gsr { void grab_mouse_and_keyboard(); void xi_setup_fake_cursor(); + void close_gsr_game_tracker_output(); void close_gpu_screen_recorder_output(); double get_time_passed_in_replay_buffer_seconds(); void update_notification_process_status(); void save_video_in_current_game_directory(std::string &video_filepath, NotificationType notification_type); void on_replay_saved(const char *replay_saved_filepath); + void process_gsr_game_tracker_output(); void process_gsr_output(); void on_gsr_process_error(int exit_code, NotificationType notification_type); void update_gsr_process_status(); void update_gsr_screenshot_process_status(); void replay_status_update_status(); - void update_focused_fullscreen_status(); void update_power_supply_status(); void update_system_startup_status(); @@ -227,7 +228,6 @@ namespace gsr { mgl::Clock replay_status_update_clock; std::string power_supply_online_filepath; bool power_supply_connected = false; - bool focused_window_is_fullscreen = false; std::string record_filepath; std::string screenshot_filepath; @@ -253,13 +253,10 @@ namespace gsr { std::unique_ptr global_hotkeys_js = nullptr; Display *x11_dpy = nullptr; XEvent x11_xev; - Atom net_active_window_atom; - bool update_focused_window = true; - std::vector game_windows; - double event_current_time_seconds = 0.0; - double gamescope_running_last_checked_seconds = 0.0; - bool is_gamescope_running = false; - bool is_game_running = false; + + int gsr_game_tracker_process_output_fd = -1; + FILE *gsr_game_tracker_process_output_file = nullptr; + pid_t gsr_game_tracker_process_id = -1; struct wl_display *wayland_dpy = nullptr; @@ -297,6 +294,5 @@ namespace gsr { std::unique_ptr led_indicator = nullptr; bool supports_window_title = false; - bool supports_window_fullscreen_state = false; }; } \ No newline at end of file diff --git a/include/gui/SettingsPage.hpp b/include/gui/SettingsPage.hpp index 2acb45e..8fc3ab5 100644 --- a/include/gui/SettingsPage.hpp +++ b/include/gui/SettingsPage.hpp @@ -41,7 +41,7 @@ namespace gsr { STREAM }; - SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack, bool supports_window_title, bool supports_window_fullscreen_state); + SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack, bool supports_window_title); SettingsPage(const SettingsPage&) = delete; SettingsPage& operator=(const SettingsPage&) = delete; @@ -253,6 +253,5 @@ namespace gsr { std::optional selected_camera_setup; bool supports_window_title = false; - bool supports_window_fullscreen_state = false; }; } \ No newline at end of file diff --git a/meson.build b/meson.build index f47e90c..6e94adf 100644 --- a/meson.build +++ b/meson.build @@ -140,6 +140,14 @@ executable( install : true ) +executable( + 'gsr-game-tracker', + [ + 'tools/gsr-game-tracker/main.c' + ], + install : true +) + install_subdir('images', install_dir : gsr_ui_resources_path) install_subdir('translations', install_dir : gsr_ui_resources_path) diff --git a/src/Config.cpp b/src/Config.cpp index d7a147b..35da83f 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -53,8 +53,8 @@ namespace gsr { return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY; else if(strcmp(startup_mode_str, "turn_on_at_system_startup") == 0) return ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP; - else if(strcmp(startup_mode_str, "turn_on_at_fullscreen") == 0) - return ReplayStartupMode::TURN_ON_AT_FULLSCREEN; + else if(strcmp(startup_mode_str, "turn_on_at_fullscreen") == 0 || strcmp(startup_mode_str, "turn_on_at_game_launch") == 0) + return ReplayStartupMode::TURN_ON_AT_GAME_LAUNCH; else if(strcmp(startup_mode_str, "turn_on_at_power_supply_connected") == 0) return ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED; else @@ -188,10 +188,7 @@ namespace gsr { {"streaming.record_options.video_height", &config.streaming_config.record_options.video_height}, {"streaming.record_options.fps", &config.streaming_config.record_options.fps}, {"streaming.record_options.video_bitrate", &config.streaming_config.record_options.video_bitrate}, - {"streaming.record_options.merge_audio_tracks", &config.streaming_config.record_options.merge_audio_tracks}, - {"streaming.record_options.application_audio_invert", &config.streaming_config.record_options.application_audio_invert}, {"streaming.record_options.change_video_resolution", &config.streaming_config.record_options.change_video_resolution}, - {"streaming.record_options.audio_track", &config.streaming_config.record_options.audio_tracks}, {"streaming.record_options.audio_track_item", &config.streaming_config.record_options.audio_tracks_list}, {"streaming.record_options.color_range", &config.streaming_config.record_options.color_range}, {"streaming.record_options.video_quality", &config.streaming_config.record_options.video_quality}, @@ -233,10 +230,7 @@ namespace gsr { {"record.record_options.video_height", &config.record_config.record_options.video_height}, {"record.record_options.fps", &config.record_config.record_options.fps}, {"record.record_options.video_bitrate", &config.record_config.record_options.video_bitrate}, - {"record.record_options.merge_audio_tracks", &config.record_config.record_options.merge_audio_tracks}, - {"record.record_options.application_audio_invert", &config.record_config.record_options.application_audio_invert}, {"record.record_options.change_video_resolution", &config.record_config.record_options.change_video_resolution}, - {"record.record_options.audio_track", &config.record_config.record_options.audio_tracks}, {"record.record_options.audio_track_item", &config.record_config.record_options.audio_tracks_list}, {"record.record_options.color_range", &config.record_config.record_options.color_range}, {"record.record_options.video_quality", &config.record_config.record_options.video_quality}, @@ -275,10 +269,7 @@ namespace gsr { {"replay.record_options.video_height", &config.replay_config.record_options.video_height}, {"replay.record_options.fps", &config.replay_config.record_options.fps}, {"replay.record_options.video_bitrate", &config.replay_config.record_options.video_bitrate}, - {"replay.record_options.merge_audio_tracks", &config.replay_config.record_options.merge_audio_tracks}, - {"replay.record_options.application_audio_invert", &config.replay_config.record_options.application_audio_invert}, {"replay.record_options.change_video_resolution", &config.replay_config.record_options.change_video_resolution}, - {"replay.record_options.audio_track", &config.replay_config.record_options.audio_tracks}, {"replay.record_options.audio_track_item", &config.replay_config.record_options.audio_tracks_list}, {"replay.record_options.color_range", &config.replay_config.record_options.color_range}, {"replay.record_options.video_quality", &config.replay_config.record_options.video_quality}, @@ -372,17 +363,6 @@ namespace gsr { return !operator==(other); } - static void populate_new_audio_track_from_old(RecordOptions &record_options) { - if(record_options.merge_audio_tracks) { - record_options.audio_tracks_list.push_back({std::move(record_options.audio_tracks), record_options.application_audio_invert}); - } else { - for(const std::string &audio_input : record_options.audio_tracks) { - record_options.audio_tracks_list.push_back({std::vector{audio_input}, record_options.application_audio_invert}); - } - } - record_options.audio_tracks.clear(); - } - std::optional read_config(const SupportedCaptureOptions &capture_options) { std::optional config; @@ -395,10 +375,6 @@ namespace gsr { config = Config(capture_options); - config->streaming_config.record_options.audio_tracks.clear(); - config->record_config.record_options.audio_tracks.clear(); - config->replay_config.record_options.audio_tracks.clear(); - config->streaming_config.record_options.audio_tracks_list.clear(); config->record_config.record_options.audio_tracks_list.clear(); config->replay_config.record_options.audio_tracks_list.clear(); @@ -465,15 +441,9 @@ namespace gsr { return true; }); - if(config->main_config.config_file_version == 1) { - populate_new_audio_track_from_old(config->streaming_config.record_options); - populate_new_audio_track_from_old(config->record_config.record_options); - populate_new_audio_track_from_old(config->replay_config.record_options); - } - - config->streaming_config.record_options.audio_tracks.clear(); - config->record_config.record_options.audio_tracks.clear(); - config->replay_config.record_options.audio_tracks.clear(); + // TODO: Remove in the future + if(config->replay_config.turn_on_replay_automatically_mode == "turn_on_at_fullscreen") + config->replay_config.turn_on_replay_automatically_mode = "turn_on_at_game_launch"; return config; } diff --git a/src/Overlay.cpp b/src/Overlay.cpp index 3cd0bb9..36f1bce 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -264,6 +264,42 @@ namespace gsr { return true; } + static bool are_all_audio_tracks_available_to_capture(const std::vector &audio_tracks) { + const auto audio_devices = get_audio_devices(); + for(const AudioTrack &audio_track : audio_tracks) { + for(const std::string &audio_input : audio_track.audio_inputs) { + std::string_view audio_track_name(audio_input.c_str()); + const bool is_app_audio = starts_with(audio_track_name, "app:"); + if(is_app_audio) + continue; + + if(starts_with(audio_track_name, "device:")) + audio_track_name.remove_prefix(7); + + auto it = std::find_if(audio_devices.begin(), audio_devices.end(), [&](const auto &audio_device) { + return audio_device.name == audio_track_name; + }); + if(it == audio_devices.end()) { + //fprintf(stderr, "Audio not ready\n"); + return false; + } + } + } + return true; + } + + static bool is_webcam_available_to_capture(const RecordOptions &record_options) { + if(record_options.webcam_source.empty()) + return true; + + const auto cameras = get_v4l2_devices(); + for(const GsrCamera &camera : cameras) { + if(camera.path == record_options.webcam_source) + return true; + } + return false; + } + // Note that this doesn't work in the flatpak right now because of this flatpak bug: // https://github.com/flatpak/flatpak/issues/6486 static bool is_hyprland_waybar_running_as_dock() { @@ -466,12 +502,15 @@ namespace gsr { } } - 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 pid_t launch_gsr_game_tracker(int *stdout_fd) { + const bool is_flatpak = getenv("FLATPAK_ID") != nullptr; + if(is_flatpak) { + const char *args[] = { "flatpak-spawn", "--host", "--", "gsr-game-tracker", NULL }; + return exec_program(args, stdout_fd, false); + } else { + const char *args[] = { "gsr-game-tracker", NULL }; + return exec_program(args, stdout_fd, false); + } } Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) : @@ -484,9 +523,6 @@ namespace gsr { top_bar_background({0.0f, 0.0f}), close_button_widget({0.0f, 0.0f}) { - event_current_time_seconds = clock_get_monotonic_seconds(); - gamescope_running_last_checked_seconds = event_current_time_seconds - 10.0; - if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND) { wayland_dpy = wl_display_connect(nullptr); if(!wayland_dpy) @@ -530,8 +566,6 @@ namespace gsr { x11_dpy = XOpenDisplay(nullptr); if(x11_dpy) { - net_active_window_atom = XInternAtom(x11_dpy, "_NET_ACTIVE_WINDOW", False); - XSelectInput(x11_dpy, DefaultRootWindow(x11_dpy), PropertyChangeMask); XKeysymToKeycode(x11_dpy, XK_F1); // If we dont call we will never get a MappingNotify } else { fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n"); @@ -540,7 +574,6 @@ namespace gsr { if(this->gsr_info.system_info.display_server == DisplayServer::X11) { cursor_tracker = std::make_unique((Display*)mgl_get_context()->connection); supports_window_title = true; - supports_window_fullscreen_state = true; } else if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND) { if(!this->gsr_info.gpu_info.card_path.empty()) cursor_tracker = std::make_unique(this->gsr_info.gpu_info.card_path.c_str(), wayland_dpy); @@ -553,6 +586,17 @@ namespace gsr { } update_led_indicator_after_settings_change(); + + gsr_game_tracker_process_id = launch_gsr_game_tracker(&gsr_game_tracker_process_output_fd); + if(gsr_game_tracker_process_id > 0) { + const int fdl = fcntl(gsr_game_tracker_process_output_fd, F_GETFL); + fcntl(gsr_game_tracker_process_output_fd, F_SETFL, fdl | O_NONBLOCK); + gsr_game_tracker_process_output_file = fdopen(gsr_game_tracker_process_output_fd, "r"); + if(gsr_game_tracker_process_output_file) + gsr_game_tracker_process_output_fd = -1; + } else { + fprintf(stderr, "Warning: failed to launch gsr-game-tracker. The feature to start replay when a game starts will not work\n"); + } } Overlay::~Overlay() { @@ -588,8 +632,19 @@ namespace gsr { gpu_screen_recorder_screenshot_process = -1; } + if(gsr_game_tracker_process_id > 0) { + kill(gsr_game_tracker_process_id, SIGINT); + int status; + if(waitpid(gsr_game_tracker_process_id, &status, 0) == -1) { + perror("waitpid failed"); + /* Ignore... */ + } + gsr_game_tracker_process_id = -1; + } + led_indicator.reset(); + close_gsr_game_tracker_output(); close_gpu_screen_recorder_output(); deinit_color_theme(); @@ -650,6 +705,18 @@ namespace gsr { } } + void Overlay::close_gsr_game_tracker_output() { + if(gsr_game_tracker_process_output_file) { + fclose(gsr_game_tracker_process_output_file); + gsr_game_tracker_process_output_file = nullptr; + } + + if(gsr_game_tracker_process_output_fd > 0) { + close(gsr_game_tracker_process_output_fd); + gsr_game_tracker_process_output_fd = -1; + } + } + void Overlay::close_gpu_screen_recorder_output() { if(gpu_screen_recorder_process_output_file) { fclose(gpu_screen_recorder_process_output_file); @@ -740,64 +807,6 @@ namespace gsr { } } - static bool x11_window_is_steam_game(Display *dpy, Window window) { - unsigned long steam_game_id = 0; - unsigned int property_size = 0; - unsigned char* steam_game_property = window_get_property(dpy, window, XA_CARDINAL, "STEAM_GAME", &property_size); - if(steam_game_property) { - if(property_size == 8) - steam_game_id = *(unsigned long*)steam_game_property; - XFree(steam_game_property); - } - - const unsigned long steam_webhelper_game_id = 769; - return steam_game_id != 0 && steam_game_id != steam_webhelper_game_id; - } - - // WINE/Godot/Unity application detection - static bool x11_window_class_is_game(Display *dpy, Window window) { - XClassHint class_hint = {nullptr, nullptr}; - XGetClassHint(dpy, window, &class_hint); - - const bool is_godot_application = class_hint.res_name && strcmp(class_hint.res_name, "Godot_Engine") == 0; - const bool is_wine_application = class_hint.res_class && (ends_with(class_hint.res_class, ".exe") || ends_with(class_hint.res_class, ".EXE")); - const bool is_native_unity_32bit_application = class_hint.res_class && ends_with(class_hint.res_class, ".x86"); - const bool is_native_unity_64bit_application = class_hint.res_class && (ends_with(class_hint.res_class, ".x86_64") || ends_with(class_hint.res_class, ".x64")); - - if(class_hint.res_name) - XFree(class_hint.res_name); - - if(class_hint.res_class) - XFree(class_hint.res_class); - - return is_godot_application || is_wine_application || is_native_unity_32bit_application || is_native_unity_64bit_application; - } - - static bool x11_window_is_game(Display *dpy, Window window) { - return x11_window_is_steam_game(dpy, window) || x11_window_class_is_game(dpy, window); - } - - static bool x11_is_server_gamescope(Display *dpy) { - bool is_gamescope = false; - unsigned int property_size = 0; - unsigned char* gamescope_focused_window = window_get_property(dpy, DefaultRootWindow(dpy), XA_CARDINAL, "GAMESCOPE_FOCUSED_WINDOW", &property_size); - if(gamescope_focused_window) { - is_gamescope = true; - XFree(gamescope_focused_window); - } - return is_gamescope; - } - - static bool is_gamescope_x11_server_running() { - bool is_gamescope = false; - Display *gamescope_x11_dpy = XOpenDisplay(":2"); - if(gamescope_x11_dpy) { - is_gamescope = x11_is_server_gamescope(gamescope_x11_dpy); - XCloseDisplay(gamescope_x11_dpy); - } - return is_gamescope; - } - void Overlay::handle_keyboard_mapping_event() { if(!x11_dpy) return; @@ -811,54 +820,14 @@ namespace gsr { mapping_updated = true; break; } - case PropertyNotify: { - if(x11_xev.xproperty.state == PropertyNewValue && x11_xev.xproperty.atom == net_active_window_atom) - update_focused_window = true; - break; - } - case DestroyNotify: { - auto it = std::find(game_windows.begin(), game_windows.end(), x11_xev.xdestroywindow.window); - if(it != game_windows.end()) - game_windows.erase(it); - break; - } } } if(mapping_updated) rebind_all_keyboard_hotkeys(); - - if(update_focused_window) { - update_focused_window = false; - - const Window focused_window = get_focused_window(x11_dpy, WindowCaptureType::FOCUSED, false); - if(x11_window_is_game(x11_dpy, focused_window)) { - if(std::find(game_windows.begin(), game_windows.end(), focused_window) == game_windows.end()) { - XSelectInput(x11_dpy, focused_window, StructureNotifyMask); - game_windows.push_back(focused_window); - } - } - } - - if(event_current_time_seconds - gamescope_running_last_checked_seconds >= 3.0) { - gamescope_running_last_checked_seconds = event_current_time_seconds; - is_gamescope_running = is_gamescope_x11_server_running(); - } - - const bool prev_game_is_running = is_game_running; - is_game_running = !game_windows.empty() || is_gamescope_running; - if(is_game_running != prev_game_is_running) { - if(is_game_running) { - //fprintf(stderr, "started game\n"); - } else { - //fprintf(stderr, "stopped game\n"); - } - } } void Overlay::handle_events() { - event_current_time_seconds = clock_get_monotonic_seconds(); - if(led_indicator) led_indicator->update(); @@ -953,6 +922,7 @@ namespace gsr { } update_notification_process_status(); + process_gsr_game_tracker_output(); process_gsr_output(); update_gsr_process_status(); update_gsr_screenshot_process_status(); @@ -1362,7 +1332,7 @@ namespace gsr { button->set_item_icon("settings", &get_theme().settings_extra_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { - auto replay_settings_page = std::make_unique(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack, supports_window_title, supports_window_fullscreen_state); + auto replay_settings_page = std::make_unique(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack, supports_window_title); replay_settings_page->on_config_changed = [this]() { replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str()); if(recording_status == RecordingStatus::REPLAY) @@ -1396,7 +1366,7 @@ namespace gsr { button->set_item_icon("settings", &get_theme().settings_extra_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { - auto record_settings_page = std::make_unique(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack, supports_window_title, supports_window_fullscreen_state); + auto record_settings_page = std::make_unique(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack, supports_window_title); record_settings_page->on_config_changed = [this]() { if(recording_status == RecordingStatus::RECORD) show_notification(TR("Recording settings have been modified. You may need to restart recording to apply the changes."), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); @@ -1423,7 +1393,7 @@ namespace gsr { button->set_item_icon("settings", &get_theme().settings_extra_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { - auto stream_settings_page = std::make_unique(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack, supports_window_title, supports_window_fullscreen_state); + auto stream_settings_page = std::make_unique(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack, supports_window_title); stream_settings_page->on_config_changed = [this]() { if(recording_status == RecordingStatus::STREAM) show_notification(TR("Streaming settings have been modified. You may need to restart streaming to apply the changes."), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); @@ -2211,6 +2181,26 @@ namespace gsr { led_indicator->blink(); } + void Overlay::process_gsr_game_tracker_output() { + char buffer[1024]; + if(gsr_game_tracker_process_output_file) { + char *line = fgets(buffer, sizeof(buffer), gsr_game_tracker_process_output_file); + if(!line || line[0] == '\0') + return; + + if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_GAME_LAUNCH) + return; + + if(recording_status == RecordingStatus::NONE && strncmp(line, "Game launched", 13) == 0) { + on_press_start_replay(false, false); + } else if(recording_status == RecordingStatus::REPLAY && strncmp(line, "Game exited", 11) == 0) { + on_press_start_replay(false, false); + } + } else if(gsr_game_tracker_process_output_fd > 0) { + read(gsr_game_tracker_process_output_fd, buffer, sizeof(buffer)); + } + } + void Overlay::process_gsr_output() { if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) { replay_save_show_notification = false; @@ -2218,8 +2208,8 @@ namespace gsr { show_notification(TR("Saving replay, this might take some time"), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); } + char buffer[1024]; if(gpu_screen_recorder_process_output_file) { - char buffer[1024]; char *line = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file); if(!line || line[0] == '\0') return; @@ -2252,7 +2242,6 @@ namespace gsr { break; } } else if(gpu_screen_recorder_process_output_fd > 0) { - char buffer[1024]; read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer)); } } @@ -2402,85 +2391,15 @@ namespace gsr { gpu_screen_recorder_screenshot_process = -1; } - static bool are_all_audio_tracks_available_to_capture(const std::vector &audio_tracks) { - const auto audio_devices = get_audio_devices(); - for(const AudioTrack &audio_track : audio_tracks) { - for(const std::string &audio_input : audio_track.audio_inputs) { - std::string_view audio_track_name(audio_input.c_str()); - const bool is_app_audio = starts_with(audio_track_name, "app:"); - if(is_app_audio) - continue; - - if(starts_with(audio_track_name, "device:")) - audio_track_name.remove_prefix(7); - - auto it = std::find_if(audio_devices.begin(), audio_devices.end(), [&](const auto &audio_device) { - return audio_device.name == audio_track_name; - }); - if(it == audio_devices.end()) { - //fprintf(stderr, "Audio not ready\n"); - return false; - } - } - } - return true; - } - - static bool is_webcam_available_to_capture(const RecordOptions &record_options) { - if(record_options.webcam_source.empty()) - return true; - - const auto cameras = get_v4l2_devices(); - for(const GsrCamera &camera : cameras) { - if(camera.path == record_options.webcam_source) - return true; - } - return false; - } - void Overlay::replay_status_update_status() { if(replay_status_update_clock.get_elapsed_time_seconds() < replay_status_update_check_timeout_seconds) return; replay_status_update_clock.restart(); - update_focused_fullscreen_status(); update_power_supply_status(); update_system_startup_status(); } - void Overlay::update_focused_fullscreen_status() { - if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_FULLSCREEN) - return; - - mgl_context *context = mgl_get_context(); - Display *display = (Display*)context->connection; - - const bool prev_focused_window_is_fullscreen = focused_window_is_fullscreen; - Window focused_window = None; - - focused_window = get_focused_window(display, WindowCaptureType::FOCUSED, false); - if(window && focused_window == (Window)window->get_system_handle()) - return; - - focused_window_is_fullscreen = focused_window != 0 && window_is_fullscreen(display, focused_window); - - if(focused_window_is_fullscreen != prev_focused_window_is_fullscreen) { - std::string fullscreen_window_monitor; - auto window_monitor = get_monitor_by_window_center(display, focused_window); - if(window_monitor.has_value()) - fullscreen_window_monitor = std::move(window_monitor->name); - else - fullscreen_window_monitor.clear(); - - if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) { - if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list) && is_webcam_available_to_capture(config.replay_config.record_options)) - on_press_start_replay(false, false, fullscreen_window_monitor); - } else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) { - on_press_start_replay(true, false, fullscreen_window_monitor); - } - } - } - // TODO: Instead of checking power supply status periodically listen to power supply event void Overlay::update_power_supply_status() { if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED) diff --git a/src/gui/SettingsPage.cpp b/src/gui/SettingsPage.cpp index d99f610..3e19134 100644 --- a/src/gui/SettingsPage.cpp +++ b/src/gui/SettingsPage.cpp @@ -45,14 +45,13 @@ namespace gsr { return result; } - SettingsPage::SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack, bool supports_window_title, bool supports_window_fullscreen_state) : + SettingsPage::SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack, bool supports_window_title) : StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()), type(type), config(config), gsr_info(gsr_info), page_stack(page_stack), - supports_window_title(supports_window_title), - supports_window_fullscreen_state(supports_window_fullscreen_state) + supports_window_title(supports_window_title) { audio_devices = get_audio_devices(); application_audio = get_application_audio(); @@ -1137,14 +1136,10 @@ namespace gsr { } std::unique_ptr SettingsPage::create_start_replay_automatically() { - // TODO: Support hyprland (same ones that support getting window title) - char fullscreen_text[256]; - snprintf(fullscreen_text, sizeof(fullscreen_text), TR("Turn on replay when starting a fullscreen application%s"), supports_window_fullscreen_state ? "" : TR(" (X11 applications only)")); - auto radiobutton = std::make_unique(get_theme().body_font_desc.c_str(), RadioButton::Orientation::VERTICAL); radiobutton->add_item(TR("Don't turn on replay automatically"), "dont_turn_on_automatically"); radiobutton->add_item(TR("Turn on replay when this program starts"), "turn_on_at_system_startup"); - radiobutton->add_item(fullscreen_text, "turn_on_at_fullscreen"); + radiobutton->add_item(TR("Turn on replay when starting a game"), "turn_on_at_game_launch"); radiobutton->add_item(TR("Turn on replay when power supply is connected"), "turn_on_at_power_supply_connected"); turn_on_replay_automatically_mode_ptr = radiobutton.get(); return radiobutton; diff --git a/tools/gsr-game-tracker/main.c b/tools/gsr-game-tracker/main.c new file mode 100644 index 0000000..585026c --- /dev/null +++ b/tools/gsr-game-tracker/main.c @@ -0,0 +1,249 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_GAMES 32 +#define ENVIRON_BUF_SIZE (64 * 1024) +#define CMDLINE_BUF_SIZE 4096 +#define RECV_BUF_SIZE 8192 + +static pid_t games[MAX_GAMES]; /* 0 = empty slot */ +static int game_count = 0; +static char environ_buf[ENVIRON_BUF_SIZE]; +static char cmdline_buf[CMDLINE_BUF_SIZE]; +static char recv_buf[RECV_BUF_SIZE]; + +static int find_slot_by_pid(pid_t pid) { + for (int i = 0; i < MAX_GAMES; i++) { + if (games[i] == pid) + return i; + } + return -1; +} + +static int find_free_slot(void) { + for (int i = 0; i < MAX_GAMES; i++) { + if (!games[i]) + return i; + } + return -1; +} + +static void add_game(pid_t pid) { + int slot = find_free_slot(); + if (slot < 0) return; + games[slot] = pid; + if (game_count++ == 0) { + printf("Game launched\n"); + fflush(stdout); + } +} + +static void remove_game(pid_t pid) { + int slot = find_slot_by_pid(pid); + if (slot < 0) return; + games[slot] = 0; + if (--game_count == 0) { + printf("Game exited\n"); + fflush(stdout); + } +} + +static ssize_t read_file(const char *path, char *buf, size_t size) { + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + ssize_t n = read(fd, buf, size - 1); + close(fd); + if (n > 0) buf[n] = '\0'; + return n; +} + +/* Return pointer to value inside null-separated environ block, or NULL. */ +static const char *env_get(const char *env, ssize_t size, const char *key) { + size_t klen = strlen(key); + const char *p = env; + const char *end = env + size; + while (p < end) { + if (strncmp(p, key, klen) == 0 && p[klen] == '=') + return p + klen + 1; + size_t elen = strnlen(p, (size_t)(end - p)); + p += elen + 1; + } + return NULL; +} + +static int is_wine_binary(const char *argv0) { + const char *base = strrchr(argv0, '/'); + base = base ? base + 1 : argv0; + return strcmp(base, "wine") == 0 || + strcmp(base, "wine64") == 0 || + strcmp(base, "wine-preloader") == 0 || + strcmp(base, "wine64-preloader") == 0; +} + +static int has_game_arch_suffix(const char *argv0) { + const char *base = strrchr(argv0, '/'); + base = base ? base + 1 : argv0; + size_t len = strlen(base); + static const char *suffixes[] = { ".x86_64", ".x64", ".x86" }; + for (int i = 0; i < 3; i++) { + size_t slen = strlen(suffixes[i]); + if (len > slen && strcmp(base + len - slen, suffixes[i]) == 0) + return 1; + } + return 0; +} + +static void check_process(pid_t pid) { + if (find_slot_by_pid(pid) >= 0) return; + + char path[64]; + ssize_t env_n, cmd_n; + + snprintf(path, sizeof(path), "/proc/%d/environ", pid); + env_n = read_file(path, environ_buf, sizeof(environ_buf)); + + if (env_n > 0) { + const char *appid = env_get(environ_buf, env_n, "SteamAppId"); + if (!appid) appid = env_get(environ_buf, env_n, "SteamGameId"); + if (appid && appid[0] >= '1' && appid[0] <= '9') { + add_game(pid); + return; + } + } + + snprintf(path, sizeof(path), "/proc/%d/cmdline", pid); + cmd_n = read_file(path, cmdline_buf, sizeof(cmdline_buf)); + + if (cmd_n > 0 && (is_wine_binary(cmdline_buf) || has_game_arch_suffix(cmdline_buf))) + add_game(pid); +} + +static void handle_proc_event(const struct proc_event *ev) { + switch (ev->what) { + case PROC_EVENT_EXEC: + check_process(ev->event_data.exec.process_tgid); + break; + case PROC_EVENT_EXIT: + /* Only act when the whole process (not just a thread) exits */ + if (ev->event_data.exit.process_pid == ev->event_data.exit.process_tgid) + remove_game(ev->event_data.exit.process_tgid); + break; + default: + break; + } +} + +static void process_netlink_msg(const char *buf, size_t len) { + const struct nlmsghdr *nl = (const struct nlmsghdr *)buf; + for (; NLMSG_OK(nl, (unsigned int)len); nl = NLMSG_NEXT(nl, len)) { + if (nl->nlmsg_type == NLMSG_NOOP || + nl->nlmsg_type == NLMSG_ERROR || + nl->nlmsg_type == NLMSG_OVERRUN) + continue; + if (nl->nlmsg_len < NLMSG_HDRLEN + sizeof(struct cn_msg)) + continue; + const struct cn_msg *cn = (const struct cn_msg *)NLMSG_DATA(nl); + if (cn->id.idx != CN_IDX_PROC || cn->id.val != CN_VAL_PROC) + continue; + if (cn->len < sizeof(struct proc_event)) + continue; + handle_proc_event((const struct proc_event *)cn->data); + } +} + +static int send_mcast_op(int sock, enum proc_cn_mcast_op op) { + /* cn_msg ends with data[0], so we can't place fields after it in a struct. + * Use a flat buffer and fill via pointers instead. */ + char buf[NLMSG_SPACE(sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op))]; + memset(buf, 0, sizeof(buf)); + + struct nlmsghdr *nl = (struct nlmsghdr *)buf; + nl->nlmsg_len = sizeof(buf); + nl->nlmsg_type = NLMSG_DONE; + nl->nlmsg_pid = (unsigned int)getpid(); + + struct cn_msg *cn = (struct cn_msg *)NLMSG_DATA(nl); + cn->id.idx = CN_IDX_PROC; + cn->id.val = CN_VAL_PROC; + cn->len = sizeof(enum proc_cn_mcast_op); + memcpy(cn->data, &op, sizeof(op)); + + return send(sock, buf, sizeof(buf), 0) < 0 ? -1 : 0; +} + +static int setup_netlink(void) { + int sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR); + if (sock < 0) { + perror("socket(AF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR)"); + return -1; + } + + struct sockaddr_nl sa; + memset(&sa, 0, sizeof(sa)); + sa.nl_family = AF_NETLINK; + sa.nl_groups = CN_IDX_PROC; + sa.nl_pid = (unsigned int)getpid(); + + if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + perror("bind"); + close(sock); + return -1; + } + + if (send_mcast_op(sock, PROC_CN_MCAST_LISTEN) < 0) { + perror("send PROC_CN_MCAST_LISTEN"); + close(sock); + return -1; + } + + return sock; +} + +static void scan_existing_processes(void) { + DIR *d = opendir("/proc"); + if (!d) return; + + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + const char *s = ent->d_name; + if (*s < '1' || *s > '9') continue; + pid_t pid = 0; + for (; *s; s++) { + if (*s < '0' || *s > '9') { pid = 0; break; } + pid = pid * 10 + (*s - '0'); + } + if (pid > 0) check_process(pid); + } + closedir(d); +} + +int main(void) { + memset(games, 0, sizeof(games)); + + int sock = setup_netlink(); + if (sock < 0) return 1; + + scan_existing_processes(); + + for (;;) { + ssize_t n = recv(sock, recv_buf, sizeof(recv_buf), 0); + if (n < 0) { + if (errno == EINTR) continue; + perror("recv"); + break; + } + process_netlink_msg(recv_buf, (size_t)n); + } + + send_mcast_op(sock, PROC_CN_MCAST_IGNORE); + close(sock); + return 0; +} diff --git a/translations/es.txt b/translations/es.txt index 82d949e..844bc80 100644 --- a/translations/es.txt +++ b/translations/es.txt @@ -388,7 +388,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=Tamaño estimado Directory to save replays:=Directorio para guardar las repeticiones: Replay indicator=Indicador de repetición replay=repetición -Turn on replay when starting a fullscreen application%s=Activar la repetición al iniciar una aplicación a pantalla completa%s +Turn on replay when starting a game=Activa la repetición al iniciar un juego Autostart=Inicio automático in RAM=en RAM on disk=en disco diff --git a/translations/fr.txt b/translations/fr.txt index 1ec62f6..c5a645a 100644 --- a/translations/fr.txt +++ b/translations/fr.txt @@ -401,7 +401,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=Taille estimée d Directory to save replays:=Répertoire pour sauvegarder les replays Replay indicator=Indicateur de replay replay=replay -Turn on replay when starting a fullscreen application%s=Activer le replay lors du lancement d’une application plein écran %s +Turn on replay when starting a game=Activez la rediffusion au début d'une partie Autostart=Démarrage automatique in RAM=En RAM on disk=Sur le disque diff --git a/translations/hu.txt b/translations/hu.txt index 8d7e86b..a833d4d 100644 --- a/translations/hu.txt +++ b/translations/hu.txt @@ -393,7 +393,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=Várható videóf Directory to save replays:=Visszajátszások mentési mappája: Replay indicator=Visszajátszás-jelző replay=visszajátszás -Turn on replay when starting a fullscreen application%s=Visszajátszás bekapcsolása teljes képernyős alkalmazás indításakor%s +Turn on replay when starting a game=Kapcsolja be az újrajátszást játék indításakor Autostart=Automatikus indítás in RAM=RAM-ban on disk=lemezen diff --git a/translations/ja.txt b/translations/ja.txt index 36a077b..35321b6 100644 --- a/translations/ja.txt +++ b/translations/ja.txt @@ -405,7 +405,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=推定動画サ Directory to save replays:=リプレイの保存先: Replay indicator=リプレイインジケータ replay=リプレイ -Turn on replay when starting a fullscreen application%s=フルスクリーンアプリ開始時にリプレイをオンにする%s +Turn on replay when starting a game=ゲーム開始時にリプレイをオンにする Autostart=自動開始 in RAM=RAM 内 on disk=ディスク上 diff --git a/translations/ru.txt b/translations/ru.txt index ecdecc4..c0cb3c2 100644 --- a/translations/ru.txt +++ b/translations/ru.txt @@ -388,7 +388,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=Примерны # Replay settings Directory to save replays:=Каталог для сохранения повторов: Replay indicator=Индикатор повтора -Turn on replay when starting a fullscreen application%s=Включать повтор при запуске полноэкранного приложения%s +Turn on replay when starting a game=Включите воспроизведение при запуске игры Autostart=Автозапуск in RAM=в ОЗУ on disk=на диске diff --git a/translations/template.txt b/translations/template.txt index bae24fa..f6669a2 100644 --- a/translations/template.txt +++ b/translations/template.txt @@ -404,7 +404,7 @@ Estimated video file size per minute (excluding audio): %.2fMB= Directory to save replays:= Replay indicator= replay= -Turn on replay when starting a fullscreen application%s= +Turn on replay when starting a game= Autostart= in RAM= on disk= diff --git a/translations/uk.txt b/translations/uk.txt index 721b1bf..2775cdd 100644 --- a/translations/uk.txt +++ b/translations/uk.txt @@ -386,7 +386,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=Приблизн # Replay settings Directory to save replays:=Каталог для збереження повторів: Replay indicator=Індикатор повтору -Turn on replay when starting a fullscreen application%s=Увімкнути повтор при запуску повноекранної програми%s +Turn on replay when starting a game=Увімкнути повтор під час початку гри Autostart=Автозапуск in RAM=в ОЗП on disk=на диску