Change 'turn on replay when starting a fullscreen game' to 'turn on replay when starting a game'

This commit is contained in:
dec05eba
2026-04-18 04:29:13 +02:00
parent a1a0736af5
commit 82453684b1
15 changed files with 384 additions and 251 deletions

View File

@@ -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";

View File

@@ -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;
}; };
} }

View File

@@ -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;
}; };
} }

View File

@@ -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)

View File

@@ -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;
} }

View File

@@ -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)

View File

@@ -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;

View 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;
}

View File

@@ -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

View File

@@ -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 dune 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

View File

@@ -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

View File

@@ -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=ディスク上

View File

@@ -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=на диске

View File

@@ -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=

View File

@@ -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=на диску