aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2026-08-05 15:26:36 +0200
committerdec05eba <dec05eba@protonmail.com>2026-08-05 15:26:36 +0200
commit3a1ce0e17f209702095aaa86fedeb74400b3d039 (patch)
treed16516e0876361da09c4ff6f05be5f190d0891a1
parentf17157274d02ddf88af7af95ad6353b231983f5a (diff)
ipc: save-replay: add restart-replay option
-rw-r--r--gpu-screen-recorder.118
-rw-r--r--gsr-cli.116
-rw-r--r--include/cli/ipc.h7
-rw-r--r--include/recorder/recorder.h11
-rw-r--r--src/cli/ipc.c57
-rw-r--r--src/cli/main.c12
-rw-r--r--src/recorder/recorder.c9
-rw-r--r--tools/gsr-cli/main.c56
8 files changed, 153 insertions, 33 deletions
diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1
index 67857a8..9f461cc 100644
--- a/gpu-screen-recorder.1
+++ b/gpu-screen-recorder.1
@@ -527,8 +527,22 @@ saved file, except in replay mode where nothing is saved.
.B save-replay
Save replay (replay mode only).
.B data
-is the number of seconds to save, which has to be larger than 0. The whole replay buffer is saved when
+is an object with these optional fields:
+.RS
+.TP
+.B seconds
+The number of seconds to save, which has to be larger than 0.
+.TP
+.B restart-replay
+true/false, which overrides the
+.B \-restart\-replay\-on\-save
+option for this save. Just like that option, the replay buffer is only cleared when the whole replay buffer is saved.
+.RE
+.IP
+The whole replay buffer is saved when
.B data
+is omitted or null, or when
+.B seconds
is omitted or null. The reply contains the path of the saved file.
.TP
.B toggle-pause
@@ -559,7 +573,7 @@ Example:
.nf
.RS
gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" &
-echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock"
+echo '{"id":1,"name":"save-replay","data":{"seconds":30}}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock"
.RE
.fi
which replies with the following when the replay has been saved:
diff --git a/gsr-cli.1 b/gsr-cli.1
index 0a05df9..14d1f51 100644
--- a/gsr-cli.1
+++ b/gsr-cli.1
@@ -6,7 +6,7 @@ gsr\-cli \- Control a running GPU Screen Recorder instance
.B \-ipc
.I socket_path
.I command
-.RI [ command_argument ]
+.RI [ command_arguments... ]
.PP
.B gsr\-cli
.B \-h
@@ -70,10 +70,15 @@ option. Does nothing when a recording is already running.
Stop the regular recording that runs during replay/streaming. Waits until the recording has been
saved and prints the path of the saved file. Fails when no recording is running.
.TP
-.BR save-replay " [" \fIseconds\fR ]
+.BR save-replay " [" \fIseconds\fR "] [" restart-replay=true|false ]
Save replay (replay mode only). The number of seconds has to be larger than 0. The whole replay
buffer is saved when no number of seconds is given. Waits until the replay has been saved and
prints the path of the saved file.
+.B restart-replay
+overrides the
+.B \-restart\-replay\-on\-save
+option of GPU Screen Recorder for this save, which clears the replay buffer after the whole replay
+buffer has been saved.
.SH EXAMPLES
Start GPU Screen Recorder in replay mode with an ipc socket:
.RS
@@ -89,6 +94,13 @@ gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30
.fi
.RE
.PP
+Save the whole replay buffer and clear it:
+.RS
+.nf
+gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay restart-replay=true
+.fi
+.RE
+.PP
Start replay only when it isn't already running:
.RS
.nf
diff --git a/include/cli/ipc.h b/include/cli/ipc.h
index 6008db9..0c4c076 100644
--- a/include/cli/ipc.h
+++ b/include/cli/ipc.h
@@ -28,8 +28,11 @@ typedef struct {
bool (*toggle_replay_recording)(char *error_message, size_t error_message_size, void *userdata);
bool (*start_replay_recording)(char *error_message, size_t error_message_size, void *userdata);
bool (*stop_replay_recording)(char *error_message, size_t error_message_size, void *userdata);
- /* |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved */
- bool (*save_replay)(int seconds, char *error_message, size_t error_message_size, void *userdata);
+ /*
+ |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved.
+ |restart_replay| overrides the -restart-replay-on-save option for this save when |has_restart_replay| is set.
+ */
+ bool (*save_replay)(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata);
void *userdata;
} gsr_ipc_handlers;
diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h
index 887b4fe..5721a98 100644
--- a/include/recorder/recorder.h
+++ b/include/recorder/recorder.h
@@ -55,7 +55,14 @@ void gsr_recorder_start_replay_recording(gsr_recorder *self);
/* Does nothing when no recording is running */
void gsr_recorder_stop_replay_recording(gsr_recorder *self);
bool gsr_recorder_is_replay_recording(const gsr_recorder *self);
-/* |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */
-void gsr_recorder_save_replay(gsr_recorder *self, int seconds);
+#define GSR_RESTART_REPLAY_USE_OPTION -1
+#define GSR_RESTART_REPLAY_DISABLE 0
+#define GSR_RESTART_REPLAY_ENABLE 1
+
+/*
+ |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer.
+ |restart_replay| overrides the -restart-replay-on-save option for this save when it's not GSR_RESTART_REPLAY_USE_OPTION.
+*/
+void gsr_recorder_save_replay(gsr_recorder *self, int seconds, int restart_replay);
#endif /* GSR_RECORDER_RECORDER_H */
diff --git a/src/cli/ipc.c b/src/cli/ipc.c
index ff4b852..2fb3000 100644
--- a/src/cli/ipc.c
+++ b/src/cli/ipc.c
@@ -37,6 +37,7 @@ typedef struct {
char name[GSR_IPC_MAX_REQUEST_NAME_SIZE];
sj_Value data;
bool has_data;
+ const char *request_end;
} gsr_ipc_request;
typedef struct {
@@ -269,6 +270,7 @@ static bool ipc_client_send_reply(gsr_ipc *self, gsr_ipc_client *client, int64_t
static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, char *error_message, size_t error_message_size) {
memset(request, 0, sizeof(*request));
+ request->request_end = data + size;
sj_Reader reader = sj_reader(data, size);
const sj_Value root = sj_read(&reader);
@@ -326,18 +328,57 @@ static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request,
return true;
}
-static bool ipc_request_get_save_replay_seconds(const gsr_ipc_request *request, int *seconds, char *error_message, size_t error_message_size) {
+static bool json_value_to_save_replay_seconds(const sj_Value *value, int *seconds, char *error_message, size_t error_message_size) {
+ int64_t data_seconds = 0;
+ if(!gsr_json_number_to_int64(value, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) {
+ snprintf(error_message, error_message_size, "expected the number of seconds to save to be larger than 0");
+ return false;
+ }
+
+ *seconds = data_seconds;
+ return true;
+}
+
+static bool ipc_request_get_save_replay_options(const gsr_ipc_request *request, int *seconds, bool *has_restart_replay, bool *restart_replay, char *error_message, size_t error_message_size) {
*seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
+ *has_restart_replay = false;
+ *restart_replay = false;
if(!request->has_data || request->data.type == SJ_NULL)
return true;
- int64_t data_seconds = 0;
- if(!gsr_json_number_to_int64(&request->data, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) {
- snprintf(error_message, error_message_size, "expected 'data' to be the number of seconds to save, which has to be larger than 0");
+ if(request->data.type != SJ_OBJECT) {
+ snprintf(error_message, error_message_size, "expected 'data' to be an object with the optional fields 'seconds' and 'restart-replay'");
+ return false;
+ }
+
+ sj_Reader reader = sj_reader(request->data.start, request->request_end - request->data.start);
+ const sj_Value data = sj_read(&reader);
+
+ sj_Value key;
+ sj_Value value;
+ while(sj_iter_object(&reader, data, &key, &value)) {
+ if(gsr_json_string_equals(&key, "seconds")) {
+ if(value.type == SJ_NULL)
+ continue;
+
+ if(!json_value_to_save_replay_seconds(&value, seconds, error_message, error_message_size))
+ return false;
+ } else if(gsr_json_string_equals(&key, "restart-replay")) {
+ if(value.type != SJ_BOOL) {
+ snprintf(error_message, error_message_size, "expected 'restart-replay' to be true or false");
+ return false;
+ }
+
+ *has_restart_replay = true;
+ *restart_replay = gsr_json_string_equals(&value, "true");
+ }
+ }
+
+ if(reader.error) {
+ snprintf(error_message, error_message_size, "failed to parse 'data': %s", reader.error);
return false;
}
- *seconds = data_seconds;
return true;
}
@@ -437,10 +478,12 @@ static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, ch
if(strcmp(request->name, "save-replay") == 0) {
int seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
- if(!ipc_request_get_save_replay_seconds(request, &seconds, error_message, error_message_size))
+ bool has_restart_replay = false;
+ bool restart_replay = false;
+ if(!ipc_request_get_save_replay_options(request, &seconds, &has_restart_replay, &restart_replay, error_message, error_message_size))
return false;
- return self->handlers.save_replay(seconds, error_message, error_message_size, self->handlers.userdata);
+ return self->handlers.save_replay(seconds, has_restart_replay, restart_replay, error_message, error_message_size, self->handlers.userdata);
}
snprintf(error_message, error_message_size, "unknown request name '%s'", request->name);
diff --git a/src/cli/main.c b/src/cli/main.c
index de014a7..ac7de24 100644
--- a/src/cli/main.c
+++ b/src/cli/main.c
@@ -59,7 +59,7 @@ static void toggle_replay_recording_handler(int signal_value) {
static void save_replay_seconds_handler(gsr_recorder *rec, int seconds) {
if(rec)
- gsr_recorder_save_replay(rec, seconds);
+ gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION);
else
pending_save_replay_seconds = seconds;
}
@@ -78,7 +78,7 @@ static void apply_pending_signals(gsr_recorder *rec) {
if(pending_save_replay_seconds != 0) {
const int seconds = pending_save_replay_seconds;
pending_save_replay_seconds = 0;
- gsr_recorder_save_replay(rec, seconds);
+ gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION);
}
}
@@ -186,14 +186,18 @@ static bool ipc_stop_replay_recording_handler(char *error_message, size_t error_
return true;
}
-static bool ipc_save_replay_handler(int seconds, char *error_message, size_t error_message_size, void *userdata) {
+static bool ipc_save_replay_handler(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata) {
const gsr_recorder_settings *settings = userdata;
if(!settings->is_replaying) {
snprintf(error_message, error_message_size, "option -r is required to save a replay");
return false;
}
- gsr_recorder_save_replay(recorder, seconds);
+ int restart_replay_request = GSR_RESTART_REPLAY_USE_OPTION;
+ if(has_restart_replay)
+ restart_replay_request = restart_replay ? GSR_RESTART_REPLAY_ENABLE : GSR_RESTART_REPLAY_DISABLE;
+
+ gsr_recorder_save_replay(recorder, seconds, restart_replay_request);
return true;
}
diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c
index fc0ccf0..3f1331f 100644
--- a/src/recorder/recorder.c
+++ b/src/recorder/recorder.c
@@ -101,6 +101,7 @@ struct gsr_recorder {
atomic_int replay_recording_request;
atomic_int replay_recording_state;
atomic_int save_replay_seconds;
+ atomic_int save_replay_restart_replay;
bool should_stop_error;
bool force_iframe_frame;
int audio_max_frame_size;
@@ -418,6 +419,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r
atomic_init(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE);
atomic_init(&self->replay_recording_state, 0);
atomic_init(&self->save_replay_seconds, 0);
+ atomic_init(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION);
self->audio_max_frame_size = 1024;
int error_code = GSR_ERROR_GENERIC;
self->hdr = video_codec_is_hdr(params->settings->video_codec);
@@ -748,11 +750,13 @@ static void recorder_poll_replay_save(gsr_recorder *self) {
current_save_replay_seconds += self->settings.keyint;
atomic_store(&self->save_replay_seconds, 0);
+ const int restart_replay_request = atomic_exchange(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION);
+ const bool restart_replay = restart_replay_request == GSR_RESTART_REPLAY_USE_OPTION ? self->settings.restart_replay_on_save : restart_replay_request == GSR_RESTART_REPLAY_ENABLE;
const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds);
if(!replay_start_result && self->callbacks.replay_saved)
self->callbacks.replay_saved(NULL, self->callbacks.userdata);
- if(self->settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) {
+ if(restart_replay && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) {
pthread_mutex_lock(&self->encoder.replay_mutex);
gsr_replay_buffer_clear(self->encoder.replay_buffer);
pthread_mutex_unlock(&self->encoder.replay_mutex);
@@ -959,6 +963,7 @@ bool gsr_recorder_is_replay_recording(const gsr_recorder *self) {
return atomic_load(&self->replay_recording_state) == 1;
}
-void gsr_recorder_save_replay(gsr_recorder *self, int seconds) {
+void gsr_recorder_save_replay(gsr_recorder *self, int seconds, int restart_replay) {
+ atomic_store(&self->save_replay_restart_replay, restart_replay);
atomic_store(&self->save_replay_seconds, seconds);
}
diff --git a/tools/gsr-cli/main.c b/tools/gsr-cli/main.c
index e430964..fbfc2fa 100644
--- a/tools/gsr-cli/main.c
+++ b/tools/gsr-cli/main.c
@@ -19,7 +19,7 @@
#define GSR_CLI_NO_REPLY_TIMEOUT 0
static void usage(void) {
- printf("usage: gsr-cli -ipc <socket_path> <command> [command_argument]\n");
+ printf("usage: gsr-cli -ipc <socket_path> <command> [command_arguments...]\n");
printf("\n");
printf("Sends a command to a GPU Screen Recorder instance that was started with the -ipc option.\n");
printf("\n");
@@ -46,14 +46,16 @@ static void usage(void) {
printf(" stop-replay-recording\n");
printf(" Stop the regular recording that runs during replay/streaming. Waits until the recording\n");
printf(" has been saved and prints the path of the saved file.\n");
- printf(" save-replay [seconds]\n");
+ printf(" save-replay [seconds] [restart-replay=true|false]\n");
printf(" Save the replay. The number of seconds has to be larger than 0. The whole replay buffer is\n");
printf(" saved when no number of seconds is given. Waits until the replay has been saved and prints\n");
- printf(" the path of the saved file.\n");
+ printf(" the path of the saved file. restart-replay overrides the -restart-replay-on-save option\n");
+ printf(" of GPU Screen Recorder for this save.\n");
printf("\n");
printf("EXAMPLES:\n");
printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n");
printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay 30\n");
+ printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay restart-replay=true\n");
fflush(stdout);
}
@@ -272,7 +274,7 @@ int main(int argc, char **argv) {
return 1;
}
- if(argc > 5) {
+ if(argc > 6) {
gsr_log(GSR_LOG_LEVEL_ERROR, "too many arguments");
usage();
return 1;
@@ -280,23 +282,53 @@ int main(int argc, char **argv) {
const char *socket_filepath = argv[2];
const char *command = argv[3];
- const char *command_argument = argc == 5 ? argv[4] : NULL;
+ const char *command_argument = argc >= 5 ? argv[4] : NULL;
+ const char *command_argument2 = argc >= 6 ? argv[5] : NULL;
char request[GSR_CLI_MAX_REQUEST_SIZE];
if(strcmp(command, "save-replay") == 0) {
- if(command_argument) {
- int64_t seconds = 0;
- if(!string_to_int64(command_argument, &seconds) || seconds <= 0 || seconds > INT_MAX) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "expected the number of seconds to save to be an integer larger than 0, got: '%s'", command_argument);
+ bool has_seconds = false;
+ int64_t seconds = 0;
+ const char *restart_replay = NULL;
+
+ const char *command_arguments[2] = { command_argument, command_argument2 };
+ for(int i = 0; i < 2; ++i) {
+ const char *argument = command_arguments[i];
+ if(!argument)
+ continue;
+
+ if(strncmp(argument, "restart-replay=", 15) == 0) {
+ restart_replay = argument + 15;
+ if(strcmp(restart_replay, "true") != 0 && strcmp(restart_replay, "false") != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "expected restart-replay to be either true or false, got: '%s'", restart_replay);
+ return 1;
+ }
+ } else if(!has_seconds && string_to_int64(argument, &seconds) && seconds > 0 && seconds <= INT_MAX) {
+ has_seconds = true;
+ } else {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "expected the argument to be the number of seconds to save (an integer larger than 0) or restart-replay=true|false, got: '%s'", argument);
return 1;
}
- snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, seconds);
- } else {
- snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID);
}
+
+ if(restart_replay && has_seconds)
+ snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"seconds\":%" PRIi64 ",\"restart-replay\":%s}}\n", GSR_CLI_REQUEST_ID, seconds, restart_replay);
+ else if(restart_replay)
+ snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"restart-replay\":%s}}\n", GSR_CLI_REQUEST_ID, restart_replay);
+ else if(has_seconds)
+ snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"seconds\":%" PRIi64 "}}\n", GSR_CLI_REQUEST_ID, seconds);
+ else
+ snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID);
+
return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT);
}
+ if(command_argument2) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take more than one argument", command);
+ usage();
+ return 1;
+ }
+
if(strcmp(command, "set-paused") == 0) {
if(!command_argument || (strcmp(command_argument, "true") != 0 && strcmp(command_argument, "false") != 0)) {
gsr_log(GSR_LOG_LEVEL_ERROR, "the 'set-paused' command expects either true or false as the argument");