diff options
| -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 } |
