diff options
| author | dec05eba <dec05eba@protonmail.com> | 2026-08-02 01:11:33 +0200 |
|---|---|---|
| committer | dec05eba <dec05eba@protonmail.com> | 2026-08-02 01:11:53 +0200 |
| commit | d72113be395891f4289f0d86a32a37a99a4d5b38 (patch) | |
| tree | c2dd74d24fe62d1020be64980b8a321e999bcc29 | |
| parent | b38d324c8fcb61b699a16c39fc0dcdb31ad9934b (diff) | |
Add -ipc option to control gpu-screen-recorder over a unix domain socket
The socket accepts the same commands as the signal handlers, except that
saving a replay takes an arbitrary number of seconds instead of only the
fixed times that the signals provide. Requests and replies are newline
terminated json objects and every request is replied to, parsed with the
sj.h json library that was added to external.
Requests are handled on a thread that only runs while the recorder
exists, so the recorder control fields are now atomics, which they have
to be now that another thread writes them.
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | external/sj.h | 156 | ||||
| -rw-r--r-- | gpu-screen-recorder.1 | 76 | ||||
| -rw-r--r-- | include/args_parser.h | 2 | ||||
| -rw-r--r-- | include/cli/ipc.h | 56 | ||||
| -rw-r--r-- | include/recorder/audio_capture.h | 6 | ||||
| -rw-r--r-- | include/recorder/replay_save.h | 4 | ||||
| -rw-r--r-- | include/recorder/screenshot.h | 4 | ||||
| -rw-r--r-- | meson.build | 1 | ||||
| -rw-r--r-- | src/args_parser.c | 3 | ||||
| -rw-r--r-- | src/cli/ipc.c | 555 | ||||
| -rw-r--r-- | src/cli/main.c | 81 | ||||
| -rw-r--r-- | src/recorder/audio_capture.c | 8 | ||||
| -rw-r--r-- | src/recorder/recorder.c | 48 | ||||
| -rw-r--r-- | src/recorder/replay_save.c | 8 | ||||
| -rw-r--r-- | src/recorder/screenshot.c | 6 |
16 files changed, 970 insertions, 49 deletions
@@ -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 } |
