aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--external/sj.h156
-rw-r--r--gpu-screen-recorder.176
-rw-r--r--include/args_parser.h2
-rw-r--r--include/cli/ipc.h56
-rw-r--r--include/recorder/audio_capture.h6
-rw-r--r--include/recorder/replay_save.h4
-rw-r--r--include/recorder/screenshot.h4
-rw-r--r--meson.build1
-rw-r--r--src/args_parser.c3
-rw-r--r--src/cli/ipc.c555
-rw-r--r--src/cli/main.c81
-rw-r--r--src/recorder/audio_capture.c8
-rw-r--r--src/recorder/recorder.c48
-rw-r--r--src/recorder/replay_save.c8
-rw-r--r--src/recorder/screenshot.c6
16 files changed, 970 insertions, 49 deletions
diff --git a/README.md b/README.md
index 953e1c1..089076e 100644
--- a/README.md
+++ b/README.md
@@ -138,7 +138,10 @@ This way of recording while using replay/streaming is more efficient than runnin
To save a video in replay mode, you need to send signal SIGUSR1 to gpu screen recorder. You can do this by running `pkill -SIGUSR1 -f "^gpu-screen-recorder"`.\
To stop recording send SIGINT to gpu screen recorder. You can do this by running `pkill -SIGINT -f "^gpu-screen-recorder"` or pressing `Ctrl-C` in the terminal that runs gpu screen recorder. When recording a regular non-replay video this will also save the video.\
To pause/unpause recording send SIGUSR2 to gpu screen recorder. You can do this by running `pkill -SIGUSR2 -f "^gpu-screen-recorder"`. This is only applicable and useful when recording (not streaming nor replay).\
-There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section).
+There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section).\
+GPU Screen Recorder can also be controlled with json messages over a unix domain socket by launching it with the `-ipc` option, for example `-ipc "$XDG_RUNTIME_DIR/gsr.sock"`.
+This gives the same control as the signals, except that a replay can be saved with an arbitrary number of seconds, for example `echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock"`.
+See the `IPC` section in the man page (`man gpu-screen-recorder`) for the full protocol.
## Simple way to run replay without gui
Run the script `scripts/start-replay.sh` to start replay and then `scripts/save-replay.sh` to save a replay and `scripts/stop-replay.sh` to stop the replay. The videos are saved to `$HOME/Videos`.
You can use these scripts to start replay at system startup if you add `scripts/start-replay.sh` to startup (this can be done differently depending on your desktop environment / window manager) and then go into
diff --git a/external/sj.h b/external/sj.h
new file mode 100644
index 0000000..60bea9e
--- /dev/null
+++ b/external/sj.h
@@ -0,0 +1,156 @@
+// sj.h - v0.4 - rxi 2025
+// public domain - no warranty implied, use at your own risk
+
+#ifndef SJ_H
+#define SJ_H
+
+#include <stddef.h>
+#include <stdbool.h>
+
+typedef struct {
+ char *data, *cur, *end;
+ int depth;
+ char *error;
+} sj_Reader;
+
+typedef struct {
+ int type;
+ char *start, *end;
+ int depth;
+} sj_Value;
+
+enum { SJ_ERROR, SJ_END, SJ_ARRAY, SJ_OBJECT, SJ_NUMBER, SJ_STRING, SJ_BOOL, SJ_NULL };
+
+sj_Reader sj_reader(char *data, size_t len);
+sj_Value sj_read(sj_Reader *r);
+bool sj_iter_array(sj_Reader *r, sj_Value arr, sj_Value *val);
+bool sj_iter_object(sj_Reader *r, sj_Value obj, sj_Value *key, sj_Value *val);
+void sj_location(sj_Reader *r, int *line, int *col);
+
+#endif // #ifndef SJ_H
+
+#ifdef SJ_IMPL
+
+
+sj_Reader sj_reader(char *data, size_t len) {
+ return (sj_Reader){ .data = data, .cur = data, .end = data + len };
+}
+
+
+static bool sj__is_number_cont(char c) {
+ return (c >= '0' && c <= '9')
+ || c == 'e' || c == 'E' || c == '.' || c == '-' || c == '+';
+}
+
+static bool sj__is_string(char *cur, char *end, char *expect) {
+ while (*expect) {
+ if (cur == end || *cur != *expect) {
+ return false;
+ }
+ expect++, cur++;
+ }
+ return true;
+}
+
+
+sj_Value sj_read(sj_Reader *r) {
+ sj_Value res;
+top:
+ if (r->error) { return (sj_Value){ .type = SJ_ERROR, .start = r->cur, .end = r->cur }; }
+ if (r->cur == r->end) { r->error = "unexpected eof"; goto top; }
+ res.start = r->cur;
+
+ switch (*r->cur) {
+ case ' ': case '\n': case '\r': case '\t':
+ case ':': case ',':
+ r->cur++;
+ goto top;
+
+ case '-': case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ res.type = SJ_NUMBER;
+ while (r->cur != r->end && sj__is_number_cont(*r->cur)) { r->cur++; }
+ break;
+
+ case '"':
+ res.type = SJ_STRING;
+ res.start = ++r->cur;
+ for (;;) {
+ if ( r->cur == r->end) { r->error = "unclosed string"; goto top; }
+ if (*r->cur == '"') { break; }
+ if (*r->cur == '\\') { r->cur++; }
+ if ( r->cur != r->end) { r->cur++; }
+ }
+ res.end = r->cur++;
+ return res;
+
+ case '{': case '[':
+ res.type = (*r->cur == '{') ? SJ_OBJECT : SJ_ARRAY;
+ res.depth = ++r->depth;
+ r->cur++;
+ break;
+
+ case '}': case ']':
+ res.type = SJ_END;
+ if (--r->depth < 0) {
+ r->error = (*r->cur == '}') ? "stray '}'" : "stray ']'";
+ goto top;
+ }
+ r->cur++;
+ break;
+
+ case 'n': case 't': case 'f':
+ res.type = (*r->cur == 'n') ? SJ_NULL : SJ_BOOL;
+ if (sj__is_string(r->cur, r->end, "null")) { r->cur += 4; break; }
+ if (sj__is_string(r->cur, r->end, "true")) { r->cur += 4; break; }
+ if (sj__is_string(r->cur, r->end, "false")) { r->cur += 5; break; }
+ // fallthrough
+
+ default:
+ r->error = "unknown token";
+ goto top;
+ }
+ res.end = r->cur;
+ return res;
+}
+
+
+static void sj__discard_until(sj_Reader *r, int depth) {
+ sj_Value val;
+ val.type = SJ_NULL;
+ while (r->depth != depth && val.type != SJ_ERROR) {
+ val = sj_read(r);
+ }
+}
+
+
+bool sj_iter_array(sj_Reader *r, sj_Value arr, sj_Value *val) {
+ sj__discard_until(r, arr.depth);
+ *val = sj_read(r);
+ if (val->type == SJ_ERROR || val->type == SJ_END) { return false; }
+ return true;
+}
+
+
+bool sj_iter_object(sj_Reader *r, sj_Value obj, sj_Value *key, sj_Value *val) {
+ sj__discard_until(r, obj.depth);
+ *key = sj_read(r);
+ if (key->type == SJ_ERROR || key->type == SJ_END) { return false; }
+ *val = sj_read(r);
+ if (val->type == SJ_END) { r->error = "unexpected object end"; return false; }
+ if (val->type == SJ_ERROR) { return false; }
+ return true;
+}
+
+
+void sj_location(sj_Reader *r, int *line, int *col) {
+ int ln = 1, cl = 1;
+ for (char *p = r->data; p != r->cur; p++) {
+ if (*p == '\n') { ln++; cl = 0; }
+ cl++;
+ }
+ *line = ln;
+ *col = cl;
+}
+
+#endif // #ifdef SJ_IMPL \ No newline at end of file
diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1
index edb855a..036197b 100644
--- a/gpu-screen-recorder.1
+++ b/gpu-screen-recorder.1
@@ -392,6 +392,14 @@ monotonic_microsec realtime_microsec
<monotonic_microsec> <realtime_microsec>
.fi
(default: no). Ignored for live streaming and when output is piped.
+.TP
+.BI \-ipc " socket_path"
+Listen for commands on a unix domain socket at
+.I socket_path
+instead of only accepting signals. The socket is created when GPU Screen Recorder starts and removed when it exits.
+See the
+.B IPC
+section for the protocol. Has no effect when taking a screenshot.
.SS Output Options
.TP
.BI \-o " output"
@@ -461,6 +469,74 @@ Use
.B pkill
to send signals (e.g.,
.BR "pkill -SIGUSR1 -f ""^gpu-screen-recorder""" ).
+.SH IPC
+When the
+.B \-ipc
+option is used GPU Screen Recorder also listens for commands on a unix domain socket. This gives the same control as signals,
+except that a replay can be saved with an arbitrary number of seconds. The socket can only be used by the user that
+started GPU Screen Recorder and it's removed when GPU Screen Recorder exits.
+.PP
+Requests and replies are json objects terminated by a newline. Every request is replied to.
+A request has these fields:
+.TP
+.B id
+Number that identifies the request. The reply to the request has the same id.
+.TP
+.B name
+String with the name of the request.
+.TP
+.B data
+Optional, the type depends on the request.
+.PP
+A reply has these fields:
+.TP
+.B id
+The id of the request that this is a reply to, or 0 when the request had no valid id.
+.TP
+.B result
+Either
+.B ok
+or
+.BR error .
+.TP
+.B data
+Optional. For an
+.B error
+result this is a string that describes what went wrong.
+.PP
+These requests are available:
+.TP
+.B stop
+Stop and save recording (stop without save in replay mode).
+.TP
+.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
+.B data
+is omitted or null.
+.TP
+.B toggle-pause
+Pause/unpause recording (not for streaming/replay).
+.TP
+.B toggle-replay-recording
+Start/stop regular recording during replay/streaming, which requires the
+.B \-ro
+option.
+.PP
+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"
+.RE
+.fi
+which replies with:
+.nf
+.RS
+{"id":1,"result":"ok"}
+.RE
+.fi
.SH EXAMPLES
.B Record monitor at 60 FPS with desktop audio:
.nf
diff --git a/include/args_parser.h b/include/args_parser.h
index 813d4c6..2954dde 100644
--- a/include/args_parser.h
+++ b/include/args_parser.h
@@ -9,7 +9,7 @@
typedef struct gsr_egl gsr_egl;
-#define NUM_ARGS 38
+#define NUM_ARGS 39
typedef enum {
GSR_CAPTURE_SOURCE_TYPE_WINDOW,
diff --git a/include/cli/ipc.h b/include/cli/ipc.h
new file mode 100644
index 0000000..0862c71
--- /dev/null
+++ b/include/cli/ipc.h
@@ -0,0 +1,56 @@
+#ifndef GSR_CLI_IPC_H
+#define GSR_CLI_IPC_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <limits.h>
+#include <pthread.h>
+
+#define GSR_IPC_MAX_CLIENTS 8
+#define GSR_IPC_MAX_REQUEST_SIZE 4096
+#define GSR_IPC_MAX_ERROR_MESSAGE_SIZE 256
+
+/*
+ These are called from the ipc thread while the recording is running.
+ Return false and write a message to |error_message| to reply to the request with an error.
+*/
+typedef struct {
+ bool (*stop)(char *error_message, size_t error_message_size, void *userdata);
+ bool (*toggle_pause)(char *error_message, size_t error_message_size, void *userdata);
+ bool (*toggle_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);
+ void *userdata;
+} gsr_ipc_handlers;
+
+typedef struct {
+ int fd;
+ char request[GSR_IPC_MAX_REQUEST_SIZE];
+ size_t request_size;
+ bool request_too_large;
+} gsr_ipc_client;
+
+/* Receives newline terminated json requests on a unix domain socket and replies to every request */
+typedef struct {
+ bool initialized;
+ int socket_fd;
+ bool socket_bound;
+ char socket_filepath[PATH_MAX];
+ int wakeup_pipe[2];
+ gsr_ipc_client clients[GSR_IPC_MAX_CLIENTS];
+ int num_clients;
+ gsr_ipc_handlers handlers;
+ pthread_t thread;
+ bool thread_running;
+} gsr_ipc;
+
+/* Returns a |gsr_error| value. Creates the socket, requests are not handled until gsr_ipc_start is called */
+int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath);
+void gsr_ipc_deinit(gsr_ipc *self);
+
+/* Returns a |gsr_error| value. Starts handling requests. Does nothing if |self| hasn't been initialized */
+int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers);
+/* Stops handling requests. Does nothing if the ipc hasn't been started */
+void gsr_ipc_stop(gsr_ipc *self);
+
+#endif /* GSR_CLI_IPC_H */
diff --git a/include/recorder/audio_capture.h b/include/recorder/audio_capture.h
index 68390aa..4f757c9 100644
--- a/include/recorder/audio_capture.h
+++ b/include/recorder/audio_capture.h
@@ -3,7 +3,7 @@
#include <stdbool.h>
#include <stddef.h>
-#include <signal.h>
+#include <stdatomic.h>
#include <pthread.h>
#include "../sound.h"
#include "recording_clock.h"
@@ -64,11 +64,11 @@ struct gsr_audio_capture {
gsr_encoder *encoder;
gsr_recording_clock *clock;
- const volatile sig_atomic_t *running;
+ const atomic_int *running;
};
/* Returns a |gsr_error| value */
-int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const volatile sig_atomic_t *running);
+int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const atomic_int *running);
void gsr_audio_capture_deinit(gsr_audio_capture *self);
bool gsr_audio_capture_add_track(gsr_audio_capture *self, const gsr_audio_track *track);
diff --git a/include/recorder/replay_save.h b/include/recorder/replay_save.h
index d92a8c0..7f48851 100644
--- a/include/recorder/replay_save.h
+++ b/include/recorder/replay_save.h
@@ -5,7 +5,7 @@
#include <stddef.h>
#include <limits.h>
#include <pthread.h>
-#include <signal.h>
+#include <stdatomic.h>
#include "muxer.h"
#include "audio_capture.h"
#include "capture_setup.h"
@@ -25,7 +25,7 @@ typedef struct {
typedef struct {
pthread_t thread;
bool thread_created;
- volatile sig_atomic_t finished;
+ atomic_int finished;
bool success;
char output_filepath[PATH_MAX];
diff --git a/include/recorder/screenshot.h b/include/recorder/screenshot.h
index 057383c..00a7889 100644
--- a/include/recorder/screenshot.h
+++ b/include/recorder/screenshot.h
@@ -2,7 +2,7 @@
#define GSR_RECORDER_SCREENSHOT_H
#include <stdbool.h>
-#include <signal.h>
+#include <stdatomic.h>
#include "../egl.h"
#include "../image_writer.h"
#include "../plugins.h"
@@ -19,7 +19,7 @@ typedef struct {
gsr_image_format image_format;
const char **plugin_filepaths;
int num_plugin_filepaths;
- const volatile sig_atomic_t *running;
+ const atomic_int *running;
void (*screenshot_saved)(const char *filepath, void *userdata);
void *userdata;
} gsr_screenshot_params;
diff --git a/meson.build b/meson.build
index 0653470..bddfc02 100644
--- a/meson.build
+++ b/meson.build
@@ -46,6 +46,7 @@ src = [
'src/recorder/screenshot.c',
'src/recorder/recorder.c',
'src/cli/commands.c',
+ 'src/cli/ipc.c',
'src/egl.c',
'src/cuda.c',
'src/window_texture.c',
diff --git a/src/args_parser.c b/src/args_parser.c
index 0adb5ef..d10da67 100644
--- a/src/args_parser.c
+++ b/src/args_parser.c
@@ -206,7 +206,7 @@ static void usage_header(void) {
"[-cursor yes|no] [-keyint <value>] [-restore-portal-session yes|no] [-portal-session-token-filepath filepath] [-encoder gpu|cpu] "
"[-fallback-cpu-encoding yes|no] [-o <output_file>] [-ro <output_directory>] [-ffmpeg-opts <options>] [--list-capture-options [card_path]] "
"[--list-monitors] [--list-audio-devices] [--list-application-audio] [--list-v4l2-devices] [-write-first-frame-ts yes|no] [-low-power yes|no] "
- "[-v yes|no] [-gl-debug yes|no] [-exclude-metadata yes|no] [--version] [-h|--help]\n", program_name);
+ "[-ipc <socket_path>] [-v yes|no] [-gl-debug yes|no] [-exclude-metadata yes|no] [--version] [-h|--help]\n", program_name);
fflush(stdout);
}
@@ -571,6 +571,7 @@ args_parse_result args_parser_parse(args_parser *self, int argc, char **argv, co
self->args[arg_index++] = (Arg){ .key = "-write-first-frame-ts", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN };
self->args[arg_index++] = (Arg){ .key = "-low-power", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN };
self->args[arg_index++] = (Arg){ .key = "-exclude-metadata", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN };
+ self->args[arg_index++] = (Arg){ .key = "-ipc", .optional = true, .list = false, .type = ARG_TYPE_STRING };
assert(arg_index == NUM_ARGS);
for(int i = 1; i < argc; i += 2) {
diff --git a/src/cli/ipc.c b/src/cli/ipc.c
new file mode 100644
index 0000000..6d555c0
--- /dev/null
+++ b/src/cli/ipc.c
@@ -0,0 +1,555 @@
+#include "../../include/cli/ipc.h"
+#include "../../include/recorder/error.h"
+#include "../../include/recorder/replay_save.h"
+#include "../../include/log.h"
+
+#define SJ_IMPL
+#include "../../external/sj.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+
+#define GSR_IPC_MAX_REQUEST_NAME_SIZE 64
+#define GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE (GSR_IPC_MAX_ERROR_MESSAGE_SIZE*6)
+#define GSR_IPC_MAX_REPLY_SIZE (GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE + 128)
+#define GSR_IPC_SEND_TIMEOUT_MILLISECONDS 1000
+#define GSR_IPC_SOCKET_MODE 0600
+
+typedef struct {
+ int64_t id;
+ char name[GSR_IPC_MAX_REQUEST_NAME_SIZE];
+ sj_Value data;
+ bool has_data;
+} gsr_ipc_request;
+
+static void json_escape_string(char *buffer, size_t buffer_size, const char *str) {
+ char escape_buffer[8];
+ size_t offset = 0;
+ buffer[0] = '\0';
+
+ for(size_t i = 0; str[i] != '\0'; ++i) {
+ const unsigned char c = str[i];
+ const char *escaped = escape_buffer;
+ switch(c) {
+ case '"': escaped = "\\\""; break;
+ case '\\': escaped = "\\\\"; break;
+ case '\n': escaped = "\\n"; break;
+ case '\r': escaped = "\\r"; break;
+ case '\t': escaped = "\\t"; break;
+ default: {
+ if(c < 0x20)
+ snprintf(escape_buffer, sizeof(escape_buffer), "\\u%04x", c);
+ else
+ snprintf(escape_buffer, sizeof(escape_buffer), "%c", c);
+ break;
+ }
+ }
+
+ const size_t escaped_size = strlen(escaped);
+ if(offset + escaped_size >= buffer_size)
+ break;
+
+ memcpy(buffer + offset, escaped, escaped_size);
+ offset += escaped_size;
+ buffer[offset] = '\0';
+ }
+}
+
+static bool json_string_equals(const sj_Value *value, const char *str) {
+ const size_t value_size = value->end - value->start;
+ return strlen(str) == value_size && memcmp(value->start, str, value_size) == 0;
+}
+
+static bool json_number_to_int64(const sj_Value *value, int64_t *result) {
+ char buffer[32];
+ const size_t value_size = value->end - value->start;
+ if(value_size == 0 || value_size >= sizeof(buffer))
+ return false;
+
+ memcpy(buffer, value->start, value_size);
+ buffer[value_size] = '\0';
+
+ char *number_end = NULL;
+ errno = 0;
+ const long long parsed_value = strtoll(buffer, &number_end, 10);
+ if(errno != 0 || number_end != buffer + value_size)
+ return false;
+
+ *result = parsed_value;
+ return true;
+}
+
+static bool string_is_only_whitespace(const char *str, size_t size) {
+ for(size_t i = 0; i < size; ++i) {
+ if(str[i] != ' ' && str[i] != '\t' && str[i] != '\r' && str[i] != '\n')
+ return false;
+ }
+ return true;
+}
+
+static bool ipc_send_all(int fd, const char *data, size_t size) {
+ size_t offset = 0;
+ while(offset < size) {
+ const ssize_t bytes_written = send(fd, data + offset, size - offset, MSG_NOSIGNAL);
+ if(bytes_written > 0) {
+ offset += bytes_written;
+ continue;
+ }
+
+ if(bytes_written == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
+ struct pollfd poll_fd;
+ poll_fd.fd = fd;
+ poll_fd.events = POLLOUT;
+ poll_fd.revents = 0;
+
+ const int poll_result = poll(&poll_fd, 1, GSR_IPC_SEND_TIMEOUT_MILLISECONDS);
+ if(poll_result == -1 && errno == EINTR)
+ continue;
+
+ if(poll_result <= 0)
+ return false;
+
+ continue;
+ }
+
+ return false;
+ }
+ return true;
+}
+
+static bool ipc_client_send_reply(gsr_ipc_client *client, int64_t id, bool success, const char *error_message) {
+ char reply[GSR_IPC_MAX_REPLY_SIZE];
+ int reply_size = 0;
+
+ if(success) {
+ reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\"}\n", id);
+ } else {
+ char escaped_error_message[GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE];
+ json_escape_string(escaped_error_message, sizeof(escaped_error_message), error_message ? error_message : "");
+ reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"error\",\"data\":\"%s\"}\n", id, escaped_error_message);
+ }
+
+ if(reply_size < 0 || reply_size >= (int)sizeof(reply)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a reply to request %" PRIi64, id);
+ return false;
+ }
+
+ return ipc_send_all(client->fd, reply, reply_size);
+}
+
+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));
+
+ sj_Reader reader = sj_reader(data, size);
+ const sj_Value root = sj_read(&reader);
+ if(root.type != SJ_OBJECT) {
+ snprintf(error_message, error_message_size, "expected the request to be a json object");
+ return false;
+ }
+
+ sj_Value id_value;
+ sj_Value name_value;
+ bool has_id = false;
+ bool has_name = false;
+
+ sj_Value key;
+ sj_Value value;
+ while(sj_iter_object(&reader, root, &key, &value)) {
+ if(json_string_equals(&key, "id")) {
+ id_value = value;
+ has_id = true;
+ } else if(json_string_equals(&key, "name")) {
+ name_value = value;
+ has_name = true;
+ } else if(json_string_equals(&key, "data")) {
+ request->data = value;
+ request->has_data = true;
+ }
+ }
+
+ if(reader.error) {
+ snprintf(error_message, error_message_size, "failed to parse the request: %s", reader.error);
+ return false;
+ }
+
+ if(!has_id) {
+ snprintf(error_message, error_message_size, "the request is missing the 'id' field");
+ return false;
+ }
+
+ if(id_value.type != SJ_NUMBER || !json_number_to_int64(&id_value, &request->id)) {
+ snprintf(error_message, error_message_size, "expected 'id' to be an integer");
+ return false;
+ }
+
+ if(!has_name) {
+ snprintf(error_message, error_message_size, "the request is missing the 'name' field");
+ return false;
+ }
+
+ if(name_value.type != SJ_STRING) {
+ snprintf(error_message, error_message_size, "expected 'name' to be a string");
+ return false;
+ }
+
+ snprintf(request->name, sizeof(request->name), "%.*s", (int)(name_value.end - name_value.start), name_value.start);
+ 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) {
+ *seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
+ if(!request->has_data || request->data.type == SJ_NULL)
+ return true;
+
+ int64_t data_seconds = 0;
+ if(request->data.type != SJ_NUMBER || !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");
+ return false;
+ }
+
+ *seconds = data_seconds;
+ return true;
+}
+
+static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, char *error_message, size_t error_message_size) {
+ if(strcmp(request->name, "stop") == 0)
+ return self->handlers.stop(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "toggle-pause") == 0)
+ return self->handlers.toggle_pause(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "toggle-replay-recording") == 0)
+ return self->handlers.toggle_replay_recording(error_message, error_message_size, self->handlers.userdata);
+
+ 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))
+ return false;
+
+ return self->handlers.save_replay(seconds, error_message, error_message_size, self->handlers.userdata);
+ }
+
+ snprintf(error_message, error_message_size, "unknown request name '%s'", request->name);
+ return false;
+}
+
+static bool ipc_client_on_request(gsr_ipc *self, gsr_ipc_client *client) {
+ if(client->request_too_large)
+ return ipc_client_send_reply(client, 0, false, "the request is too large");
+
+ if(string_is_only_whitespace(client->request, client->request_size))
+ return ipc_client_send_reply(client, 0, false, "the request is empty");
+
+ char error_message[GSR_IPC_MAX_ERROR_MESSAGE_SIZE];
+ error_message[0] = '\0';
+
+ gsr_ipc_request request;
+ if(!ipc_request_parse(client->request, client->request_size, &request, error_message, sizeof(error_message)))
+ return ipc_client_send_reply(client, request.id, false, error_message);
+
+ if(!ipc_handle_request(self, &request, error_message, sizeof(error_message)))
+ return ipc_client_send_reply(client, request.id, false, error_message);
+
+ return ipc_client_send_reply(client, request.id, true, NULL);
+}
+
+static bool ipc_client_on_byte(gsr_ipc *self, gsr_ipc_client *client, char c) {
+ if(c != '\n') {
+ if(client->request_size < GSR_IPC_MAX_REQUEST_SIZE)
+ client->request[client->request_size++] = c;
+ else
+ client->request_too_large = true;
+ return true;
+ }
+
+ const bool keep_client = ipc_client_on_request(self, client);
+ client->request_size = 0;
+ client->request_too_large = false;
+ return keep_client;
+}
+
+static bool ipc_client_receive(gsr_ipc *self, gsr_ipc_client *client) {
+ for(;;) {
+ char buffer[1024];
+ const ssize_t bytes_read = recv(client->fd, buffer, sizeof(buffer), 0);
+ if(bytes_read == 0)
+ return false;
+
+ if(bytes_read == -1) {
+ if(errno == EINTR)
+ continue;
+
+ return errno == EAGAIN || errno == EWOULDBLOCK;
+ }
+
+ for(ssize_t i = 0; i < bytes_read; ++i) {
+ if(!ipc_client_on_byte(self, client, buffer[i]))
+ return false;
+ }
+ }
+}
+
+static bool fd_set_cloexec(int fd) {
+ const int flags = fcntl(fd, F_GETFD);
+ return flags != -1 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1;
+}
+
+static bool fd_set_nonblocking(int fd) {
+ const int flags = fcntl(fd, F_GETFL);
+ return flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1;
+}
+
+static void ipc_add_client(gsr_ipc *self, int client_fd) {
+ if(self->num_clients == GSR_IPC_MAX_CLIENTS) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: too many ipc clients are connected, rejecting the new connection");
+ close(client_fd);
+ return;
+ }
+
+ if(!fd_set_cloexec(client_fd) || !fd_set_nonblocking(client_fd)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to setup the ipc client socket, error: %s", strerror(errno));
+ close(client_fd);
+ return;
+ }
+
+ gsr_ipc_client *client = &self->clients[self->num_clients];
+ client->fd = client_fd;
+ client->request_size = 0;
+ client->request_too_large = false;
+ ++self->num_clients;
+}
+
+static void ipc_accept_client(gsr_ipc *self) {
+ const int client_fd = accept(self->socket_fd, NULL, NULL);
+ if(client_fd == -1)
+ return;
+
+ ipc_add_client(self, client_fd);
+}
+
+static void ipc_remove_client(gsr_ipc *self, int index) {
+ close(self->clients[index].fd);
+ for(int i = index; i < self->num_clients - 1; ++i) {
+ self->clients[i] = self->clients[i + 1];
+ }
+ --self->num_clients;
+}
+
+static void* ipc_thread(void *userdata) {
+ gsr_ipc *self = userdata;
+ struct pollfd poll_fds[2 + GSR_IPC_MAX_CLIENTS];
+
+ for(;;) {
+ poll_fds[0].fd = self->wakeup_pipe[0];
+ poll_fds[0].events = POLLIN;
+ poll_fds[0].revents = 0;
+ poll_fds[1].fd = self->socket_fd;
+ poll_fds[1].events = POLLIN;
+ poll_fds[1].revents = 0;
+
+ const int num_polled_clients = self->num_clients;
+ for(int i = 0; i < num_polled_clients; ++i) {
+ poll_fds[2 + i].fd = self->clients[i].fd;
+ poll_fds[2 + i].events = POLLIN;
+ poll_fds[2 + i].revents = 0;
+ }
+
+ if(poll(poll_fds, 2 + num_polled_clients, -1) == -1) {
+ if(errno == EINTR)
+ continue;
+
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to poll the ipc sockets, error: %s", strerror(errno));
+ break;
+ }
+
+ if(poll_fds[0].revents != 0)
+ break;
+
+ if(poll_fds[1].revents & POLLIN)
+ ipc_accept_client(self);
+
+ for(int i = num_polled_clients - 1; i >= 0; --i) {
+ bool keep_client = true;
+ if(poll_fds[2 + i].revents & POLLIN)
+ keep_client = ipc_client_receive(self, &self->clients[i]);
+ else if(poll_fds[2 + i].revents & (POLLHUP | POLLERR | POLLNVAL))
+ keep_client = false;
+
+ if(!keep_client)
+ ipc_remove_client(self, i);
+ }
+ }
+
+ return NULL;
+}
+
+static bool ipc_socket_filepath_in_use(const char *socket_filepath) {
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath);
+
+ const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if(fd == -1)
+ return true;
+
+ const bool in_use = connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == 0;
+ close(fd);
+ return in_use;
+}
+
+static bool ipc_bind(gsr_ipc *self, const struct sockaddr_un *addr) {
+ const mode_t prev_mask = umask(0777 & ~GSR_IPC_SOCKET_MODE);
+ int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr));
+ if(bind_result == -1 && errno == EADDRINUSE && !ipc_socket_filepath_in_use(self->socket_filepath)) {
+ unlink(self->socket_filepath);
+ bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr));
+ }
+ const int bind_error = errno;
+ umask(prev_mask);
+
+ if(bind_result == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to bind the ipc socket to \"%s\", error: %s", self->socket_filepath, strerror(bind_error));
+ return false;
+ }
+
+ self->socket_bound = true;
+ return true;
+}
+
+static void ipc_close(gsr_ipc *self) {
+ for(int i = 0; i < self->num_clients; ++i) {
+ close(self->clients[i].fd);
+ }
+ self->num_clients = 0;
+
+ for(int i = 0; i < 2; ++i) {
+ if(self->wakeup_pipe[i] != -1) {
+ close(self->wakeup_pipe[i]);
+ self->wakeup_pipe[i] = -1;
+ }
+ }
+
+ if(self->socket_fd != -1) {
+ close(self->socket_fd);
+ self->socket_fd = -1;
+ }
+
+ if(self->socket_bound) {
+ unlink(self->socket_filepath);
+ self->socket_bound = false;
+ }
+}
+
+int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) {
+ memset(self, 0, sizeof(*self));
+ self->socket_fd = -1;
+ self->wakeup_pipe[0] = -1;
+ self->wakeup_pipe[1] = -1;
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ if(snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath) >= (int)sizeof(addr.sun_path)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath);
+ goto err;
+ }
+
+ snprintf(self->socket_filepath, sizeof(self->socket_filepath), "%s", socket_filepath);
+
+ if(pipe(self->wakeup_pipe) == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc wakeup pipe, error: %s", strerror(errno));
+ self->wakeup_pipe[0] = -1;
+ self->wakeup_pipe[1] = -1;
+ goto err;
+ }
+
+ if(!fd_set_cloexec(self->wakeup_pipe[0]) || !fd_set_cloexec(self->wakeup_pipe[1])) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to setup the ipc wakeup pipe, error: %s", strerror(errno));
+ goto err;
+ }
+
+ self->socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ if(self->socket_fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc socket, error: %s", strerror(errno));
+ goto err;
+ }
+
+ if(!ipc_bind(self, &addr))
+ goto err;
+
+ if(listen(self->socket_fd, GSR_IPC_MAX_CLIENTS) == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to listen on the ipc socket, error: %s", strerror(errno));
+ goto err;
+ }
+
+ self->initialized = true;
+ return GSR_ERROR_OK;
+
+ err:
+ ipc_close(self);
+ return GSR_ERROR_GENERIC;
+}
+
+void gsr_ipc_deinit(gsr_ipc *self) {
+ if(!self->initialized)
+ return;
+
+ gsr_ipc_stop(self);
+ ipc_close(self);
+ self->initialized = false;
+}
+
+int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers) {
+ if(!self->initialized)
+ return GSR_ERROR_OK;
+
+ self->handlers = *handlers;
+
+ /* Block all signals in the ipc thread to keep the signal handlers running on the main thread */
+ sigset_t all_signals;
+ sigset_t prev_signals;
+ sigfillset(&all_signals);
+ pthread_sigmask(SIG_SETMASK, &all_signals, &prev_signals);
+ const int thread_create_result = pthread_create(&self->thread, NULL, ipc_thread, self);
+ pthread_sigmask(SIG_SETMASK, &prev_signals, NULL);
+
+ if(thread_create_result != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_start: failed to create the ipc thread, error: %s", strerror(thread_create_result));
+ return GSR_ERROR_GENERIC;
+ }
+
+ self->thread_running = true;
+ return GSR_ERROR_OK;
+}
+
+void gsr_ipc_stop(gsr_ipc *self) {
+ if(!self->thread_running)
+ return;
+
+ const char wakeup_value = 1;
+ ssize_t bytes_written = 0;
+ do {
+ bytes_written = write(self->wakeup_pipe[1], &wakeup_value, 1);
+ } while(bytes_written == -1 && errno == EINTR);
+
+ if(bytes_written == -1)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_stop: failed to wake up the ipc thread, error: %s", strerror(errno));
+
+ pthread_join(self->thread, NULL);
+ self->thread_running = false;
+}
diff --git a/src/cli/main.c b/src/cli/main.c
index 9d5f7e2..5ead4a4 100644
--- a/src/cli/main.c
+++ b/src/cli/main.c
@@ -16,6 +16,7 @@
*/
#include "../../include/cli/commands.h"
+#include "../../include/cli/ipc.h"
#include "../../include/recorder/recorder.h"
#include "../../include/recorder/screenshot.h"
#include "../../include/recorder/capture_source.h"
@@ -39,10 +40,11 @@
#include <assert.h>
#include <locale.h>
#include <signal.h>
+#include <stdatomic.h>
#include <unistd.h>
#include <malloc.h>
-static volatile sig_atomic_t running = 1;
+static atomic_int running = 1;
static gsr_recorder *recorder = NULL;
/* Signals that are received before the recorder has been created are applied when it has been created */
static volatile sig_atomic_t pending_toggle_pause = 0;
@@ -51,7 +53,7 @@ static volatile sig_atomic_t pending_save_replay_seconds = 0;
static void stop_handler(int signal_value) {
(void)signal_value;
- running = 0;
+ atomic_store(&running, 0);
if(recorder)
gsr_recorder_stop(recorder);
}
@@ -132,6 +134,48 @@ static void save_replay_30_minutes_handler(int signal_value) {
save_replay_seconds_handler(recorder, 60*30);
}
+static bool ipc_stop_handler(char *error_message, size_t error_message_size, void *userdata) {
+ (void)error_message;
+ (void)error_message_size;
+ (void)userdata;
+ atomic_store(&running, 0);
+ gsr_recorder_stop(recorder);
+ return true;
+}
+
+static bool ipc_toggle_pause_handler(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, "pausing is not supported when recording a replay");
+ return false;
+ }
+
+ gsr_recorder_toggle_pause(recorder);
+ return true;
+}
+
+static bool ipc_toggle_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->replay_recording_directory) {
+ snprintf(error_message, error_message_size, "option -ro is required to start a recording");
+ return false;
+ }
+
+ gsr_recorder_toggle_replay_recording(recorder);
+ return true;
+}
+
+static bool ipc_save_replay_handler(int seconds, 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);
+ return true;
+}
+
static void install_signal_handlers(void) {
signal(SIGINT, stop_handler);
signal(SIGTERM, stop_handler);
@@ -366,7 +410,7 @@ static int take_screenshot(args_parser *arg_parser, gsr_windowing *windowing, gs
return gsr_screenshot_take(&screenshot_params);
}
-static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_audio_input_tracks *audio_input_tracks) {
+static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_audio_input_tracks *audio_input_tracks, gsr_ipc *ipc) {
const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p");
assert(plugin_arg);
@@ -396,10 +440,21 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture
return error;
apply_pending_signals(recorder);
- if(!running)
+ if(!atomic_load(&running))
gsr_recorder_stop(recorder);
- const int run_result = gsr_recorder_run(recorder);
+ gsr_ipc_handlers ipc_handlers;
+ ipc_handlers.stop = ipc_stop_handler;
+ ipc_handlers.toggle_pause = ipc_toggle_pause_handler;
+ ipc_handlers.toggle_replay_recording = ipc_toggle_replay_recording_handler;
+ ipc_handlers.save_replay = ipc_save_replay_handler;
+ ipc_handlers.userdata = &arg_parser->settings;
+
+ int run_result = gsr_ipc_start(ipc, &ipc_handlers);
+ if(run_result == GSR_ERROR_OK)
+ run_result = gsr_recorder_run(recorder);
+
+ gsr_ipc_stop(ipc);
gsr_recorder_destroy(recorder);
recorder = NULL;
return run_result;
@@ -413,11 +468,16 @@ static int run(args_parser *arg_parser) {
gsr_app_audio_names app_audio_names;
gsr_windowing windowing;
gsr_capture_deps capture_deps;
+ gsr_ipc ipc;
memset(&audio_input_tracks, 0, sizeof(audio_input_tracks));
memset(&app_audio_names, 0, sizeof(app_audio_names));
memset(&windowing, 0, sizeof(windowing));
+ memset(&ipc, 0, sizeof(ipc));
gsr_capture_deps_init(&capture_deps);
+ const Arg *ipc_arg = args_parser_get_arg(arg_parser, "-ipc");
+ assert(ipc_arg);
+
const int parse_capture_sources_result = gsr_capture_sources_parse(&capture_sources, arg_parser->settings.capture_source, arg_parser->settings.region_position, arg_parser->settings.region_size);
if(parse_capture_sources_result != GSR_ERROR_OK) {
exit_code = gsr_error_to_exit_code(parse_capture_sources_result);
@@ -437,6 +497,11 @@ static int run(args_parser *arg_parser) {
goto done;
}
+ if(ipc_arg->num_values > 0 && gsr_ipc_init(&ipc, ipc_arg->values[0]) != GSR_ERROR_OK) {
+ exit_code = 1;
+ goto done;
+ }
+
const int parse_audio_inputs_result = parse_audio_inputs(arg_parser, &audio_input_tracks);
if(parse_audio_inputs_result != GSR_ERROR_OK) {
exit_code = gsr_error_to_exit_code(parse_audio_inputs_result);
@@ -518,12 +583,16 @@ static int run(args_parser *arg_parser) {
goto done;
}
+ if(ipc_arg->num_values > 0)
+ gsr_log(GSR_LOG_LEVEL_WARNING, "option -ipc has no effect when taking a screenshot");
+
exit_code = gsr_error_to_exit_code(take_screenshot(arg_parser, &windowing, &capture_deps, &capture_sources, image_format));
} else {
- exit_code = gsr_error_to_exit_code(record(arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks));
+ exit_code = gsr_error_to_exit_code(record(arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks, &ipc));
}
done:
+ gsr_ipc_deinit(&ipc);
gsr_capture_deps_deinit(&capture_deps);
gsr_windowing_deinit(&windowing);
#ifdef GSR_APP_AUDIO
diff --git a/src/recorder/audio_capture.c b/src/recorder/audio_capture.c
index ee1c75f..6275c93 100644
--- a/src/recorder/audio_capture.c
+++ b/src/recorder/audio_capture.c
@@ -155,7 +155,7 @@ static void* audio_device_thread(void *userdata) {
gsr_audio_track *track = thread_userdata->track;
gsr_audio_device_capture *device = thread_userdata->device;
gsr_recording_clock *clock = self->clock;
- const volatile sig_atomic_t *running = self->running;
+ const atomic_int *running = self->running;
const enum AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(track->codec_context));
/* TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged */
@@ -194,7 +194,7 @@ static void* audio_device_thread(void *userdata) {
if(device->sound_device.handle)
sound_device_flush(&device->sound_device);
- while(*running) {
+ while(atomic_load(running)) {
void *sound_buffer;
int sound_buffer_size = -1;
const double time_before_read_seconds = clock_get_monotonic_seconds();
@@ -332,7 +332,7 @@ static void* audio_device_thread(void *userdata) {
static void* amix_thread(void *userdata) {
gsr_audio_capture *self = userdata;
AVFrame *aframe = av_frame_alloc();
- while(*self->running) {
+ while(atomic_load(self->running)) {
pthread_mutex_lock(&self->filter_mutex);
for(size_t i = 0; i < self->num_tracks; ++i) {
gsr_audio_track *track = &self->tracks[i];
@@ -360,7 +360,7 @@ static void* amix_thread(void *userdata) {
return NULL;
}
-int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const volatile sig_atomic_t *running) {
+int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const atomic_int *running) {
memset(self, 0, sizeof(*self));
self->encoder = encoder;
self->clock = clock;
diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c
index 7c45594..6f9ea12 100644
--- a/src/recorder/recorder.c
+++ b/src/recorder/recorder.c
@@ -26,6 +26,7 @@
#include <assert.h>
#include <limits.h>
#include <unistd.h>
+#include <stdatomic.h>
#include <libavutil/time.h>
#include <libavformat/avformat.h>
@@ -85,10 +86,10 @@ struct gsr_recorder {
int64_t video_prev_pts;
bool hdr_metadata_set;
- volatile sig_atomic_t running;
- volatile sig_atomic_t toggle_pause;
- volatile sig_atomic_t toggle_replay_recording;
- volatile sig_atomic_t save_replay_seconds;
+ atomic_int running;
+ atomic_int toggle_pause;
+ atomic_int toggle_replay_recording;
+ atomic_int save_replay_seconds;
bool should_stop_error;
bool force_iframe_frame;
int audio_max_frame_size;
@@ -400,7 +401,10 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r
self->capture_deps = params->capture_deps;
self->capture_sources = params->capture_sources;
self->audio_input_tracks = params->audio_input_tracks;
- self->running = 1;
+ atomic_init(&self->running, 1);
+ atomic_init(&self->toggle_pause, 0);
+ atomic_init(&self->toggle_replay_recording, 0);
+ atomic_init(&self->save_replay_seconds, 0);
self->audio_max_frame_size = 1024;
int error_code = GSR_ERROR_GENERIC;
self->hdr = video_codec_is_hdr(params->settings->video_codec);
@@ -465,7 +469,7 @@ static bool recorder_tick_video_sources(gsr_recorder *self) {
gsr_capture_tick(video_source->capture);
if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) {
- self->running = 0;
+ atomic_store(&self->running, 0);
break;
}
@@ -621,23 +625,23 @@ static void recorder_capture_and_encode_frame(gsr_recorder *self, bool damaged)
}
static void recorder_apply_pause_toggle(gsr_recorder *self) {
- if(self->toggle_pause == 1 && !self->settings.is_replaying) {
+ if(atomic_load(&self->toggle_pause) == 1 && !self->settings.is_replaying) {
self->paused = !self->paused;
gsr_recording_clock_set_paused(self->recording_clock, self->paused);
gsr_log(GSR_LOG_LEVEL_INFO, self->paused ? "Paused" : "Unpaused");
- self->toggle_pause = 0;
+ atomic_store(&self->toggle_pause, 0);
}
}
static void recorder_apply_replay_recording_toggle(gsr_recorder *self) {
- if(self->toggle_replay_recording && !self->settings.replay_recording_directory) {
- self->toggle_replay_recording = 0;
+ if(atomic_load(&self->toggle_replay_recording) && !self->settings.replay_recording_directory) {
+ atomic_store(&self->toggle_replay_recording, 0);
if(self->callbacks.recording_started)
self->callbacks.recording_started(NULL, self->callbacks.userdata);
}
- if(self->toggle_replay_recording && self->settings.replay_recording_directory) {
- self->toggle_replay_recording = 0;
+ if(atomic_load(&self->toggle_replay_recording) && self->settings.replay_recording_directory) {
+ atomic_store(&self->toggle_replay_recording, 0);
const bool new_replay_recording_state = !self->replay_recording;
if(new_replay_recording_state) {
gsr_audio_capture_lock_filter(&self->audio_capture);
@@ -704,12 +708,12 @@ static void recorder_poll_replay_save(gsr_recorder *self) {
self->callbacks.replay_saved(replay_save_output_filepath[0] == '\0' || !replay_save_result ? NULL : replay_save_output_filepath, self->callbacks.userdata);
}
- if(self->save_replay_seconds != 0 && !gsr_replay_save_is_running(&self->replay_save) && self->settings.is_replaying) {
- int current_save_replay_seconds = self->save_replay_seconds;
+ if(atomic_load(&self->save_replay_seconds) != 0 && !gsr_replay_save_is_running(&self->replay_save) && self->settings.is_replaying) {
+ int current_save_replay_seconds = atomic_load(&self->save_replay_seconds);
if(current_save_replay_seconds > 0)
current_save_replay_seconds += self->settings.keyint;
- self->save_replay_seconds = 0;
+ atomic_store(&self->save_replay_seconds, 0);
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);
@@ -762,7 +766,7 @@ int gsr_recorder_run(gsr_recorder *self) {
if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) {
/* The audio threads that did start have to stop before they can be joined */
- self->running = 0;
+ atomic_store(&self->running, 0);
return GSR_ERROR_GENERIC;
}
@@ -802,7 +806,7 @@ int gsr_recorder_run(gsr_recorder *self) {
}
}
- while(self->running) {
+ while(atomic_load(&self->running)) {
recorder_process_events(self);
const bool damaged = recorder_tick_video_sources(self);
recorder_update_fps_counters(self);
@@ -818,7 +822,7 @@ int gsr_recorder_run(gsr_recorder *self) {
}
static void gsr_recorder_stop_recording(gsr_recorder *self) {
- self->running = 0;
+ atomic_store(&self->running, 0);
bool final_replay_save_result = false;
const char *final_replay_save_output_filepath = NULL;
@@ -894,17 +898,17 @@ void gsr_recorder_destroy(gsr_recorder *self) {
}
void gsr_recorder_stop(gsr_recorder *self) {
- self->running = 0;
+ atomic_store(&self->running, 0);
}
void gsr_recorder_toggle_pause(gsr_recorder *self) {
- self->toggle_pause = 1;
+ atomic_store(&self->toggle_pause, 1);
}
void gsr_recorder_toggle_replay_recording(gsr_recorder *self) {
- self->toggle_replay_recording = 1;
+ atomic_store(&self->toggle_replay_recording, 1);
}
void gsr_recorder_save_replay(gsr_recorder *self, int seconds) {
- self->save_replay_seconds = seconds;
+ atomic_store(&self->save_replay_seconds, seconds);
}
diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c
index 6e5bc7b..1d33437 100644
--- a/src/recorder/replay_save.c
+++ b/src/recorder/replay_save.c
@@ -8,7 +8,7 @@
void gsr_replay_save_init(gsr_replay_save *self) {
memset(self, 0, sizeof(*self));
- self->finished = 0;
+ atomic_init(&self->finished, 0);
}
bool gsr_replay_save_is_running(const gsr_replay_save *self) {
@@ -110,7 +110,7 @@ static void* replay_save_thread(void *userdata) {
self->success = success;
gsr_replay_save_cleanup(self);
- self->finished = 1;
+ atomic_store(&self->finished, 1);
return NULL;
}
@@ -123,7 +123,7 @@ bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_co
self->video_stream_index = video_stream_index;
self->output_filepath[0] = '\0';
self->success = false;
- self->finished = 0;
+ atomic_store(&self->finished, 0);
pthread_mutex_lock(&encoder->replay_mutex);
self->cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer);
@@ -193,7 +193,7 @@ static bool gsr_replay_save_finish(gsr_replay_save *self, bool *success, const c
}
bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath) {
- if(!self->thread_created || !self->finished)
+ if(!self->thread_created || !atomic_load(&self->finished))
return false;
return gsr_replay_save_finish(self, success, output_filepath);
diff --git a/src/recorder/screenshot.c b/src/recorder/screenshot.c
index 47fedef..2e4357d 100644
--- a/src/recorder/screenshot.c
+++ b/src/recorder/screenshot.c
@@ -67,7 +67,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) {
gsr_window *window = params->window;
gsr_capture_deps *capture_deps = params->capture_deps;
gsr_capture_sources *capture_sources = params->capture_sources;
- const volatile sig_atomic_t *running = params->running;
+ const atomic_int *running = params->running;
const int image_quality = video_quality_to_image_quality_value(settings->video_quality);
const gsr_color_range color_range = image_format_to_color_range(params->image_format, image_quality);
@@ -122,7 +122,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) {
bool should_stop_error = false;
egl->glClear(0);
- while(*running) {
+ while(atomic_load(running)) {
while(gsr_window_process_event(window)) {
if(capture_deps->x11_cursor_display && settings->record_cursor)
gsr_cursor_on_event(&capture_deps->x11_cursor, gsr_window_get_event_data(window));
@@ -175,7 +175,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) {
if(all_sources_captured)
break;
- if(*running)
+ if(atomic_load(running))
usleep(30 * 1000); // 30 ms
}