mirror of
https://repo.dec05eba.com/gpu-screen-recorder-ui
synced 2026-05-06 06:50:44 +09:00
Change 'turn on replay when starting a fullscreen game' to 'turn on replay when starting a game'
This commit is contained in:
@@ -14,7 +14,7 @@ namespace gsr {
|
|||||||
enum class ReplayStartupMode {
|
enum class ReplayStartupMode {
|
||||||
DONT_TURN_ON_AUTOMATICALLY,
|
DONT_TURN_ON_AUTOMATICALLY,
|
||||||
TURN_ON_AT_SYSTEM_STARTUP,
|
TURN_ON_AT_SYSTEM_STARTUP,
|
||||||
TURN_ON_AT_FULLSCREEN,
|
TURN_ON_AT_GAME_LAUNCH,
|
||||||
TURN_ON_AT_POWER_SUPPLY_CONNECTED
|
TURN_ON_AT_POWER_SUPPLY_CONNECTED
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,10 +46,7 @@ namespace gsr {
|
|||||||
int32_t video_height = 0;
|
int32_t video_height = 0;
|
||||||
int32_t fps = 60;
|
int32_t fps = 60;
|
||||||
int32_t video_bitrate = 8000;
|
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;
|
bool change_video_resolution = false;
|
||||||
std::vector<std::string> audio_tracks; // ids, TODO: Remove in the future
|
|
||||||
std::vector<AudioTrack> audio_tracks_list;
|
std::vector<AudioTrack> audio_tracks_list;
|
||||||
std::string color_range = "limited";
|
std::string color_range = "limited";
|
||||||
std::string video_quality = "very_high";
|
std::string video_quality = "very_high";
|
||||||
|
|||||||
@@ -123,19 +123,20 @@ namespace gsr {
|
|||||||
void grab_mouse_and_keyboard();
|
void grab_mouse_and_keyboard();
|
||||||
void xi_setup_fake_cursor();
|
void xi_setup_fake_cursor();
|
||||||
|
|
||||||
|
void close_gsr_game_tracker_output();
|
||||||
void close_gpu_screen_recorder_output();
|
void close_gpu_screen_recorder_output();
|
||||||
|
|
||||||
double get_time_passed_in_replay_buffer_seconds();
|
double get_time_passed_in_replay_buffer_seconds();
|
||||||
void update_notification_process_status();
|
void update_notification_process_status();
|
||||||
void save_video_in_current_game_directory(std::string &video_filepath, NotificationType notification_type);
|
void save_video_in_current_game_directory(std::string &video_filepath, NotificationType notification_type);
|
||||||
void on_replay_saved(const char *replay_saved_filepath);
|
void on_replay_saved(const char *replay_saved_filepath);
|
||||||
|
void process_gsr_game_tracker_output();
|
||||||
void process_gsr_output();
|
void process_gsr_output();
|
||||||
void on_gsr_process_error(int exit_code, NotificationType notification_type);
|
void on_gsr_process_error(int exit_code, NotificationType notification_type);
|
||||||
void update_gsr_process_status();
|
void update_gsr_process_status();
|
||||||
void update_gsr_screenshot_process_status();
|
void update_gsr_screenshot_process_status();
|
||||||
|
|
||||||
void replay_status_update_status();
|
void replay_status_update_status();
|
||||||
void update_focused_fullscreen_status();
|
|
||||||
void update_power_supply_status();
|
void update_power_supply_status();
|
||||||
void update_system_startup_status();
|
void update_system_startup_status();
|
||||||
|
|
||||||
@@ -227,7 +228,6 @@ namespace gsr {
|
|||||||
mgl::Clock replay_status_update_clock;
|
mgl::Clock replay_status_update_clock;
|
||||||
std::string power_supply_online_filepath;
|
std::string power_supply_online_filepath;
|
||||||
bool power_supply_connected = false;
|
bool power_supply_connected = false;
|
||||||
bool focused_window_is_fullscreen = false;
|
|
||||||
|
|
||||||
std::string record_filepath;
|
std::string record_filepath;
|
||||||
std::string screenshot_filepath;
|
std::string screenshot_filepath;
|
||||||
@@ -253,13 +253,10 @@ namespace gsr {
|
|||||||
std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr;
|
std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr;
|
||||||
Display *x11_dpy = nullptr;
|
Display *x11_dpy = nullptr;
|
||||||
XEvent x11_xev;
|
XEvent x11_xev;
|
||||||
Atom net_active_window_atom;
|
|
||||||
bool update_focused_window = true;
|
int gsr_game_tracker_process_output_fd = -1;
|
||||||
std::vector<Window> game_windows;
|
FILE *gsr_game_tracker_process_output_file = nullptr;
|
||||||
double event_current_time_seconds = 0.0;
|
pid_t gsr_game_tracker_process_id = -1;
|
||||||
double gamescope_running_last_checked_seconds = 0.0;
|
|
||||||
bool is_gamescope_running = false;
|
|
||||||
bool is_game_running = false;
|
|
||||||
|
|
||||||
struct wl_display *wayland_dpy = nullptr;
|
struct wl_display *wayland_dpy = nullptr;
|
||||||
|
|
||||||
@@ -297,6 +294,5 @@ namespace gsr {
|
|||||||
std::unique_ptr<LedIndicator> led_indicator = nullptr;
|
std::unique_ptr<LedIndicator> led_indicator = nullptr;
|
||||||
|
|
||||||
bool supports_window_title = false;
|
bool supports_window_title = false;
|
||||||
bool supports_window_fullscreen_state = false;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ namespace gsr {
|
|||||||
STREAM
|
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(const SettingsPage&) = delete;
|
||||||
SettingsPage& operator=(const SettingsPage&) = delete;
|
SettingsPage& operator=(const SettingsPage&) = delete;
|
||||||
|
|
||||||
@@ -253,6 +253,5 @@ namespace gsr {
|
|||||||
std::optional<GsrCameraSetup> selected_camera_setup;
|
std::optional<GsrCameraSetup> selected_camera_setup;
|
||||||
|
|
||||||
bool supports_window_title = false;
|
bool supports_window_title = false;
|
||||||
bool supports_window_fullscreen_state = false;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -140,6 +140,14 @@ executable(
|
|||||||
install : true
|
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('images', install_dir : gsr_ui_resources_path)
|
||||||
install_subdir('translations', install_dir : gsr_ui_resources_path)
|
install_subdir('translations', install_dir : gsr_ui_resources_path)
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ namespace gsr {
|
|||||||
return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY;
|
return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY;
|
||||||
else if(strcmp(startup_mode_str, "turn_on_at_system_startup") == 0)
|
else if(strcmp(startup_mode_str, "turn_on_at_system_startup") == 0)
|
||||||
return ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
|
return ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
|
||||||
else if(strcmp(startup_mode_str, "turn_on_at_fullscreen") == 0)
|
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_FULLSCREEN;
|
return ReplayStartupMode::TURN_ON_AT_GAME_LAUNCH;
|
||||||
else if(strcmp(startup_mode_str, "turn_on_at_power_supply_connected") == 0)
|
else if(strcmp(startup_mode_str, "turn_on_at_power_supply_connected") == 0)
|
||||||
return ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED;
|
return ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED;
|
||||||
else
|
else
|
||||||
@@ -188,10 +188,7 @@ namespace gsr {
|
|||||||
{"streaming.record_options.video_height", &config.streaming_config.record_options.video_height},
|
{"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.fps", &config.streaming_config.record_options.fps},
|
||||||
{"streaming.record_options.video_bitrate", &config.streaming_config.record_options.video_bitrate},
|
{"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.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.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.color_range", &config.streaming_config.record_options.color_range},
|
||||||
{"streaming.record_options.video_quality", &config.streaming_config.record_options.video_quality},
|
{"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.video_height", &config.record_config.record_options.video_height},
|
||||||
{"record.record_options.fps", &config.record_config.record_options.fps},
|
{"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.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.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.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.color_range", &config.record_config.record_options.color_range},
|
||||||
{"record.record_options.video_quality", &config.record_config.record_options.video_quality},
|
{"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.video_height", &config.replay_config.record_options.video_height},
|
||||||
{"replay.record_options.fps", &config.replay_config.record_options.fps},
|
{"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.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.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.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.color_range", &config.replay_config.record_options.color_range},
|
||||||
{"replay.record_options.video_quality", &config.replay_config.record_options.video_quality},
|
{"replay.record_options.video_quality", &config.replay_config.record_options.video_quality},
|
||||||
@@ -372,17 +363,6 @@ namespace gsr {
|
|||||||
return !operator==(other);
|
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<std::string>{audio_input}, record_options.application_audio_invert});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
record_options.audio_tracks.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options) {
|
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options) {
|
||||||
std::optional<Config> config;
|
std::optional<Config> config;
|
||||||
|
|
||||||
@@ -395,10 +375,6 @@ namespace gsr {
|
|||||||
|
|
||||||
config = Config(capture_options);
|
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->streaming_config.record_options.audio_tracks_list.clear();
|
||||||
config->record_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();
|
config->replay_config.record_options.audio_tracks_list.clear();
|
||||||
@@ -465,15 +441,9 @@ namespace gsr {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if(config->main_config.config_file_version == 1) {
|
// TODO: Remove in the future
|
||||||
populate_new_audio_track_from_old(config->streaming_config.record_options);
|
if(config->replay_config.turn_on_replay_automatically_mode == "turn_on_at_fullscreen")
|
||||||
populate_new_audio_track_from_old(config->record_config.record_options);
|
config->replay_config.turn_on_replay_automatically_mode = "turn_on_at_game_launch";
|
||||||
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();
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
289
src/Overlay.cpp
289
src/Overlay.cpp
@@ -264,6 +264,42 @@ namespace gsr {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool are_all_audio_tracks_available_to_capture(const std::vector<AudioTrack> &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:
|
// Note that this doesn't work in the flatpak right now because of this flatpak bug:
|
||||||
// https://github.com/flatpak/flatpak/issues/6486
|
// https://github.com/flatpak/flatpak/issues/6486
|
||||||
static bool is_hyprland_waybar_running_as_dock() {
|
static bool is_hyprland_waybar_running_as_dock() {
|
||||||
@@ -466,12 +502,15 @@ namespace gsr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static double clock_get_monotonic_seconds(void) {
|
static pid_t launch_gsr_game_tracker(int *stdout_fd) {
|
||||||
struct timespec ts;
|
const bool is_flatpak = getenv("FLATPAK_ID") != nullptr;
|
||||||
ts.tv_sec = 0;
|
if(is_flatpak) {
|
||||||
ts.tv_nsec = 0;
|
const char *args[] = { "flatpak-spawn", "--host", "--", "gsr-game-tracker", NULL };
|
||||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
return exec_program(args, stdout_fd, false);
|
||||||
return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001;
|
} 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) :
|
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}),
|
top_bar_background({0.0f, 0.0f}),
|
||||||
close_button_widget({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) {
|
if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
|
||||||
wayland_dpy = wl_display_connect(nullptr);
|
wayland_dpy = wl_display_connect(nullptr);
|
||||||
if(!wayland_dpy)
|
if(!wayland_dpy)
|
||||||
@@ -530,8 +566,6 @@ namespace gsr {
|
|||||||
|
|
||||||
x11_dpy = XOpenDisplay(nullptr);
|
x11_dpy = XOpenDisplay(nullptr);
|
||||||
if(x11_dpy) {
|
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
|
XKeysymToKeycode(x11_dpy, XK_F1); // If we dont call we will never get a MappingNotify
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n");
|
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) {
|
if(this->gsr_info.system_info.display_server == DisplayServer::X11) {
|
||||||
cursor_tracker = std::make_unique<CursorTrackerX11>((Display*)mgl_get_context()->connection);
|
cursor_tracker = std::make_unique<CursorTrackerX11>((Display*)mgl_get_context()->connection);
|
||||||
supports_window_title = true;
|
supports_window_title = true;
|
||||||
supports_window_fullscreen_state = true;
|
|
||||||
} else if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
|
} else if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
|
||||||
if(!this->gsr_info.gpu_info.card_path.empty())
|
if(!this->gsr_info.gpu_info.card_path.empty())
|
||||||
cursor_tracker = std::make_unique<CursorTrackerWayland>(this->gsr_info.gpu_info.card_path.c_str(), wayland_dpy);
|
cursor_tracker = std::make_unique<CursorTrackerWayland>(this->gsr_info.gpu_info.card_path.c_str(), wayland_dpy);
|
||||||
@@ -553,6 +586,17 @@ namespace gsr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_led_indicator_after_settings_change();
|
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() {
|
Overlay::~Overlay() {
|
||||||
@@ -588,8 +632,19 @@ namespace gsr {
|
|||||||
gpu_screen_recorder_screenshot_process = -1;
|
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();
|
led_indicator.reset();
|
||||||
|
|
||||||
|
close_gsr_game_tracker_output();
|
||||||
close_gpu_screen_recorder_output();
|
close_gpu_screen_recorder_output();
|
||||||
deinit_color_theme();
|
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() {
|
void Overlay::close_gpu_screen_recorder_output() {
|
||||||
if(gpu_screen_recorder_process_output_file) {
|
if(gpu_screen_recorder_process_output_file) {
|
||||||
fclose(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() {
|
void Overlay::handle_keyboard_mapping_event() {
|
||||||
if(!x11_dpy)
|
if(!x11_dpy)
|
||||||
return;
|
return;
|
||||||
@@ -811,54 +820,14 @@ namespace gsr {
|
|||||||
mapping_updated = true;
|
mapping_updated = true;
|
||||||
break;
|
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)
|
if(mapping_updated)
|
||||||
rebind_all_keyboard_hotkeys();
|
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() {
|
void Overlay::handle_events() {
|
||||||
event_current_time_seconds = clock_get_monotonic_seconds();
|
|
||||||
|
|
||||||
if(led_indicator)
|
if(led_indicator)
|
||||||
led_indicator->update();
|
led_indicator->update();
|
||||||
|
|
||||||
@@ -953,6 +922,7 @@ namespace gsr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_notification_process_status();
|
update_notification_process_status();
|
||||||
|
process_gsr_game_tracker_output();
|
||||||
process_gsr_output();
|
process_gsr_output();
|
||||||
update_gsr_process_status();
|
update_gsr_process_status();
|
||||||
update_gsr_screenshot_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->set_item_icon("settings", &get_theme().settings_extra_small_texture);
|
||||||
button->on_click = [this](const std::string &id) {
|
button->on_click = [this](const std::string &id) {
|
||||||
if(id == "settings") {
|
if(id == "settings") {
|
||||||
auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack, supports_window_title, supports_window_fullscreen_state);
|
auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack, supports_window_title);
|
||||||
replay_settings_page->on_config_changed = [this]() {
|
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());
|
replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str());
|
||||||
if(recording_status == RecordingStatus::REPLAY)
|
if(recording_status == RecordingStatus::REPLAY)
|
||||||
@@ -1396,7 +1366,7 @@ namespace gsr {
|
|||||||
button->set_item_icon("settings", &get_theme().settings_extra_small_texture);
|
button->set_item_icon("settings", &get_theme().settings_extra_small_texture);
|
||||||
button->on_click = [this](const std::string &id) {
|
button->on_click = [this](const std::string &id) {
|
||||||
if(id == "settings") {
|
if(id == "settings") {
|
||||||
auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack, supports_window_title, supports_window_fullscreen_state);
|
auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack, supports_window_title);
|
||||||
record_settings_page->on_config_changed = [this]() {
|
record_settings_page->on_config_changed = [this]() {
|
||||||
if(recording_status == RecordingStatus::RECORD)
|
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);
|
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->set_item_icon("settings", &get_theme().settings_extra_small_texture);
|
||||||
button->on_click = [this](const std::string &id) {
|
button->on_click = [this](const std::string &id) {
|
||||||
if(id == "settings") {
|
if(id == "settings") {
|
||||||
auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack, supports_window_title, supports_window_fullscreen_state);
|
auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack, supports_window_title);
|
||||||
stream_settings_page->on_config_changed = [this]() {
|
stream_settings_page->on_config_changed = [this]() {
|
||||||
if(recording_status == RecordingStatus::STREAM)
|
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);
|
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();
|
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() {
|
void Overlay::process_gsr_output() {
|
||||||
if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) {
|
if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) {
|
||||||
replay_save_show_notification = false;
|
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);
|
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) {
|
if(gpu_screen_recorder_process_output_file) {
|
||||||
char buffer[1024];
|
|
||||||
char *line = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file);
|
char *line = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file);
|
||||||
if(!line || line[0] == '\0')
|
if(!line || line[0] == '\0')
|
||||||
return;
|
return;
|
||||||
@@ -2252,7 +2242,6 @@ namespace gsr {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if(gpu_screen_recorder_process_output_fd > 0) {
|
} else if(gpu_screen_recorder_process_output_fd > 0) {
|
||||||
char buffer[1024];
|
|
||||||
read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer));
|
read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2402,85 +2391,15 @@ namespace gsr {
|
|||||||
gpu_screen_recorder_screenshot_process = -1;
|
gpu_screen_recorder_screenshot_process = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool are_all_audio_tracks_available_to_capture(const std::vector<AudioTrack> &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() {
|
void Overlay::replay_status_update_status() {
|
||||||
if(replay_status_update_clock.get_elapsed_time_seconds() < replay_status_update_check_timeout_seconds)
|
if(replay_status_update_clock.get_elapsed_time_seconds() < replay_status_update_check_timeout_seconds)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
replay_status_update_clock.restart();
|
replay_status_update_clock.restart();
|
||||||
update_focused_fullscreen_status();
|
|
||||||
update_power_supply_status();
|
update_power_supply_status();
|
||||||
update_system_startup_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
|
// TODO: Instead of checking power supply status periodically listen to power supply event
|
||||||
void Overlay::update_power_supply_status() {
|
void Overlay::update_power_supply_status() {
|
||||||
if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED)
|
if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED)
|
||||||
|
|||||||
@@ -45,14 +45,13 @@ namespace gsr {
|
|||||||
return result;
|
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()),
|
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
|
||||||
type(type),
|
type(type),
|
||||||
config(config),
|
config(config),
|
||||||
gsr_info(gsr_info),
|
gsr_info(gsr_info),
|
||||||
page_stack(page_stack),
|
page_stack(page_stack),
|
||||||
supports_window_title(supports_window_title),
|
supports_window_title(supports_window_title)
|
||||||
supports_window_fullscreen_state(supports_window_fullscreen_state)
|
|
||||||
{
|
{
|
||||||
audio_devices = get_audio_devices();
|
audio_devices = get_audio_devices();
|
||||||
application_audio = get_application_audio();
|
application_audio = get_application_audio();
|
||||||
@@ -1137,14 +1136,10 @@ namespace gsr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() {
|
std::unique_ptr<RadioButton> 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<RadioButton>(get_theme().body_font_desc.c_str(), RadioButton::Orientation::VERTICAL);
|
auto radiobutton = std::make_unique<RadioButton>(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("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(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");
|
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();
|
turn_on_replay_automatically_mode_ptr = radiobutton.get();
|
||||||
return radiobutton;
|
return radiobutton;
|
||||||
|
|||||||
249
tools/gsr-game-tracker/main.c
Normal file
249
tools/gsr-game-tracker/main.c
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <linux/netlink.h>
|
||||||
|
#include <linux/connector.h>
|
||||||
|
#include <linux/cn_proc.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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:
|
Directory to save replays:=Directorio para guardar las repeticiones:
|
||||||
Replay indicator=Indicador de repetición
|
Replay indicator=Indicador de repetición
|
||||||
replay=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
|
Autostart=Inicio automático
|
||||||
in RAM=en RAM
|
in RAM=en RAM
|
||||||
on disk=en disco
|
on disk=en disco
|
||||||
|
|||||||
@@ -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
|
Directory to save replays:=Répertoire pour sauvegarder les replays
|
||||||
Replay indicator=Indicateur de replay
|
Replay indicator=Indicateur de replay
|
||||||
replay=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
|
Autostart=Démarrage automatique
|
||||||
in RAM=En RAM
|
in RAM=En RAM
|
||||||
on disk=Sur le disque
|
on disk=Sur le disque
|
||||||
|
|||||||
@@ -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:
|
Directory to save replays:=Visszajátszások mentési mappája:
|
||||||
Replay indicator=Visszajátszás-jelző
|
Replay indicator=Visszajátszás-jelző
|
||||||
replay=visszajátszás
|
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
|
Autostart=Automatikus indítás
|
||||||
in RAM=RAM-ban
|
in RAM=RAM-ban
|
||||||
on disk=lemezen
|
on disk=lemezen
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=推定動画サ
|
|||||||
Directory to save replays:=リプレイの保存先:
|
Directory to save replays:=リプレイの保存先:
|
||||||
Replay indicator=リプレイインジケータ
|
Replay indicator=リプレイインジケータ
|
||||||
replay=リプレイ
|
replay=リプレイ
|
||||||
Turn on replay when starting a fullscreen application%s=フルスクリーンアプリ開始時にリプレイをオンにする%s
|
Turn on replay when starting a game=ゲーム開始時にリプレイをオンにする
|
||||||
Autostart=自動開始
|
Autostart=自動開始
|
||||||
in RAM=RAM 内
|
in RAM=RAM 内
|
||||||
on disk=ディスク上
|
on disk=ディスク上
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=Примерны
|
|||||||
# Replay settings
|
# Replay settings
|
||||||
Directory to save replays:=Каталог для сохранения повторов:
|
Directory to save replays:=Каталог для сохранения повторов:
|
||||||
Replay indicator=Индикатор повтора
|
Replay indicator=Индикатор повтора
|
||||||
Turn on replay when starting a fullscreen application%s=Включать повтор при запуске полноэкранного приложения%s
|
Turn on replay when starting a game=Включите воспроизведение при запуске игры
|
||||||
Autostart=Автозапуск
|
Autostart=Автозапуск
|
||||||
in RAM=в ОЗУ
|
in RAM=в ОЗУ
|
||||||
on disk=на диске
|
on disk=на диске
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=
|
|||||||
Directory to save replays:=
|
Directory to save replays:=
|
||||||
Replay indicator=
|
Replay indicator=
|
||||||
replay=
|
replay=
|
||||||
Turn on replay when starting a fullscreen application%s=
|
Turn on replay when starting a game=
|
||||||
Autostart=
|
Autostart=
|
||||||
in RAM=
|
in RAM=
|
||||||
on disk=
|
on disk=
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ Estimated video file size per minute (excluding audio): %.2fMB=Приблизн
|
|||||||
# Replay settings
|
# Replay settings
|
||||||
Directory to save replays:=Каталог для збереження повторів:
|
Directory to save replays:=Каталог для збереження повторів:
|
||||||
Replay indicator=Індикатор повтору
|
Replay indicator=Індикатор повтору
|
||||||
Turn on replay when starting a fullscreen application%s=Увімкнути повтор при запуску повноекранної програми%s
|
Turn on replay when starting a game=Увімкнути повтор під час початку гри
|
||||||
Autostart=Автозапуск
|
Autostart=Автозапуск
|
||||||
in RAM=в ОЗП
|
in RAM=в ОЗП
|
||||||
on disk=на диску
|
on disk=на диску
|
||||||
|
|||||||
Reference in New Issue
Block a user