aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--include/recorder/recorder.h54
-rw-r--r--meson.build19
-rw-r--r--src/cli/main.c562
-rw-r--r--src/main.cpp1158
-rw-r--r--src/recorder/recorder.c826
5 files changed, 1452 insertions, 1167 deletions
diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h
new file mode 100644
index 0000000..bbd37e1
--- /dev/null
+++ b/include/recorder/recorder.h
@@ -0,0 +1,54 @@
+#ifndef GSR_RECORDER_RECORDER_H
+#define GSR_RECORDER_RECORDER_H
+
+#include <stdbool.h>
+#include "settings.h"
+#include "capture_source.h"
+#include "capture_setup.h"
+#include "audio_input.h"
+#include "windowing.h"
+#ifdef GSR_APP_AUDIO
+#include "../pipewire_audio.h"
+#endif
+
+/* Records video and audio to a file, or to a replay buffer that can be saved to a file at any time */
+typedef struct gsr_recorder gsr_recorder;
+
+typedef struct {
+ /* |filepath| is NULL when the recording failed to save */
+ void (*replay_saved)(const char *filepath, void *userdata);
+ void (*recording_started)(const char *filepath, void *userdata);
+ /* |filepath| is NULL when the recording failed to save */
+ void (*recording_stopped)(const char *filepath, void *userdata);
+ void (*paused_changed)(bool paused, void *userdata);
+ void *userdata;
+} gsr_recorder_callbacks;
+
+typedef struct {
+ const gsr_recorder_settings *settings;
+ gsr_windowing *windowing;
+ gsr_capture_deps *capture_deps;
+ gsr_capture_sources *capture_sources;
+ gsr_audio_input_tracks *audio_input_tracks;
+ const char **plugin_filepaths;
+ int num_plugin_filepaths;
+#ifdef GSR_APP_AUDIO
+ gsr_pipewire_audio *pipewire_audio;
+#endif
+} gsr_recorder_params;
+
+/* Returns NULL on failure and sets |error| to a |gsr_error| value */
+gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error);
+void gsr_recorder_destroy(gsr_recorder *self);
+
+/* Returns a |gsr_error| value. Records until gsr_recorder_stop is called or until the capture target is gone */
+int gsr_recorder_run(gsr_recorder *self);
+
+/* These are safe to call from a signal handler or from another thread */
+void gsr_recorder_stop(gsr_recorder *self);
+void gsr_recorder_toggle_pause(gsr_recorder *self);
+void gsr_recorder_toggle_replay_recording(gsr_recorder *self);
+/* |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */
+void gsr_recorder_save_replay(gsr_recorder *self, int seconds);
+
+#endif /* GSR_RECORDER_RECORDER_H */
diff --git a/meson.build b/meson.build
index 5bece63..0653470 100644
--- a/meson.build
+++ b/meson.build
@@ -1,10 +1,10 @@
-project('gpu-screen-recorder', ['c', 'cpp'], version : '5.15.3', default_options : ['warning_level=2', 'cpp_std=c++17'])
+project('gpu-screen-recorder', 'c', version : '5.15.3', default_options : ['warning_level=2'])
-add_project_arguments('-Wshadow', language : ['c', 'cpp'])
+add_project_arguments('-Wshadow', language : 'c')
if get_option('buildtype') == 'debug'
- add_project_arguments('-g3', language : ['c', 'cpp'])
+ add_project_arguments('-g3', language : 'c')
elif get_option('buildtype') == 'release'
- add_project_arguments('-DNDEBUG', language : ['c', 'cpp'])
+ add_project_arguments('-DNDEBUG', language : 'c')
endif
src = [
@@ -44,6 +44,7 @@ src = [
'src/recorder/muxer.c',
'src/recorder/replay_save.c',
'src/recorder/screenshot.c',
+ 'src/recorder/recorder.c',
'src/cli/commands.c',
'src/egl.c',
'src/cuda.c',
@@ -61,7 +62,7 @@ src = [
'src/plugins.c',
'src/wayland_host_bridge.c',
'src/sound.c',
- 'src/main.cpp',
+ 'src/cli/main.c',
]
subdir('protocol')
@@ -105,7 +106,7 @@ if get_option('portal') == true
'src/dbus.c',
'src/pipewire_video.c',
]
- add_project_arguments('-DGSR_PORTAL', language : ['c', 'cpp'])
+ add_project_arguments('-DGSR_PORTAL', language : 'c')
uses_pipewire = true
endif
@@ -113,7 +114,7 @@ if get_option('app_audio') == true
src += [
'src/pipewire_audio.c',
]
- add_project_arguments('-DGSR_APP_AUDIO', language : ['c', 'cpp'])
+ add_project_arguments('-DGSR_APP_AUDIO', language : 'c')
uses_pipewire = true
endif
@@ -123,10 +124,10 @@ if uses_pipewire == true
dependency('libspa-0.2'),
dependency('dbus-1'),
]
- add_project_arguments('-DGSR_DBUS', language : ['c', 'cpp'])
+ add_project_arguments('-DGSR_DBUS', language : 'c')
endif
-add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
+add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: 'c')
executable('gsr-kms-server', 'kms/server/kms_server.c', dependencies : dependency('libdrm'), c_args : '-fstack-protector-all', install : true)
executable('gpu-screen-recorder', src, dependencies : dep, install : true)
diff --git a/src/cli/main.c b/src/cli/main.c
new file mode 100644
index 0000000..b74eb1c
--- /dev/null
+++ b/src/cli/main.c
@@ -0,0 +1,562 @@
+/*
+ Copyright (C) 2020 dec05eba
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "../../include/cli/commands.h"
+#include "../../include/recorder/recorder.h"
+#include "../../include/recorder/screenshot.h"
+#include "../../include/recorder/capture_source.h"
+#include "../../include/recorder/capture_setup.h"
+#include "../../include/recorder/windowing.h"
+#include "../../include/recorder/audio_input.h"
+#include "../../include/recorder/replay_save.h"
+#include "../../include/recorder/error.h"
+#include "../../include/args_parser.h"
+#include "../../include/sound.h"
+#include "../../include/shader.h"
+#include "../../include/utils.h"
+#include "../../include/log.h"
+#ifdef GSR_APP_AUDIO
+#include "../../include/pipewire_audio.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <locale.h>
+#include <signal.h>
+#include <unistd.h>
+#include <malloc.h>
+
+static volatile sig_atomic_t 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;
+static volatile sig_atomic_t pending_toggle_replay_recording = 0;
+static volatile sig_atomic_t pending_save_replay_seconds = 0;
+
+static void stop_handler(int signal_value) {
+ (void)signal_value;
+ running = 0;
+ if(recorder)
+ gsr_recorder_stop(recorder);
+}
+
+static void toggle_pause_handler(int signal_value) {
+ (void)signal_value;
+ if(recorder)
+ gsr_recorder_toggle_pause(recorder);
+ else
+ pending_toggle_pause = 1;
+}
+
+static void toggle_replay_recording_handler(int signal_value) {
+ (void)signal_value;
+ if(recorder)
+ gsr_recorder_toggle_replay_recording(recorder);
+ else
+ pending_toggle_replay_recording = 1;
+}
+
+static void save_replay_seconds_handler(gsr_recorder *rec, int seconds) {
+ if(rec)
+ gsr_recorder_save_replay(rec, seconds);
+ else
+ pending_save_replay_seconds = seconds;
+}
+
+static void apply_pending_signals(gsr_recorder *rec) {
+ if(pending_toggle_pause) {
+ pending_toggle_pause = 0;
+ gsr_recorder_toggle_pause(rec);
+ }
+
+ if(pending_toggle_replay_recording) {
+ pending_toggle_replay_recording = 0;
+ gsr_recorder_toggle_replay_recording(rec);
+ }
+
+ if(pending_save_replay_seconds != 0) {
+ const int seconds = pending_save_replay_seconds;
+ pending_save_replay_seconds = 0;
+ gsr_recorder_save_replay(rec, seconds);
+ }
+}
+
+static void save_replay_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, GSR_SAVE_REPLAY_SECONDS_FULL);
+}
+
+static void save_replay_10_seconds_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 10);
+}
+
+static void save_replay_30_seconds_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 30);
+}
+
+static void save_replay_1_minute_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60);
+}
+
+static void save_replay_5_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*5);
+}
+
+static void save_replay_10_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*10);
+}
+
+static void save_replay_30_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*30);
+}
+
+static void install_signal_handlers(void) {
+ signal(SIGINT, stop_handler);
+ signal(SIGTERM, stop_handler);
+ signal(SIGUSR1, save_replay_handler);
+ signal(SIGUSR2, toggle_pause_handler);
+ signal(SIGRTMIN, toggle_replay_recording_handler);
+ signal(SIGRTMIN+1, save_replay_10_seconds_handler);
+ signal(SIGRTMIN+2, save_replay_30_seconds_handler);
+ signal(SIGRTMIN+3, save_replay_1_minute_handler);
+ signal(SIGRTMIN+4, save_replay_5_minutes_handler);
+ signal(SIGRTMIN+5, save_replay_10_minutes_handler);
+ signal(SIGRTMIN+6, save_replay_30_minutes_handler);
+}
+
+static void set_display_server_environment_variables(void) {
+ /* Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) */
+ const char *display = getenv("DISPLAY");
+ if(!display) {
+ display = ":0";
+ setenv("DISPLAY", display, true);
+ }
+
+ const char *wayland_display = getenv("WAYLAND_DISPLAY");
+ if(!wayland_display) {
+ wayland_display = "wayland-0";
+ setenv("WAYLAND_DISPLAY", wayland_display, true);
+ }
+}
+
+static void set_environment_variables(void) {
+ set_display_server_environment_variables();
+
+ /* Linux nvidia driver 580.105.08 added the environment variable CUDA_DISABLE_PERF_BOOST to disable the p2 power level issue,
+ where running cuda (which includes nvenc) causes the gpu to be forcefully set to p2 power level which on many nvidia gpus
+ decreases gpu performance in games. On my GTX 1080 it decreased game performance by 10% for absolutely no reason. */
+ setenv("CUDA_DISABLE_PERF_BOOST", "1", true);
+ /* Stop nvidia driver from buffering frames */
+ setenv("__GL_MaxFramesAllowed", "1", true);
+ /* If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context,
+ so we overwrite it */
+ setenv("__GL_THREADED_OPTIMIZATIONS", "0", true);
+ /* Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since
+ nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet.
+ Let vaapi find the right vaapi driver instead of forcing a specific one. */
+ unsetenv("LIBVA_DRIVER_NAME");
+ /* Some people set this to force all applications to vsync on nvidia, but this makes eglSwapBuffers never return. */
+ unsetenv("__GL_SYNC_TO_VBLANK");
+ /* Same as above, but for amd/intel */
+ unsetenv("vblank_mode");
+}
+
+static void install_cuda_no_stable_perf_limit(void) {
+ if(access("/proc/driver/nvidia/version", F_OK) != 0)
+ return;
+
+ const char *home = getenv("HOME");
+ if(!home) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: $HOME not set");
+ return;
+ }
+
+ char nv_profiles_path[4096];
+ snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d", home);
+
+ if(create_directory_recursive(nv_profiles_path) != 0) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create directory: %s", nv_profiles_path);
+ return;
+ }
+
+ snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d/10-gsr-cuda-no-stable-perf-limit", home);
+
+ FILE *f = fopen(nv_profiles_path, "wb");
+ if(!f) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create file: %s", nv_profiles_path);
+ return;
+ }
+
+ const char *profile_data =
+ "{\n"
+ " \"profiles\": [\n"
+ " {\n"
+ " \"name\": \"CudaNoStablePerfLimit\",\n"
+ " \"settings\": [\"0x166c5e\", 0]\n"
+ " }\n"
+ " ],\n"
+ " \"rules\": [\n"
+ " { \"pattern\": \"gpu-screen-recorder\", \"profile\": \"CudaNoStablePerfLimit\" }\n"
+ " ]\n"
+ "}\n";
+
+ fwrite(profile_data, 1, strlen(profile_data), f);
+ fclose(f);
+}
+
+static int validate_args_with_capture_sources(args_parser *arg_parser, const gsr_capture_sources *capture_sources) {
+ const Arg *output_resolution_arg = args_parser_get_arg(arg_parser, "-s");
+ assert(output_resolution_arg);
+
+ const Arg *region_arg = args_parser_get_arg(arg_parser, "-region");
+ assert(region_arg);
+
+ if(gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -s is required when using '-w focused' option");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+
+ const bool is_capturing_region = gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION);
+ if(region_arg->num_values == 0) {
+ if(is_capturing_region && !gsr_capture_sources_has_region_set(capture_sources)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -region is required when '-w region' is used");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+ } else {
+ if(is_capturing_region) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "option -region is deprecated, use -w with region directly instead, for example: -w %s", region_arg->values[0]);
+ } else {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -region can only be used when option '-w region' is used");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+ }
+
+ if(!arg_parser->settings.restore_portal_session && gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL))
+ gsr_log(GSR_LOG_LEVEL_INFO, "option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored");
+
+ return GSR_ERROR_OK;
+}
+
+static void screenshot_saved_callback(const char *filepath, void *userdata) {
+ const char *recording_saved_script = userdata;
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, filepath, "screenshot");
+}
+
+static void replay_saved_callback(const char *filepath, void *userdata) {
+ const char *recording_saved_script = userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to save replay\n");
+ fflush(stdout);
+ return;
+ }
+
+ puts(filepath);
+ fflush(stdout);
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, filepath, "replay");
+}
+
+static void recording_started_callback(const char *filepath, void *userdata) {
+ (void)userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to start recording\n");
+ fflush(stdout);
+ }
+}
+
+static void recording_stopped_callback(const char *filepath, void *userdata) {
+ const char *recording_saved_script = userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to save recording\n");
+ fflush(stdout);
+ return;
+ }
+
+ puts(filepath);
+ fflush(stdout);
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, filepath, "regular");
+}
+
+#ifdef GSR_APP_AUDIO
+static bool app_audio_name_callback(const char *app_name, void *userdata) {
+ gsr_app_audio_names *app_audio_names = userdata;
+ gsr_app_audio_names_add(app_audio_names, app_name);
+ return true;
+}
+
+static int setup_app_audio(gsr_pipewire_audio *pipewire_audio, gsr_app_audio_names *app_audio_names) {
+ if(!pulseaudio_server_is_pipewire()) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "your sound server is not PipeWire. Application audio is only available when running PipeWire audio server");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+
+ if(!gsr_pipewire_audio_init(pipewire_audio)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup PipeWire audio for application audio capture");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+
+ gsr_pipewire_audio_for_each_app(pipewire_audio, app_audio_name_callback, app_audio_names);
+ return GSR_ERROR_OK;
+}
+#endif
+
+static int parse_audio_inputs(args_parser *arg_parser, gsr_audio_input_tracks *audio_input_tracks) {
+ const Arg *audio_input_arg = args_parser_get_arg(arg_parser, "-a");
+ assert(audio_input_arg);
+
+ gsr_audio_devices audio_devices;
+ memset(&audio_devices, 0, sizeof(audio_devices));
+ if(audio_input_arg->num_values > 0)
+ get_pulseaudio_inputs(&audio_devices);
+
+ const int parse_result = gsr_audio_input_tracks_parse(audio_input_tracks, audio_input_arg->values, audio_input_arg->num_values, &audio_devices);
+ gsr_audio_devices_deinit(&audio_devices);
+ return parse_result;
+}
+
+static int take_screenshot(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_image_format image_format) {
+ const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p");
+ assert(plugin_arg);
+
+ arg_parser->settings.fps = 60; /* We want to capture an image as soon as possible */
+
+ gsr_screenshot_params screenshot_params;
+ memset(&screenshot_params, 0, sizeof(screenshot_params));
+ screenshot_params.settings = &arg_parser->settings;
+ screenshot_params.egl = &windowing->egl;
+ screenshot_params.window = windowing->window;
+ screenshot_params.capture_deps = capture_deps;
+ screenshot_params.capture_sources = capture_sources;
+ screenshot_params.image_format = image_format;
+ screenshot_params.plugin_filepaths = plugin_arg->values;
+ screenshot_params.num_plugin_filepaths = plugin_arg->num_values;
+ screenshot_params.running = &running;
+ screenshot_params.screenshot_saved = screenshot_saved_callback;
+ screenshot_params.userdata = (void*)arg_parser->settings.recording_saved_script;
+
+ 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, gsr_pipewire_audio *pipewire_audio) {
+ const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p");
+ assert(plugin_arg);
+
+ gsr_recorder_params recorder_params;
+ memset(&recorder_params, 0, sizeof(recorder_params));
+ recorder_params.settings = &arg_parser->settings;
+ recorder_params.windowing = windowing;
+ recorder_params.capture_deps = capture_deps;
+ recorder_params.capture_sources = capture_sources;
+ recorder_params.audio_input_tracks = audio_input_tracks;
+ recorder_params.plugin_filepaths = plugin_arg->values;
+ recorder_params.num_plugin_filepaths = plugin_arg->num_values;
+#ifdef GSR_APP_AUDIO
+ recorder_params.pipewire_audio = pipewire_audio;
+#else
+ (void)pipewire_audio;
+#endif
+
+ gsr_recorder_callbacks callbacks;
+ memset(&callbacks, 0, sizeof(callbacks));
+ callbacks.replay_saved = replay_saved_callback;
+ callbacks.recording_started = recording_started_callback;
+ callbacks.recording_stopped = recording_stopped_callback;
+ callbacks.userdata = (void*)arg_parser->settings.recording_saved_script;
+
+ int error = GSR_ERROR_OK;
+ recorder = gsr_recorder_create(&recorder_params, &callbacks, &error);
+ if(!recorder)
+ return error;
+
+ apply_pending_signals(recorder);
+ if(!running)
+ gsr_recorder_stop(recorder);
+
+ const int run_result = gsr_recorder_run(recorder);
+ gsr_recorder_destroy(recorder);
+ recorder = NULL;
+ return run_result;
+}
+
+int main(int argc, char **argv) {
+ setlocale(LC_ALL, "C"); /* Sigh... stupid C */
+#ifdef __GLIBC__
+ mallopt(M_MMAP_THRESHOLD, 65536);
+#endif
+
+ install_signal_handlers();
+ set_environment_variables();
+ install_cuda_no_stable_perf_limit();
+
+ if(geteuid() == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "don't run gpu-screen-recorder as the root user");
+ _exit(1);
+ }
+
+ args_handlers arg_handlers;
+ arg_handlers.version = version_command;
+ arg_handlers.info = info_command;
+ arg_handlers.list_audio_devices = list_audio_devices_command;
+ arg_handlers.list_application_audio = list_application_audio_command;
+ arg_handlers.list_v4l2_devices = list_v4l2_devices;
+ arg_handlers.list_capture_options = list_capture_options_command;
+ arg_handlers.list_monitors = list_monitors_command;
+
+ args_parser arg_parser;
+ int command_exit_code = 0;
+ switch(args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL, &command_exit_code)) {
+ case ARGS_PARSE_RESULT_ERROR:
+ _exit(1);
+ case ARGS_PARSE_RESULT_COMMAND_HANDLED:
+ _exit(command_exit_code);
+ case ARGS_PARSE_RESULT_OK:
+ break;
+ }
+
+ if(!arg_parser.settings.low_power) {
+ /* Forces low latency encoding mode. Use this environment variable until vaapi supports setting this as a parameter.
+ The downside of this is that it always uses maximum power, which is not ideal for replay mode that runs on system startup.
+ This option was added in mesa 24.1.4, released in july 17, 2024.
+ Seems like the performance issue is not in encoding, but rendering the frame.
+ Some frames end up taking 10 times longer. Seems to be an issue with amd gpu power management when letting the application sleep on the cpu side? */
+ setenv("AMD_DEBUG", "lowlatencyenc", true);
+ }
+
+ gsr_capture_sources capture_sources;
+ 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(gsr_error_to_exit_code(parse_capture_sources_result));
+
+ if(capture_sources.num_items == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -w can't be empty. You need to capture video from at least one source");
+ args_parser_print_usage();
+ _exit(1);
+ }
+
+ const int validate_args_result = validate_args_with_capture_sources(&arg_parser, &capture_sources);
+ if(validate_args_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(validate_args_result));
+
+ gsr_audio_input_tracks audio_input_tracks;
+ const int parse_audio_inputs_result = parse_audio_inputs(&arg_parser, &audio_input_tracks);
+ if(parse_audio_inputs_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(parse_audio_inputs_result));
+
+ const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&audio_input_tracks);
+ gsr_app_audio_names app_audio_names;
+ memset(&app_audio_names, 0, sizeof(app_audio_names));
+
+ gsr_pipewire_audio pipewire_audio;
+ memset(&pipewire_audio, 0, sizeof(pipewire_audio));
+#ifdef GSR_APP_AUDIO
+ if(uses_app_audio) {
+ const int app_audio_result = setup_app_audio(&pipewire_audio, &app_audio_names);
+ if(app_audio_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(app_audio_result));
+ }
+#else
+ if(uses_app_audio) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "application audio can't be recorded because GPU Screen Recorder is built without application audio support (-Dapp_audio option)");
+ _exit(2);
+ }
+#endif
+
+ const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&audio_input_tracks, &app_audio_names);
+ gsr_app_audio_names_deinit(&app_audio_names);
+ if(validate_app_audio_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(validate_app_audio_result));
+
+ gsr_windowing windowing;
+ gsr_windowing_params windowing_params;
+ windowing_params.monitor_capture = gsr_capture_sources_has_monitor_or_region(&capture_sources);
+ windowing_params.gl_debug = arg_parser.settings.gl_debug;
+ windowing_params.listen_to_x11_events = true;
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK)
+ _exit(1);
+
+ if(gsr_capture_sources_has_type(&capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) {
+ if(gsr_windowing_is_using_prime_run()) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run with -w portal option is currently not supported. Disabling prime-run");
+ gsr_windowing_disable_prime_run();
+ }
+
+ if(video_codec_is_hdr(arg_parser.settings.video_codec)) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr");
+ arg_parser.settings.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser.settings.video_codec);
+ }
+ }
+
+ if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK)
+ _exit(1);
+
+ gsr_shader_enable_debug_output(arg_parser.settings.gl_debug);
+#ifndef NDEBUG
+ gsr_shader_enable_debug_output(true);
+#endif
+
+ if(!args_parser_validate_with_gl_info(&arg_parser, &windowing.egl))
+ _exit(1);
+
+ if(!windowing.card_path_found) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "no /dev/dri/cardX device found. Make sure that you have at least one monitor connected or record a single window instead on X11 or record with the -w portal option");
+ _exit(2);
+ }
+
+ gsr_capture_deps capture_deps;
+ gsr_capture_deps_init(&capture_deps);
+ gsr_capture_deps_init_cursor(&capture_deps, &windowing.egl, arg_parser.settings.record_cursor);
+
+ int result = GSR_ERROR_OK;
+ gsr_image_format image_format;
+ if(get_image_format_from_filename(arg_parser.settings.filename, &image_format)) {
+ if(audio_input_tracks.num_items > 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "can't record audio (-a) when taking a screenshot");
+ _exit(1);
+ }
+
+ result = take_screenshot(&arg_parser, &windowing, &capture_deps, &capture_sources, image_format);
+ } else {
+ result = record(&arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks, &pipewire_audio);
+ }
+
+ gsr_capture_deps_deinit(&capture_deps);
+#ifdef GSR_APP_AUDIO
+ gsr_pipewire_audio_deinit(&pipewire_audio);
+#endif
+ gsr_audio_input_tracks_deinit(&audio_input_tracks);
+ gsr_capture_sources_deinit(&capture_sources);
+ args_parser_deinit(&arg_parser);
+
+ /* We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze,
+ but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling
+ the at_exit registered functions.
+ Cuda (nvenc) is loaded in a separate process, but this still happens. */
+ _exit(gsr_error_to_exit_code(result));
+}
diff --git a/src/main.cpp b/src/main.cpp
deleted file mode 100644
index 46b54e2..0000000
--- a/src/main.cpp
+++ /dev/null
@@ -1,1158 +0,0 @@
-extern "C" {
-#include "../include/ffmpeg_utils.h"
-#include "../include/recorder/audio_codec.h"
-#include "../include/recorder/video_codec.h"
-#include "../include/recorder/codec_select.h"
-#include "../include/recorder/windowing.h"
-#include "../include/recorder/capture_source.h"
-#include "../include/recorder/capture_setup.h"
-#include "../include/recorder/audio_input.h"
-#include "../include/recorder/audio_capture.h"
-#include "../include/recorder/muxer.h"
-#include "../include/recorder/replay_save.h"
-#include "../include/recorder/screenshot.h"
-#include "../include/cli/commands.h"
-#include "../include/recorder/error.h"
-#include "../include/capture/nvfbc.h"
-#include "../include/capture/xcomposite.h"
-#include "../include/capture/ximage.h"
-#include "../include/capture/kms.h"
-#include "../include/capture/v4l2.h"
-#ifdef GSR_PORTAL
-#include "../include/capture/portal.h"
-#include "../include/dbus.h"
-#endif
-#ifdef GSR_APP_AUDIO
-#include "../include/pipewire_audio.h"
-#endif
-#include "../include/encoder/encoder.h"
-#include "../include/encoder/video/nvenc.h"
-#include "../include/encoder/video/vaapi.h"
-#include "../include/encoder/video/vulkan.h"
-#include "../include/encoder/video/software.h"
-#include "../include/codec_query/nvenc.h"
-#include "../include/codec_query/vaapi.h"
-#include "../include/codec_query/vulkan.h"
-#include "../include/window/x11.h"
-#include "../include/window/wayland.h"
-#include "../include/egl.h"
-#include "../include/utils.h"
-#include "../include/damage.h"
-#include "../include/color_conversion.h"
-#include "../include/image_writer.h"
-#include "../include/args_parser.h"
-#include "../include/plugins.h"
-#include "../kms/client/kms_client.h"
-}
-
-#include "../include/log.h"
-#include <assert.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string>
-#include <vector>
-#include <optional>
-#include <thread>
-#include <mutex>
-#include <signal.h>
-#include <sys/stat.h>
-#include <unistd.h>
-#include <sys/wait.h>
-#include <inttypes.h>
-#include <libgen.h>
-#include <malloc.h>
-
-#include "../include/sound.h"
-
-extern "C" {
-#include <libavutil/pixfmt.h>
-#include <libavcodec/avcodec.h>
-#include <libavformat/avformat.h>
-#include <libavutil/opt.h>
-#include <libswresample/swresample.h>
-#include <libavutil/avutil.h>
-#include <libavutil/time.h>
-#include <libavutil/mastering_display_metadata.h>
-#include <libavfilter/avfilter.h>
-#include <libavfilter/buffersink.h>
-#include <libavfilter/buffersrc.h>
-}
-
-#include <future>
-
-#ifndef GSR_VERSION
-#define GSR_VERSION "unknown"
-#endif
-
-// TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd.
-
-// TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0
-
-static const int VIDEO_STREAM_INDEX = 0;
-
-/* AV1/VP9 use the 0-255 qindex quantizer scale and VP8 uses 0-127. Unlike h264 qp the quantizer step size
- is not exponential in qindex, so the mapping to an equivalent h264 qp is not linear in the quantizer range.
- qindex = h264 qp * 4 matches the qindex = crf * 4 scale used by libvpx/libaom, where crf is roughly equivalent to h264 qp. */
-static sig_atomic_t running = 1;
-static sig_atomic_t toggle_pause = 0;
-static sig_atomic_t toggle_replay_recording = 0;
-static sig_atomic_t save_replay_seconds = 0;
-
-static void stop_handler(int) {
- running = 0;
-}
-
-static void toggle_pause_handler(int) {
- toggle_pause = 1;
-}
-
-static void toggle_replay_recording_handler(int) {
- toggle_replay_recording = 1;
-}
-
-static void save_replay_handler(int) {
- save_replay_seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
-}
-
-static void save_replay_10_seconds_handler(int) {
- save_replay_seconds = 10;
-}
-
-static void save_replay_30_seconds_handler(int) {
- save_replay_seconds = 30;
-}
-
-static void save_replay_1_minute_handler(int) {
- save_replay_seconds = 60;
-}
-
-static void save_replay_5_minutes_handler(int) {
- save_replay_seconds = 60*5;
-}
-
-static void save_replay_10_minutes_handler(int) {
- save_replay_seconds = 60*10;
-}
-
-static void save_replay_30_minutes_handler(int) {
- save_replay_seconds = 60*30;
-}
-
-// TODO: Cleanup
-// Returns the number of monitors found
-// |card_path| can be NULL. If not NULL then |vendor| has to be valid
-static gsr_capture_deps capture_deps;
-
-// TODO: 10-bit and hdr.
-// Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio.
-// Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED.
-// OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU
-// Should use amix if more than 1 audio device and 0 application audio, merged
-/* Returns -1 if none is available */
-#ifdef GSR_APP_AUDIO
-#endif
-
-static void screenshot_saved_callback(const char *filepath, void *userdata) {
- const char *recording_saved_script = (const char*)userdata;
- if(recording_saved_script)
- run_recording_saved_script_async(recording_saved_script, filepath, "screenshot");
-}
-
-static void set_display_server_environment_variables() {
- // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY)
- const char *display = getenv("DISPLAY");
- if(!display) {
- display = ":0";
- setenv("DISPLAY", display, true);
- }
-
- const char *wayland_display = getenv("WAYLAND_DISPLAY");
- if(!wayland_display) {
- wayland_display = "wayland-0";
- setenv("WAYLAND_DISPLAY", wayland_display, true);
- }
-}
-
-static void validate_args_with_capture_sources(args_parser &arg_parser, const gsr_capture_sources *capture_sources) {
- const Arg *output_resolution_arg = args_parser_get_arg(&arg_parser, "-s");
- assert(output_resolution_arg);
-
- const Arg *region_arg = args_parser_get_arg(&arg_parser, "-region");
- assert(region_arg);
-
- if(gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "option -s is required when using '-w focused' option");
- args_parser_print_usage();
- _exit(1);
- }
-
- const bool is_capturing_region = gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION);
- if(region_arg->num_values == 0) {
- if(is_capturing_region && !gsr_capture_sources_has_region_set(capture_sources)) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "option -region is required when '-w region' is used");
- args_parser_print_usage();
- _exit(1);
- }
- } else {
- if(is_capturing_region) {
- gsr_log(GSR_LOG_LEVEL_WARNING, "option -region is deprecated, use -w with region directly instead, for example: -w %s", region_arg->values[0]);
- } else {
- gsr_log(GSR_LOG_LEVEL_ERROR, "option -region can only be used when option '-w region' is used");
- args_parser_print_usage();
- _exit(1);
- }
- }
-
- if(!arg_parser.settings.restore_portal_session && gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL))
- gsr_log(GSR_LOG_LEVEL_INFO, "option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored");
-}
-
-static void install_cuda_no_stable_perf_limit() {
- if(access("/proc/driver/nvidia/version", F_OK) != 0)
- return;
-
- const char *home = getenv("HOME");
- if(!home) {
- gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: $HOME not set");
- return;
- }
-
- char nv_profiles_path[4096];
- snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d", home);
-
- if(create_directory_recursive(nv_profiles_path) != 0) {
- gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create directory: %s", nv_profiles_path);
- return;
- }
-
- snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d/10-gsr-cuda-no-stable-perf-limit", home);
-
- FILE *f = fopen(nv_profiles_path, "wb");
- if(!f) {
- gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create file: %s", nv_profiles_path);
- return;
- }
-
- const char *profile_data =
- "{\n"
- " \"profiles\": [\n"
- " {\n"
- " \"name\": \"CudaNoStablePerfLimit\",\n"
- " \"settings\": [\"0x166c5e\", 0]\n"
- " }\n"
- " ],\n"
- " \"rules\": [\n"
- " { \"pattern\": \"gpu-screen-recorder\", \"profile\": \"CudaNoStablePerfLimit\" }\n"
- " ]\n"
- "}\n";
-
- fwrite(profile_data, 1, strlen(profile_data), f);
- fclose(f);
-}
-
-int main(int argc, char **argv) {
- setlocale(LC_ALL, "C"); // Sigh... stupid C
-#ifdef __GLIBC__
- mallopt(M_MMAP_THRESHOLD, 65536);
-#endif
-
- signal(SIGINT, stop_handler);
- signal(SIGTERM, stop_handler);
- signal(SIGUSR1, save_replay_handler);
- signal(SIGUSR2, toggle_pause_handler);
- signal(SIGRTMIN, toggle_replay_recording_handler);
- signal(SIGRTMIN+1, save_replay_10_seconds_handler);
- signal(SIGRTMIN+2, save_replay_30_seconds_handler);
- signal(SIGRTMIN+3, save_replay_1_minute_handler);
- signal(SIGRTMIN+4, save_replay_5_minutes_handler);
- signal(SIGRTMIN+5, save_replay_10_minutes_handler);
- signal(SIGRTMIN+6, save_replay_30_minutes_handler);
-
- set_display_server_environment_variables();
- install_cuda_no_stable_perf_limit();
-
- // Linux nvidia driver 580.105.08 added the environment variable CUDA_DISABLE_PERF_BOOST to disable the p2 power level issue,
- // where running cuda (which includes nvenc) causes the gpu to be forcefully set to p2 power level which on many nvidia gpus
- // decreases gpu performance in games. On my GTX 1080 it decreased game performance by 10% for absolutely no reason.
- setenv("CUDA_DISABLE_PERF_BOOST", "1", true);
- // Stop nvidia driver from buffering frames
- setenv("__GL_MaxFramesAllowed", "1", true);
- // If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context,
- // so we overwrite it
- setenv("__GL_THREADED_OPTIMIZATIONS", "0", true);
- // Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since
- // nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet.
- // Let vaapi find the right vaapi driver instead of forcing a specific one.
- unsetenv("LIBVA_DRIVER_NAME");
- // Some people set this to force all applications to vsync on nvidia, but this makes eglSwapBuffers never return.
- unsetenv("__GL_SYNC_TO_VBLANK");
- // Same as above, but for amd/intel
- unsetenv("vblank_mode");
-
- if(geteuid() == 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "don't run gpu-screen-recorder as the root user");
- _exit(1);
- }
-
- args_handlers arg_handlers;
- arg_handlers.version = version_command;
- arg_handlers.info = info_command;
- arg_handlers.list_audio_devices = list_audio_devices_command;
- arg_handlers.list_application_audio = list_application_audio_command;
- arg_handlers.list_v4l2_devices = list_v4l2_devices;
- arg_handlers.list_capture_options = list_capture_options_command;
- arg_handlers.list_monitors = list_monitors_command;
-
- args_parser arg_parser;
- int command_exit_code = 0;
- switch(args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL, &command_exit_code)) {
- case ARGS_PARSE_RESULT_ERROR:
- _exit(1);
- case ARGS_PARSE_RESULT_COMMAND_HANDLED:
- _exit(command_exit_code);
- case ARGS_PARSE_RESULT_OK:
- break;
- }
-
- if(!arg_parser.settings.low_power) {
- // Forces low latency encoding mode. Use this environment variable until vaapi supports setting this as a parameter.
- // The downside of this is that it always uses maximum power, which is not ideal for replay mode that runs on system startup.
- // This option was added in mesa 24.1.4, released in july 17, 2024.
- // Seems like the performance issue is not in encoding, but rendering the frame.
- // Some frames end up taking 10 times longer. Seems to be an issue with amd gpu power management when letting the application sleep on the cpu side?
- setenv("AMD_DEBUG", "lowlatencyenc", true);
- }
-
- gsr_capture_sources capture_sources_data;
- const int parse_capture_sources_result = gsr_capture_sources_parse(&capture_sources_data, arg_parser.settings.capture_source, arg_parser.settings.region_position, arg_parser.settings.region_size);
- if(parse_capture_sources_result != GSR_ERROR_OK)
- _exit(gsr_error_to_exit_code(parse_capture_sources_result));
-
- gsr_capture_sources *capture_sources = &capture_sources_data;
- if(capture_sources->num_items == 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "option -w can't be empty. You need to capture video from at least one source");
- args_parser_print_usage();
- _exit(1);
- }
- validate_args_with_capture_sources(arg_parser, capture_sources);
-
- //av_log_set_level(AV_LOG_TRACE);
-
- const Arg *audio_input_arg = args_parser_get_arg(&arg_parser, "-a");
- assert(audio_input_arg);
-
- gsr_audio_devices audio_devices;
- memset(&audio_devices, 0, sizeof(audio_devices));
- if(audio_input_arg->num_values > 0)
- get_pulseaudio_inputs(&audio_devices);
-
- gsr_audio_input_tracks requested_audio_inputs;
- const int parse_audio_inputs_result = gsr_audio_input_tracks_parse(&requested_audio_inputs, audio_input_arg->values, audio_input_arg->num_values, &audio_devices);
- gsr_audio_devices_deinit(&audio_devices);
- if(parse_audio_inputs_result != GSR_ERROR_OK)
- _exit(gsr_error_to_exit_code(parse_audio_inputs_result));
-
- const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&requested_audio_inputs);
- gsr_app_audio_names app_audio_names;
- memset(&app_audio_names, 0, sizeof(app_audio_names));
-#ifdef GSR_APP_AUDIO
- gsr_pipewire_audio pipewire_audio;
- memset(&pipewire_audio, 0, sizeof(pipewire_audio));
- if(uses_app_audio) {
- if(!pulseaudio_server_is_pipewire()) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "your sound server is not PipeWire. Application audio is only available when running PipeWire audio server");
- _exit(2);
- }
-
- if(!gsr_pipewire_audio_init(&pipewire_audio)) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup PipeWire audio for application audio capture");
- _exit(2);
- }
-
- gsr_pipewire_audio_for_each_app(&pipewire_audio, [](const char *app_name, void *userdata) {
- gsr_app_audio_names *app_audio_names = (gsr_app_audio_names*)userdata;
- gsr_app_audio_names_add(app_audio_names, app_name);
- return true;
- }, &app_audio_names);
- }
-#else
- if(uses_app_audio) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "application audio can't be recorded because GPU Screen Recorder is built without application audio support (-Dapp_audio option)");
- _exit(2);
- }
-#endif
-
- const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&requested_audio_inputs, &app_audio_names);
- gsr_app_audio_names_deinit(&app_audio_names);
- if(validate_app_audio_result != GSR_ERROR_OK)
- _exit(gsr_error_to_exit_code(validate_app_audio_result));
-
- gsr_windowing windowing;
- gsr_windowing_params windowing_params;
- windowing_params.monitor_capture = gsr_capture_sources_has_monitor_or_region(capture_sources);
- windowing_params.gl_debug = arg_parser.settings.gl_debug;
- windowing_params.listen_to_x11_events = true;
- if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK)
- _exit(1);
-
- Display *dpy = windowing.display;
- gsr_window *window = windowing.window;
-
- if(gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) {
- if(gsr_windowing_is_using_prime_run()) {
- gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run with -w portal option is currently not supported. Disabling prime-run");
- gsr_windowing_disable_prime_run();
- }
-
- if(video_codec_is_hdr(arg_parser.settings.video_codec)) {
- gsr_log(GSR_LOG_LEVEL_WARNING, "portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr");
- arg_parser.settings.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser.settings.video_codec);
- }
- }
-
- if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK)
- _exit(1);
-
- gsr_egl &egl = windowing.egl;
-
- gsr_shader_enable_debug_output(arg_parser.settings.gl_debug);
-#ifndef NDEBUG
- gsr_shader_enable_debug_output(true);
-#endif
-
- if(!args_parser_validate_with_gl_info(&arg_parser, &egl))
- _exit(1);
-
- if(!windowing.card_path_found) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "no /dev/dri/cardX device found. Make sure that you have at least one monitor connected or record a single window instead on X11 or record with the -w portal option");
- _exit(2);
- }
-
- gsr_capture_deps_init_cursor(&capture_deps, &egl, arg_parser.settings.record_cursor);
-
- // if(wayland && arg_parser.capture_source_type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) {
- // fprintf(stderr, "gsr warning: it's not possible to sync video to recorded monitor exactly on wayland when recording a monitor."
- // " If you experience stutter in the video then record with portal capture option instead (-w portal) or use X11 instead\n");
- // }
-
- gsr_image_format image_format;
- if(get_image_format_from_filename(arg_parser.settings.filename, &image_format)) {
- if(audio_input_arg->num_values > 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "can't record audio (-a) when taking a screenshot");
- _exit(1);
- }
-
- const Arg *screenshot_plugin_arg = args_parser_get_arg(&arg_parser, "-p");
- assert(screenshot_plugin_arg);
-
- arg_parser.settings.fps = 60; // We want to capture an image as soon as possible
-
- gsr_screenshot_params screenshot_params;
- memset(&screenshot_params, 0, sizeof(screenshot_params));
- screenshot_params.settings = &arg_parser.settings;
- screenshot_params.egl = &egl;
- screenshot_params.window = window;
- screenshot_params.capture_deps = &capture_deps;
- screenshot_params.capture_sources = capture_sources;
- screenshot_params.image_format = image_format;
- screenshot_params.plugin_filepaths = screenshot_plugin_arg->values;
- screenshot_params.num_plugin_filepaths = screenshot_plugin_arg->num_values;
- screenshot_params.running = &running;
- screenshot_params.screenshot_saved = screenshot_saved_callback;
- screenshot_params.userdata = (void*)arg_parser.settings.recording_saved_script;
-
- const int screenshot_result = gsr_screenshot_take(&screenshot_params);
- _exit(gsr_error_to_exit_code(screenshot_result));
- }
-
- AVFormatContext *av_format_context;
- // The output format is automatically guessed by the file extension
- avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.settings.container_format, arg_parser.settings.filename);
- if (!av_format_context) {
- if(arg_parser.settings.container_format) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "Container format '%s' (argument -c) is not valid", arg_parser.settings.container_format);
- } else {
- gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format");
- args_parser_print_usage();
- _exit(1);
- }
- _exit(1);
- }
-
- set_format_context_options(av_format_context);
-
- const AVOutputFormat *output_format = av_format_context->oformat;
-
- std::string file_extension = output_format->extensions ? output_format->extensions : "";
- {
- size_t comma_index = file_extension.find(',');
- if(comma_index != std::string::npos)
- file_extension = file_extension.substr(0, comma_index);
- }
-
- if(file_extension.empty())
- file_extension = arg_parser.settings.container_format ? arg_parser.settings.container_format : "";
-
- const bool force_no_audio_offset = arg_parser.settings.is_livestream || arg_parser.settings.is_output_piped || (file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm");
- const double target_fps = 1.0 / (double)arg_parser.settings.fps;
-
- const bool uses_amix = gsr_audio_input_tracks_should_use_amix(&requested_audio_inputs);
- arg_parser.settings.audio_codec = select_audio_codec_with_fallback(arg_parser.settings.audio_codec, file_extension.c_str(), uses_amix);
-
- vec2i video_size = {0, 0};
- gsr_video_sources video_sources_data;
- const int video_sources_result = gsr_video_sources_create(&video_sources_data, &arg_parser.settings, &egl, &capture_deps, false, capture_sources, &video_size);
- if(video_sources_result != GSR_ERROR_OK)
- _exit(gsr_error_to_exit_code(video_sources_result));
-
- gsr_video_sources *video_sources = &video_sources_data;
-
- // (Some?) livestreaming services require at least one audio track to work.
- // If not audio is provided then create one silent audio track.
- if(arg_parser.settings.is_livestream && requested_audio_inputs.num_items == 0) {
- gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track");
- gsr_merged_audio_inputs silent_audio_track;
- memset(&silent_audio_track, 0, sizeof(silent_audio_track));
- gsr_audio_input silent_audio_input;
- memset(&silent_audio_input, 0, sizeof(silent_audio_input));
- if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(&requested_audio_inputs, &silent_audio_track))
- _exit(1);
- }
-
- AVStream *video_stream = nullptr;
-
- if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && arg_parser.settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && arg_parser.settings.video_codec != GSR_VIDEO_CODEC_H264) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "-encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment");
- _exit(1);
- }
-
- bool low_power = false;
- const AVCodec *video_codec_f = nullptr;
- const int select_video_codec_result = select_video_codec_with_fallback(video_size, &arg_parser.settings, file_extension.c_str(), &egl, &low_power, &video_codec_f);
- if(select_video_codec_result != GSR_ERROR_OK)
- _exit(gsr_error_to_exit_code(select_video_codec_result));
-
- const enum AVPixelFormat video_pix_fmt = get_pixel_format(arg_parser.settings.video_codec, egl.gpu_info.vendor, arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU);
- AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, &egl, &arg_parser.settings, video_size.x, video_size.y);
- if(!arg_parser.settings.is_replaying)
- video_stream = create_stream(av_format_context, video_codec_context);
-
- AVFrame *video_frame = av_frame_alloc();
- if(!video_frame) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to allocate video frame");
- _exit(1);
- }
- video_frame->format = video_codec_context->pix_fmt;
- video_frame->width = video_size.x;
- video_frame->height = video_size.y;
- video_frame->color_range = video_codec_context->color_range;
- video_frame->color_primaries = video_codec_context->color_primaries;
- video_frame->color_trc = video_codec_context->color_trc;
- video_frame->colorspace = video_codec_context->colorspace;
- video_frame->chroma_location = video_codec_context->chroma_sample_location;
-
- const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.fps, arg_parser.settings.audio_codec, &requested_audio_inputs);
- gsr_recording_clock *recording_clock = gsr_recording_clock_create();
- if(!recording_clock)
- _exit(1);
-
- gsr_encoder encoder;
- if(!gsr_encoder_init(&encoder, arg_parser.settings.replay_storage, estimated_replay_buffer_packets, arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.filename)) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder");
- _exit(1);
- }
-
- gsr_video_encoder *video_encoder = create_video_encoder(&egl, &arg_parser.settings);
- if(!video_encoder) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video encoder");
- _exit(1);
- }
-
- if(!gsr_video_encoder_start(video_encoder, video_codec_context, video_frame)) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video encoder");
- _exit(1);
- }
-
- video_size.x = video_codec_context->width;
- video_size.y = video_codec_context->height;
- gsr_video_sources_update_with_real_video_size(video_sources, video_size);
-
- const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p");
- assert(plugin_arg);
-
- gsr_plugins plugins;
- memset(&plugins, 0, sizeof(plugins));
-
- if(gsr_load_plugins(&plugins, plugin_arg->values, plugin_arg->num_values, &arg_parser.settings, &egl, video_size) != GSR_ERROR_OK)
- _exit(1);
-
- gsr_color_conversion_params color_conversion_params;
- memset(&color_conversion_params, 0, sizeof(color_conversion_params));
- color_conversion_params.color_range = arg_parser.settings.color_range;
- color_conversion_params.egl = &egl;
- color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(video_sources);
- gsr_video_encoder_get_textures(video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color);
-
- gsr_color_conversion color_conversion;
- if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "main: failed to create color conversion");
- _exit(1);
- }
-
- gsr_color_conversion_clear(&color_conversion);
-
- gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion;
-
- if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) {
- if(!open_video_software(video_codec_context, &arg_parser.settings))
- _exit(1);
- } else {
- if(!open_video_hardware(video_codec_context, low_power, &egl, &arg_parser.settings))
- _exit(1);
- }
-
- if(video_stream) {
- avcodec_parameters_from_context(video_stream->codecpar, video_codec_context);
- const size_t video_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, av_format_context, video_stream, 0);
- if(arg_parser.settings.write_first_frame_ts && video_destination_id != (size_t)-1) {
- std::string ts_filepath = std::string(arg_parser.settings.filename) + ".ts";
- gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_destination_id, ts_filepath.c_str());
- }
- }
-
- gsr_audio_capture audio_capture;
- if(gsr_audio_capture_init(&audio_capture, &encoder, recording_clock, &running) != GSR_ERROR_OK)
- _exit(1);
-
- int audio_max_frame_size = 1024;
- int audio_stream_index = VIDEO_STREAM_INDEX + 1;
- for(size_t audio_track_index = 0; audio_track_index < requested_audio_inputs.num_items; ++audio_track_index) {
- const gsr_merged_audio_inputs &merged_audio_inputs = requested_audio_inputs.items[audio_track_index];
- const bool use_amix = gsr_audio_inputs_should_use_amix(&merged_audio_inputs);
- AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.settings.fps, arg_parser.settings.audio_codec, use_amix, arg_parser.settings.audio_bitrate);
- if(!audio_codec_context)
- _exit(1);
-
- AVStream *audio_stream = nullptr;
- if(!arg_parser.settings.is_replaying) {
- audio_stream = create_stream(av_format_context, audio_codec_context);
- if(gsr_encoder_add_recording_destination(&encoder, audio_codec_context, av_format_context, audio_stream, 0) == (size_t)-1)
- gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources");
- }
-
- if(audio_stream && merged_audio_inputs.track_name[0] != '\0' && !arg_parser.settings.exclude_metadata)
- av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name, 0);
-
- if(!open_audio(audio_codec_context, arg_parser.settings.ffmpeg_audio_opts))
- _exit(1);
- if(audio_stream)
- avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context);
-
- #if LIBAVCODEC_VERSION_MAJOR < 60
- const int num_channels = audio_codec_context->channels;
- #else
- const int num_channels = audio_codec_context->ch_layout.nb_channels;
- #endif
-
- //audio_frame->sample_rate = audio_codec_context->sample_rate;
-
- AVFilterContext *src_filter_ctx[GSR_MAX_AUDIO_SOURCES_PER_TRACK];
- AVFilterGraph *graph = nullptr;
- AVFilterContext *sink = nullptr;
- if(use_amix) {
- if(merged_audio_inputs.num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK);
- _exit(1);
- }
-
- if(gsr_audio_init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.num_items) < 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter");
- _exit(1);
- }
- }
-
- // TODO: Cleanup above
-
- const double audio_fps = (double)audio_codec_context->sample_rate / (double)audio_codec_context->frame_size;
- const double timeout_sec = 1000.0 / audio_fps / 1000.0;
-
- const double audio_startup_time_seconds = force_no_audio_offset ? 0 : audio_codec_get_desired_delay(arg_parser.settings.audio_codec, arg_parser.settings.fps);// * ((double)audio_codec_context->frame_size / 1024.0);
- const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec;
-
- gsr_audio_track audio_track;
- memset(&audio_track, 0, sizeof(audio_track));
- snprintf(audio_track.name, sizeof(audio_track.name), "%s", merged_audio_inputs.track_name);
- audio_track.codec_context = audio_codec_context;
- audio_track.graph = graph;
- audio_track.sink = sink;
- audio_track.stream_index = audio_stream_index;
- audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift;
-
- int audio_track_result = GSR_ERROR_OK;
- if(gsr_audio_inputs_has_app_audio(&merged_audio_inputs)) {
- assert(!use_amix);
-#ifdef GSR_APP_AUDIO
- audio_track_result = gsr_audio_track_init_application_input(&audio_track, &merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, &pipewire_audio);
-#endif
- } else {
- audio_track_result = gsr_audio_track_init_device_inputs(&audio_track, &merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix);
- }
-
- if(audio_track_result != GSR_ERROR_OK)
- _exit(gsr_error_to_exit_code(audio_track_result));
-
- if(!gsr_audio_capture_add_track(&audio_capture, &audio_track))
- _exit(1);
- ++audio_stream_index;
-
- if(audio_codec_context->frame_size > audio_max_frame_size)
- audio_max_frame_size = audio_codec_context->frame_size;
- }
-
- //av_dump_format(av_format_context, 0, filename, 1);
-
- if(!arg_parser.settings.is_replaying && !(output_format->flags & AVFMT_NOFILE)) {
- const int ret = avio_open(&av_format_context->pb, arg_parser.settings.filename, AVIO_FLAG_WRITE);
- if(ret < 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", arg_parser.settings.filename, gsr_av_error_to_string(ret));
- _exit(1);
- }
- }
-
- if(!arg_parser.settings.is_replaying)
- av_write_header(av_format_context, arg_parser.settings.ffmpeg_opts);
-
- double fps_start_time = clock_get_monotonic_seconds();
- //double frame_timer_start = fps_start_time;
- int fps_counter = 0;
- int damage_fps_counter = 0;
-
- bool paused = false;
- bool replay_recording = false;
- gsr_recording_output replay_recording_output;
- memset(&replay_recording_output, 0, sizeof(replay_recording_output));
- gsr_replay_save replay_save;
- gsr_replay_save_init(&replay_save);
- std::vector<size_t> replay_recording_items;
- std::string replay_recording_filepath;
- bool force_iframe_frame = false; // Only needed for video since audio frames are always iframes
-
- gsr_recording_clock_start(recording_clock);
- const double record_start_time = gsr_recording_clock_get_start_time(recording_clock);
-
- if(gsr_audio_capture_start(&audio_capture, audio_max_frame_size, uses_amix) != GSR_ERROR_OK)
- _exit(1);
-
- // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video.
- //const double update_fps = fps + 190;
- bool should_stop_error = false;
-
- int64_t video_pts_counter = 0;
- int64_t video_prev_pts = 0;
-
- bool hdr_metadata_set = false;
- const bool hdr = video_codec_is_hdr(arg_parser.settings.video_codec);
-
- bool use_damage_tracking = false;
- gsr_damage damage;
- memset(&damage, 0, sizeof(damage));
- if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(capture_sources)) {
- if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11) {
- gsr_damage_init(&damage, &egl, &capture_deps.x11_cursor, arg_parser.settings.record_cursor);
- use_damage_tracking = true;
-
- for(size_t i = 0; i < capture_sources->num_items; ++i) {
- const gsr_capture_source *capture_source = &capture_sources->items[i];
- switch(capture_source->type) {
- case GSR_CAPTURE_SOURCE_TYPE_WINDOW:
- gsr_damage_start_tracking_window(&damage, capture_source->window_id);
- break;
- case GSR_CAPTURE_SOURCE_TYPE_MONITOR:
- case GSR_CAPTURE_SOURCE_TYPE_REGION:
- // TODO: When capturing a region only track damage in that region
- gsr_damage_start_tracking_monitor(&damage, capture_source->name);
- break;
- default:
- break;
- }
- }
- } else if(gsr_capture_sources_has_monitor_or_region(capture_sources)) {
- gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)");
- }
- }
-
- while(running) {
- while(gsr_window_process_event(window)) {
- if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor)
- gsr_cursor_on_event(&capture_deps.x11_cursor, gsr_window_get_event_data(window));
-
- gsr_damage_on_event(&damage, gsr_window_get_event_data(window));
- for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) {
- gsr_video_source &video_source = video_sources->items[video_source_index];
- gsr_capture_on_event(video_source.capture, &egl);
- }
- }
-
- if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor)
- gsr_cursor_tick(&capture_deps.x11_cursor, DefaultRootWindow(capture_deps.x11_cursor_display));
-
- gsr_damage_tick(&damage);
-
- should_stop_error = false;
- bool damaged = false;
-
- if(use_damage_tracking)
- damaged = gsr_damage_is_damaged(&damage);
-
- for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) {
- gsr_video_source &video_source = video_sources->items[video_source_index];
- gsr_capture_tick(video_source.capture);
-
- if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) {
- running = 0;
- break;
- }
-
- if(video_source.capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) {
- assert(video_source.capture->get_window_id);
- const Window damage_target_window = video_source.capture->get_window_id(video_source.capture);
-
- if((int64_t)damage_target_window != video_source.capture_source->window_id) {
- gsr_damage_stop_tracking_window(&damage, video_source.capture_source->window_id);
- if(damage_target_window != 0)
- gsr_damage_start_tracking_window(&damage, damage_target_window);
- }
-
- video_source.capture_source->window_id = damage_target_window;
- }
-
- if(video_source.capture->is_damaged)
- damaged |= video_source.capture->is_damaged(video_source.capture);
- else if(!use_damage_tracking)
- damaged = true;
- }
-
- damaged |= gsr_plugins_is_damaged(&plugins);
-
- // TODO: Readd wayland sync warning when removing this
- if(arg_parser.settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT)
- damaged = true;
-
- if(damaged)
- ++damage_fps_counter;
-
- ++fps_counter;
- const double time_now = clock_get_monotonic_seconds();
- //const double frame_timer_elapsed = time_now - frame_timer_start;
- const double elapsed = time_now - fps_start_time;
- if (elapsed >= 1.0) {
- if(arg_parser.settings.verbose) {
- fprintf(stderr, "update fps: %d, damage fps: %d\n", fps_counter, damage_fps_counter);
- }
- fps_start_time = time_now;
- fps_counter = 0;
- damage_fps_counter = 0;
- }
-
- const double this_video_frame_time = gsr_recording_clock_get_time(recording_clock);
- const int64_t expected_frames = std::floor((this_video_frame_time - record_start_time) / target_fps);
- const int64_t num_missed_frames = expected_frames - video_pts_counter;
-
- if(damaged && num_missed_frames >= 1 && !paused) {
- // TODO: Dont do this if no damage?
- egl.glClear(0);
-
- gsr_damage_clear(&damage);
- gsr_plugins_clear_damage(&plugins);
- gsr_capture_deps_cleanup_kms_fds(&capture_deps);
-
- gsr_capture_deps_update_kms(&capture_deps);
-
- bool capture_has_synchronous_task = false;
- for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) {
- gsr_video_source &video_source = video_sources->items[video_source_index];
- if(video_source.capture->clear_damage)
- video_source.capture->clear_damage(video_source.capture);
-
- if(video_source.capture->capture_has_synchronous_task) {
- capture_has_synchronous_task = video_source.capture->capture_has_synchronous_task(video_source.capture);
- if(capture_has_synchronous_task) {
- paused = true;
- gsr_recording_clock_set_paused(recording_clock, true);
- }
- }
- }
-
- for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) {
- gsr_video_source &video_source = video_sources->items[video_source_index];
- if(video_source.capture->pre_capture)
- video_source.capture->pre_capture(video_source.capture, &video_source.metadata, output_color_conversion);
- }
-
- if(output_color_conversion->schedule_clear) {
- output_color_conversion->schedule_clear = false;
- gsr_color_conversion_clear(output_color_conversion);
- }
-
- for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) {
- gsr_video_source &video_source = video_sources->items[video_source_index];
- gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion);
- }
-
- gsr_capture_deps_cleanup_kms_fds(&capture_deps);
-
- if(plugins.num_plugins > 0) {
- gsr_plugins_draw(&plugins);
- gsr_color_conversion_draw(&color_conversion, plugins.texture,
- {0, 0}, video_size,
- {0, 0}, video_size,
- video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false);
- }
-
- if(capture_has_synchronous_task) {
- paused = false;
- gsr_recording_clock_set_paused(recording_clock, false);
- }
-
- gsr_egl_swap_buffers(&egl);
- gsr_video_encoder_copy_textures_to_frame(video_encoder, video_frame, output_color_conversion);
-
- for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) {
- gsr_video_source &video_source = video_sources->items[video_source_index];
- if(hdr && !hdr_metadata_set && !arg_parser.settings.is_replaying && add_hdr_metadata_to_video_stream(video_source.capture, video_stream))
- hdr_metadata_set = true;
- }
-
- // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again
- const int num_frames_to_encode = arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1;
- for(int i = 0; i < num_frames_to_encode; ++i) {
- if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) {
- video_frame->pts = video_pts_counter + i;
- } else {
- video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE;
- const bool same_pts = video_frame->pts == video_prev_pts;
- video_prev_pts = video_frame->pts;
- if(same_pts)
- continue;
- }
-
- if(force_iframe_frame) {
- video_frame->pict_type = AV_PICTURE_TYPE_I;
- }
-
- int ret = avcodec_send_frame(video_codec_context, video_frame);
- if(ret == 0) {
- // TODO: Move to separate thread because this could write to network (for example when livestreaming)
- gsr_encoder_receive_packets(&encoder, video_codec_context, video_frame->pts, VIDEO_STREAM_INDEX);
- } else {
- gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret));
- }
-
- if(force_iframe_frame) {
- force_iframe_frame = false;
- video_frame->pict_type = AV_PICTURE_TYPE_NONE;
- }
- }
-
- video_pts_counter += num_missed_frames;
- }
-
- if(toggle_pause == 1 && !arg_parser.settings.is_replaying) {
- paused = !paused;
- gsr_recording_clock_set_paused(recording_clock, paused);
- gsr_log(GSR_LOG_LEVEL_INFO, paused ? "Paused" : "Unpaused");
- toggle_pause = 0;
- }
-
- if(toggle_replay_recording && !arg_parser.settings.replay_recording_directory) {
- toggle_replay_recording = 0;
- printf("gsr error: Unable to start recording since the -ro option was not specified\n");
- fflush(stdout);
- }
-
- if(toggle_replay_recording && arg_parser.settings.replay_recording_directory) {
- toggle_replay_recording = 0;
- const bool new_replay_recording_state = !replay_recording;
- if(new_replay_recording_state) {
- gsr_audio_capture_lock_filter(&audio_capture);
- replay_recording_items.clear();
- char replay_recording_filepath_buf[PATH_MAX];
- const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(replay_recording_filepath_buf, sizeof(replay_recording_filepath_buf), arg_parser.settings.replay_recording_directory, "Video", file_extension.c_str(), arg_parser.settings.date_folders);
- replay_recording_filepath = filepath_created ? replay_recording_filepath_buf : "";
- if(filepath_created && gsr_recording_output_start(&replay_recording_output, replay_recording_filepath.c_str(), &arg_parser.settings, video_codec_context, &audio_capture, hdr, video_sources)) {
- const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_output.av_format_context, replay_recording_output.video_stream, video_frame->pts);
- if(arg_parser.settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) {
- std::string ts_filepath = replay_recording_filepath + ".ts";
- gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_recording_destination_id, ts_filepath.c_str());
- }
-
- if(video_recording_destination_id != (size_t)-1)
- replay_recording_items.push_back(video_recording_destination_id);
-
- for(size_t i = 0; i < replay_recording_output.num_audio_streams; ++i) {
- const gsr_recording_audio_stream *audio_stream = &replay_recording_output.audio_streams[i];
- const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, audio_stream->audio_track->codec_context, replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts);
- if(audio_recording_destination_id != (size_t)-1)
- replay_recording_items.push_back(audio_recording_destination_id);
- }
-
- replay_recording = true;
- force_iframe_frame = true;
- gsr_log(GSR_LOG_LEVEL_INFO, "Started recording");
- } else {
- printf("gsr error: Failed to start recording\n");
- fflush(stdout);
- }
- gsr_audio_capture_unlock_filter(&audio_capture);
- } else if(replay_recording_output.av_format_context) {
- for(size_t id : replay_recording_items) {
- gsr_encoder_remove_recording_destination(&encoder, id);
- }
- replay_recording_items.clear();
-
- if(gsr_recording_output_stop(&replay_recording_output)) {
- gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording");
- puts(replay_recording_filepath.c_str());
- fflush(stdout);
- if(arg_parser.settings.recording_saved_script)
- run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_recording_filepath.c_str(), "regular");
- } else {
- printf("gsr error: Failed to save recording\n");
- fflush(stdout);
- }
-
- replay_recording = false;
- replay_recording_filepath.clear();
- }
- }
-
- bool replay_save_result = false;
- const char *replay_save_output_filepath = nullptr;
- if(gsr_replay_save_poll(&replay_save, &replay_save_result, &replay_save_output_filepath)) {
- if(replay_save_output_filepath[0] == '\0' || !replay_save_result) {
- printf("gsr error: Failed to save replay\n");
- fflush(stdout);
- } else {
- puts(replay_save_output_filepath);
- fflush(stdout);
- if(arg_parser.settings.recording_saved_script)
- run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_save_output_filepath, "replay");
- }
- }
-
- if(save_replay_seconds != 0 && !gsr_replay_save_is_running(&replay_save) && arg_parser.settings.is_replaying) {
- int current_save_replay_seconds = save_replay_seconds;
- if(current_save_replay_seconds > 0)
- current_save_replay_seconds += arg_parser.settings.keyint;
-
- save_replay_seconds = 0;
- const bool replay_start_result = gsr_replay_save_start(&replay_save, video_codec_context, VIDEO_STREAM_INDEX, &audio_capture, &encoder, &arg_parser.settings, file_extension.c_str(), hdr, video_sources, current_save_replay_seconds);
- if(!replay_start_result) {
- printf("gsr error: Failed to save replay\n");
- fflush(stdout);
- }
-
- if(arg_parser.settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) {
- pthread_mutex_lock(&encoder.replay_mutex);
- gsr_replay_buffer_clear(encoder.replay_buffer);
- pthread_mutex_unlock(&encoder.replay_mutex);
- }
- }
-
- const double time_at_frame_end = gsr_recording_clock_get_time(recording_clock);
- const double time_elapsed_total = time_at_frame_end - record_start_time;
- const int64_t frames_elapsed = std::floor(time_elapsed_total / target_fps);
- const double time_at_next_frame = (frames_elapsed + 1) * target_fps;
- double time_to_next_frame = time_at_next_frame - time_elapsed_total;
- if(time_to_next_frame > target_fps)
- time_to_next_frame = target_fps;
- const int64_t end_num_missed_frames = frames_elapsed - video_pts_counter;
-
- if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0)
- av_usleep(time_to_next_frame * 1000.0 * 1000.0);
- else {
- if(paused)
- av_usleep(20.0 * 1000.0); // 20 milliseconds
- else if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT)
- av_usleep(2.8 * 1000.0); // 2.8 milliseconds
- }
- }
-
- running = 0;
-
- bool final_replay_save_result = false;
- const char *final_replay_save_output_filepath = nullptr;
- if(gsr_replay_save_join(&replay_save, &final_replay_save_result, &final_replay_save_output_filepath)) {
- if(final_replay_save_output_filepath[0] == '\0') {
- // TODO: Output failed to save
- } else {
- puts(final_replay_save_output_filepath);
- fflush(stdout);
- if(arg_parser.settings.recording_saved_script)
- run_recording_saved_script_async(arg_parser.settings.recording_saved_script, final_replay_save_output_filepath, "replay");
- }
- }
-
- gsr_plugins_deinit(&plugins);
-
- if(replay_recording_output.av_format_context) {
- for(size_t id : replay_recording_items) {
- gsr_encoder_remove_recording_destination(&encoder, id);
- }
- replay_recording_items.clear();
-
- if(gsr_recording_output_stop(&replay_recording_output)) {
- gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording");
- puts(replay_recording_filepath.c_str());
- fflush(stdout);
- if(arg_parser.settings.recording_saved_script)
- run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_recording_filepath.c_str(), "regular");
- } else {
- printf("gsr error: Failed to save recording\n");
- fflush(stdout);
- }
- }
-
- gsr_audio_capture_join_threads(&audio_capture);
-
- // TODO: Replace this with start_recording_create_steams
- if(!arg_parser.settings.is_replaying && av_write_trailer(av_format_context) != 0) {
- //fprintf(stderr, "Failed to write trailer\n");
- }
-
- if(!arg_parser.settings.is_replaying && !(output_format->flags & AVFMT_NOFILE)) {
- avio_close(av_format_context->pb);
- avformat_free_context(av_format_context);
- }
-
- gsr_damage_deinit(&damage);
- gsr_color_conversion_deinit(&color_conversion);
- gsr_video_encoder_destroy(video_encoder, video_codec_context);
- gsr_encoder_deinit(&encoder);
- gsr_video_sources_deinit(video_sources);
-#ifdef GSR_APP_AUDIO
- gsr_pipewire_audio_deinit(&pipewire_audio);
-#endif
- gsr_capture_deps_deinit(&capture_deps);
-
- if(!arg_parser.settings.is_replaying && arg_parser.settings.recording_saved_script)
- run_recording_saved_script_async(arg_parser.settings.recording_saved_script, arg_parser.settings.filename, "regular");
-
- if(dpy) {
- // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this???
- //XCloseDisplay(dpy);
- }
-
- //gsr_egl_unload(&egl);
- //gsr_window_destroy(&window);
-
- //av_frame_free(&video_frame);
- args_parser_deinit(&arg_parser);
- // We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze,
- // but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling
- // the at_exit registered functions.
- // Cuda (cuvid library in this case) seems to be waiting for a thread that never finishes execution.
- // Maybe this happens because we dont clean up all ffmpeg resources?
- // TODO: Investigate this.
- _exit(should_stop_error ? 3 : 0);
-}
diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c
new file mode 100644
index 0000000..27cc1ad
--- /dev/null
+++ b/src/recorder/recorder.c
@@ -0,0 +1,826 @@
+#include "../../include/recorder/recorder.h"
+#include "../../include/recorder/error.h"
+#include "../../include/recorder/audio_codec.h"
+#include "../../include/recorder/video_codec.h"
+#include "../../include/recorder/codec_select.h"
+#include "../../include/recorder/muxer.h"
+#include "../../include/recorder/replay_save.h"
+#include "../../include/recorder/audio_capture.h"
+#include "../../include/recorder/recording_clock.h"
+#include "../../include/recorder/screenshot.h"
+#include "../../include/encoder/encoder.h"
+#include "../../include/encoder/video/video.h"
+#include "../../include/window/window.h"
+#include "../../include/color_conversion.h"
+#include "../../include/damage.h"
+#include "../../include/cursor.h"
+#include "../../include/plugins.h"
+#include "../../include/utils.h"
+#include "../../include/ffmpeg_utils.h"
+#include "../../include/log.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+#include <assert.h>
+#include <limits.h>
+#include <unistd.h>
+
+#include <libavutil/time.h>
+#include <libavformat/avformat.h>
+
+#include <X11/Xlib.h>
+
+#define GSR_VIDEO_STREAM_INDEX 0
+
+struct gsr_recorder {
+ gsr_recorder_settings settings;
+ gsr_recorder_callbacks callbacks;
+ gsr_windowing *windowing;
+ gsr_egl *egl;
+ gsr_window *window;
+ gsr_capture_deps *capture_deps;
+ gsr_capture_sources *capture_sources;
+ gsr_audio_input_tracks *audio_input_tracks;
+
+ char file_extension[32];
+ bool force_no_audio_offset;
+ double target_fps;
+ bool uses_amix;
+ bool hdr;
+ bool low_power;
+ vec2i video_size;
+
+ AVFormatContext *av_format_context;
+ AVStream *video_stream;
+ AVCodecContext *video_codec_context;
+ AVFrame *video_frame;
+ gsr_video_sources video_sources_data;
+ gsr_video_sources *video_sources;
+ gsr_encoder encoder;
+ bool encoder_initialized;
+ gsr_video_encoder *video_encoder;
+ gsr_color_conversion color_conversion;
+ bool color_conversion_initialized;
+ gsr_color_conversion *output_color_conversion;
+ gsr_plugins plugins;
+ gsr_recording_clock *recording_clock;
+ gsr_audio_capture audio_capture;
+ bool audio_capture_initialized;
+ gsr_replay_save replay_save;
+
+ gsr_recording_output replay_recording_output;
+ size_t replay_recording_items[GSR_MAX_RECORDING_DESTINATIONS];
+ size_t num_replay_recording_items;
+ char replay_recording_filepath[PATH_MAX];
+ bool replay_recording;
+
+ 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;
+ bool should_stop_error;
+ bool force_iframe_frame;
+ int audio_max_frame_size;
+ bool use_damage_tracking;
+ gsr_damage damage;
+ const char **plugin_filepaths;
+ int num_plugin_filepaths;
+#ifdef GSR_APP_AUDIO
+ gsr_pipewire_audio *pipewire_audio;
+#endif
+};
+
+static void gsr_recorder_stop_recording(gsr_recorder *self);
+
+static int64_t gsr_max_int64(int64_t a, int64_t b) {
+ return a > b ? a : b;
+}
+
+static int64_t gsr_min_int64(int64_t a, int64_t b) {
+ return a < b ? a : b;
+}
+
+gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error) {
+ *error = GSR_ERROR_GENERIC;
+
+ gsr_recorder *self = calloc(1, sizeof(gsr_recorder));
+ if(!self) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recorder_create: failed to allocate recorder");
+ return NULL;
+ }
+
+ self->settings = *params->settings;
+ if(callbacks)
+ self->callbacks = *callbacks;
+ self->windowing = params->windowing;
+ self->egl = &params->windowing->egl;
+ self->window = params->windowing->window;
+ self->capture_deps = params->capture_deps;
+ self->capture_sources = params->capture_sources;
+ self->audio_input_tracks = params->audio_input_tracks;
+ self->running = 1;
+ self->audio_max_frame_size = 1024;
+ self->hdr = video_codec_is_hdr(params->settings->video_codec);
+ self->plugin_filepaths = params->plugin_filepaths;
+ self->num_plugin_filepaths = params->num_plugin_filepaths;
+#ifdef GSR_APP_AUDIO
+ self->pipewire_audio = params->pipewire_audio;
+#endif
+
+
+ // The output format is automatically guessed by the file extension
+ avformat_alloc_output_context2(&self->av_format_context, NULL, self->settings.container_format, self->settings.filename);
+ if (!self->av_format_context) {
+ if(self->settings.container_format) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "Container format '%s' (argument -c) is not valid", self->settings.container_format);
+ } else {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format");
+ args_parser_print_usage();
+ _exit(1);
+ }
+ _exit(1);
+ }
+
+ set_format_context_options(self->av_format_context);
+
+ const AVOutputFormat *output_format = self->av_format_context->oformat;
+
+ const char *file_extensions = output_format->extensions ? output_format->extensions : "";
+ const char *file_extension_end = strchr(file_extensions, ',');
+ if(file_extension_end)
+ snprintf(self->file_extension, sizeof(self->file_extension), "%.*s", (int)(file_extension_end - file_extensions), file_extensions);
+ else
+ snprintf(self->file_extension, sizeof(self->file_extension), "%s", file_extensions);
+
+ if(self->file_extension[0] == '\0')
+ snprintf(self->file_extension, sizeof(self->file_extension), "%s", self->settings.container_format ? self->settings.container_format : "");
+
+ self->force_no_audio_offset = self->settings.is_livestream || self->settings.is_output_piped || (strcmp(self->file_extension, "mp4") != 0 && strcmp(self->file_extension, "mkv") != 0 && strcmp(self->file_extension, "webm") != 0);
+ self->target_fps = 1.0 / (double)self->settings.fps;
+
+ self->uses_amix = gsr_audio_input_tracks_should_use_amix(self->audio_input_tracks);
+ self->settings.audio_codec = select_audio_codec_with_fallback(self->settings.audio_codec, self->file_extension, self->uses_amix);
+
+ self->video_size = (vec2i){0, 0};
+ const int video_sources_result = gsr_video_sources_create(&self->video_sources_data, &self->settings, self->egl, self->capture_deps, false, self->capture_sources, &self->video_size);
+ if(video_sources_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(video_sources_result));
+
+ self->video_sources = &self->video_sources_data;
+
+ // (Some?) livestreaming services require at least one audio track to work.
+ // If not audio is provided then create one silent audio track.
+ if(self->settings.is_livestream && self->audio_input_tracks->num_items == 0) {
+ gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track");
+ gsr_merged_audio_inputs silent_audio_track;
+ memset(&silent_audio_track, 0, sizeof(silent_audio_track));
+ gsr_audio_input silent_audio_input;
+ memset(&silent_audio_input, 0, sizeof(silent_audio_input));
+ if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(self->audio_input_tracks, &silent_audio_track))
+ _exit(1);
+ }
+
+ self->video_stream = NULL;
+
+ if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && self->settings.video_codec != GSR_VIDEO_CODEC_H264) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "-self->encoder cpu was specified but a codec other than h264 was specified. -self->encoder cpu supports only h264 at the moment");
+ _exit(1);
+ }
+
+ self->low_power = false;
+ const AVCodec *video_codec_f = NULL;
+ const int select_video_codec_result = select_video_codec_with_fallback(self->video_size, &self->settings, self->file_extension, self->egl, &self->low_power, &video_codec_f);
+ if(select_video_codec_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(select_video_codec_result));
+
+ const enum AVPixelFormat video_pix_fmt = get_pixel_format(self->settings.video_codec, self->egl->gpu_info.vendor, self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU);
+ self->video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, self->egl, &self->settings, self->video_size.x, self->video_size.y);
+ if(!self->settings.is_replaying)
+ self->video_stream = create_stream(self->av_format_context, self->video_codec_context);
+
+ self->video_frame = av_frame_alloc();
+ if(!self->video_frame) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to allocate video frame");
+ _exit(1);
+ }
+ self->video_frame->format = self->video_codec_context->pix_fmt;
+ self->video_frame->width = self->video_size.x;
+ self->video_frame->height = self->video_size.y;
+ self->video_frame->color_range = self->video_codec_context->color_range;
+ self->video_frame->color_primaries = self->video_codec_context->color_primaries;
+ self->video_frame->color_trc = self->video_codec_context->color_trc;
+ self->video_frame->colorspace = self->video_codec_context->colorspace;
+ self->video_frame->chroma_location = self->video_codec_context->chroma_sample_location;
+
+ const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(self->settings.replay_buffer_size_secs, self->settings.fps, self->settings.audio_codec, self->audio_input_tracks);
+ self->recording_clock = gsr_recording_clock_create();
+ if(!self->recording_clock)
+ _exit(1);
+
+ if(!gsr_encoder_init(&self->encoder, self->settings.replay_storage, estimated_replay_buffer_packets, self->settings.replay_buffer_size_secs, self->settings.filename)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create self->encoder");
+ _exit(1);
+ }
+
+ self->video_encoder = create_video_encoder(self->egl, &self->settings);
+ if(!self->video_encoder) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video self->encoder");
+ _exit(1);
+ }
+
+ if(!gsr_video_encoder_start(self->video_encoder, self->video_codec_context, self->video_frame)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video self->encoder");
+ _exit(1);
+ }
+
+ self->video_size.x = self->video_codec_context->width;
+ self->video_size.y = self->video_codec_context->height;
+ gsr_video_sources_update_with_real_video_size(self->video_sources, self->video_size);
+
+ memset(&self->plugins, 0, sizeof(self->plugins));
+
+ if(gsr_load_plugins(&self->plugins, self->plugin_filepaths, self->num_plugin_filepaths, &self->settings, self->egl, self->video_size) != GSR_ERROR_OK)
+ _exit(1);
+
+ gsr_color_conversion_params color_conversion_params;
+ memset(&color_conversion_params, 0, sizeof(color_conversion_params));
+ color_conversion_params.color_range = self->settings.color_range;
+ color_conversion_params.egl = self->egl;
+ color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(self->video_sources);
+ gsr_video_encoder_get_textures(self->video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color);
+
+ if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "main: failed to create color conversion");
+ _exit(1);
+ }
+
+ gsr_color_conversion_clear(&self->color_conversion);
+
+ self->output_color_conversion = self->plugins.num_plugins > 0 ? &self->plugins.color_conversion : &self->color_conversion;
+
+ if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) {
+ if(!open_video_software(self->video_codec_context, &self->settings))
+ _exit(1);
+ } else {
+ if(!open_video_hardware(self->video_codec_context, self->low_power, self->egl, &self->settings))
+ _exit(1);
+ }
+
+ if(self->video_stream) {
+ avcodec_parameters_from_context(self->video_stream->codecpar, self->video_codec_context);
+ const size_t video_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->av_format_context, self->video_stream, 0);
+ if(self->settings.write_first_frame_ts && video_destination_id != (size_t)-1) {
+ char ts_filepath[PATH_MAX];
+ snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->settings.filename);
+ gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_destination_id, ts_filepath);
+ }
+ }
+
+ if(gsr_audio_capture_init(&self->audio_capture, &self->encoder, self->recording_clock, &self->running) != GSR_ERROR_OK)
+ _exit(1);
+
+
+ int audio_stream_index = GSR_VIDEO_STREAM_INDEX + 1;
+ for(size_t audio_track_index = 0; audio_track_index < self->audio_input_tracks->num_items; ++audio_track_index) {
+ const gsr_merged_audio_inputs *merged_audio_inputs = &self->audio_input_tracks->items[audio_track_index];
+ const bool use_amix = gsr_audio_inputs_should_use_amix(merged_audio_inputs);
+ AVCodecContext *audio_codec_context = create_audio_codec_context(self->settings.fps, self->settings.audio_codec, use_amix, self->settings.audio_bitrate);
+ if(!audio_codec_context)
+ _exit(1);
+
+ AVStream *audio_stream = NULL;
+ if(!self->settings.is_replaying) {
+ audio_stream = create_stream(self->av_format_context, audio_codec_context);
+ if(gsr_encoder_add_recording_destination(&self->encoder, audio_codec_context, self->av_format_context, audio_stream, 0) == (size_t)-1)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources");
+ }
+
+ if(audio_stream && merged_audio_inputs->track_name[0] != '\0' && !self->settings.exclude_metadata)
+ av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs->track_name, 0);
+
+ if(!open_audio(audio_codec_context, self->settings.ffmpeg_audio_opts))
+ _exit(1);
+ if(audio_stream)
+ avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context);
+
+ #if LIBAVCODEC_VERSION_MAJOR < 60
+ const int num_channels = audio_codec_context->channels;
+ #else
+ const int num_channels = audio_codec_context->ch_layout.nb_channels;
+ #endif
+
+ //audio_frame->sample_rate = audio_codec_context->sample_rate;
+
+ AVFilterContext *src_filter_ctx[GSR_MAX_AUDIO_SOURCES_PER_TRACK];
+ AVFilterGraph *graph = NULL;
+ AVFilterContext *sink = NULL;
+ if(use_amix) {
+ if(merged_audio_inputs->num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK);
+ _exit(1);
+ }
+
+ if(gsr_audio_init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs->num_items) < 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter");
+ _exit(1);
+ }
+ }
+
+ // TODO: Cleanup above
+
+ const double audio_fps = (double)audio_codec_context->sample_rate / (double)audio_codec_context->frame_size;
+ const double timeout_sec = 1000.0 / audio_fps / 1000.0;
+
+ const double audio_startup_time_seconds = self->force_no_audio_offset ? 0 : audio_codec_get_desired_delay(self->settings.audio_codec, self->settings.fps);// * ((double)audio_codec_context->frame_size / 1024.0);
+ const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec;
+
+ gsr_audio_track audio_track;
+ memset(&audio_track, 0, sizeof(audio_track));
+ snprintf(audio_track.name, sizeof(audio_track.name), "%s", merged_audio_inputs->track_name);
+ audio_track.codec_context = audio_codec_context;
+ audio_track.graph = graph;
+ audio_track.sink = sink;
+ audio_track.stream_index = audio_stream_index;
+ audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift;
+
+ int audio_track_result = GSR_ERROR_OK;
+ if(gsr_audio_inputs_has_app_audio(merged_audio_inputs)) {
+ assert(!use_amix);
+#ifdef GSR_APP_AUDIO
+ audio_track_result = gsr_audio_track_init_application_input(&audio_track, merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, self->pipewire_audio);
+#endif
+ } else {
+ audio_track_result = gsr_audio_track_init_device_inputs(&audio_track, merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix);
+ }
+
+ if(audio_track_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(audio_track_result));
+
+ if(!gsr_audio_capture_add_track(&self->audio_capture, &audio_track))
+ _exit(1);
+ ++audio_stream_index;
+
+ if(audio_codec_context->frame_size > self->audio_max_frame_size)
+ self->audio_max_frame_size = audio_codec_context->frame_size;
+ }
+
+ //av_dump_format(self->av_format_context, 0, filename, 1);
+
+ if(!self->settings.is_replaying && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) {
+ const int ret = avio_open(&self->av_format_context->pb, self->settings.filename, AVIO_FLAG_WRITE);
+ if(ret < 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", self->settings.filename, gsr_av_error_to_string(ret));
+ _exit(1);
+ }
+ }
+
+ if(!self->settings.is_replaying)
+ av_write_header(self->av_format_context, self->settings.ffmpeg_opts);
+
+ *error = GSR_ERROR_OK;
+ return self;
+}
+
+int gsr_recorder_run(gsr_recorder *self) {
+ double fps_start_time = clock_get_monotonic_seconds();
+ //double frame_timer_start = fps_start_time;
+ int fps_counter = 0;
+ int damage_fps_counter = 0;
+
+ bool paused = false;
+ self->replay_recording = false;
+
+ memset(&self->replay_recording_output, 0, sizeof(self->replay_recording_output));
+
+ gsr_replay_save_init(&self->replay_save);
+
+
+ self->force_iframe_frame = false;
+
+ gsr_recording_clock_start(self->recording_clock);
+ const double record_start_time = gsr_recording_clock_get_start_time(self->recording_clock);
+
+ if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK)
+ _exit(1);
+
+ // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video.
+ //const double update_fps = fps + 190;
+ self->should_stop_error = false;
+
+ int64_t video_pts_counter = 0;
+ int64_t video_prev_pts = 0;
+
+ bool hdr_metadata_set = false;
+ self->hdr = video_codec_is_hdr(self->settings.video_codec);
+
+
+ memset(&self->damage, 0, sizeof(self->damage));
+ if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(self->capture_sources)) {
+ if(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11) {
+ gsr_damage_init(&self->damage, self->egl, &self->capture_deps->x11_cursor, self->settings.record_cursor);
+ self->use_damage_tracking = true;
+
+ for(size_t i = 0; i < self->capture_sources->num_items; ++i) {
+ const gsr_capture_source *capture_source = &self->capture_sources->items[i];
+ switch(capture_source->type) {
+ case GSR_CAPTURE_SOURCE_TYPE_WINDOW:
+ gsr_damage_start_tracking_window(&self->damage, capture_source->window_id);
+ break;
+ case GSR_CAPTURE_SOURCE_TYPE_MONITOR:
+ case GSR_CAPTURE_SOURCE_TYPE_REGION:
+ // TODO: When capturing a region only track damage in that region
+ gsr_damage_start_tracking_monitor(&self->damage, capture_source->name);
+ break;
+ default:
+ break;
+ }
+ }
+ } else if(gsr_capture_sources_has_monitor_or_region(self->capture_sources)) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)");
+ }
+ }
+
+ while(self->running) {
+ while(gsr_window_process_event(self->window)) {
+ if(self->capture_deps->x11_cursor_display && self->settings.record_cursor)
+ gsr_cursor_on_event(&self->capture_deps->x11_cursor, gsr_window_get_event_data(self->window));
+
+ gsr_damage_on_event(&self->damage, gsr_window_get_event_data(self->window));
+ for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) {
+ gsr_video_source *video_source = &self->video_sources->items[video_source_index];
+ gsr_capture_on_event(video_source->capture, self->egl);
+ }
+ }
+
+ if(self->capture_deps->x11_cursor_display && self->settings.record_cursor)
+ gsr_cursor_tick(&self->capture_deps->x11_cursor, DefaultRootWindow(self->capture_deps->x11_cursor_display));
+
+ gsr_damage_tick(&self->damage);
+
+ self->should_stop_error = false;
+ bool damaged = false;
+
+ if(self->use_damage_tracking)
+ damaged = gsr_damage_is_damaged(&self->damage);
+
+ for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) {
+ gsr_video_source *video_source = &self->video_sources->items[video_source_index];
+ gsr_capture_tick(video_source->capture);
+
+ if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) {
+ self->running = 0;
+ break;
+ }
+
+ if(video_source->capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) {
+ assert(video_source->capture->get_window_id);
+ const Window damage_target_window = video_source->capture->get_window_id(video_source->capture);
+
+ if((int64_t)damage_target_window != video_source->capture_source->window_id) {
+ gsr_damage_stop_tracking_window(&self->damage, video_source->capture_source->window_id);
+ if(damage_target_window != 0)
+ gsr_damage_start_tracking_window(&self->damage, damage_target_window);
+ }
+
+ video_source->capture_source->window_id = damage_target_window;
+ }
+
+ if(video_source->capture->is_damaged)
+ damaged |= video_source->capture->is_damaged(video_source->capture);
+ else if(!self->use_damage_tracking)
+ damaged = true;
+ }
+
+ damaged |= gsr_plugins_is_damaged(&self->plugins);
+
+ // TODO: Readd wayland sync warning when removing this
+ if(self->settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT)
+ damaged = true;
+
+ if(damaged)
+ ++damage_fps_counter;
+
+ ++fps_counter;
+ const double time_now = clock_get_monotonic_seconds();
+ //const double frame_timer_elapsed = time_now - frame_timer_start;
+ const double elapsed = time_now - fps_start_time;
+ if (elapsed >= 1.0) {
+ if(self->settings.verbose) {
+ fprintf(stderr, "update fps: %d, damage fps: %d\n", fps_counter, damage_fps_counter);
+ }
+ fps_start_time = time_now;
+ fps_counter = 0;
+ damage_fps_counter = 0;
+ }
+
+ const double this_video_frame_time = gsr_recording_clock_get_time(self->recording_clock);
+ const int64_t expected_frames = floor((this_video_frame_time - record_start_time) / self->target_fps);
+ const int64_t num_missed_frames = expected_frames - video_pts_counter;
+
+ if(damaged && num_missed_frames >= 1 && !paused) {
+ // TODO: Dont do this if no damage?
+ self->egl->glClear(0);
+
+ gsr_damage_clear(&self->damage);
+ gsr_plugins_clear_damage(&self->plugins);
+ gsr_capture_deps_cleanup_kms_fds(self->capture_deps);
+
+ gsr_capture_deps_update_kms(self->capture_deps);
+
+ bool capture_has_synchronous_task = false;
+ for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) {
+ gsr_video_source *video_source = &self->video_sources->items[video_source_index];
+ if(video_source->capture->clear_damage)
+ video_source->capture->clear_damage(video_source->capture);
+
+ if(video_source->capture->capture_has_synchronous_task) {
+ capture_has_synchronous_task = video_source->capture->capture_has_synchronous_task(video_source->capture);
+ if(capture_has_synchronous_task) {
+ paused = true;
+ gsr_recording_clock_set_paused(self->recording_clock, true);
+ }
+ }
+ }
+
+ for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) {
+ gsr_video_source *video_source = &self->video_sources->items[video_source_index];
+ if(video_source->capture->pre_capture)
+ video_source->capture->pre_capture(video_source->capture, &video_source->metadata, self->output_color_conversion);
+ }
+
+ if(self->output_color_conversion->schedule_clear) {
+ self->output_color_conversion->schedule_clear = false;
+ gsr_color_conversion_clear(self->output_color_conversion);
+ }
+
+ for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) {
+ gsr_video_source *video_source = &self->video_sources->items[video_source_index];
+ gsr_capture_capture(video_source->capture, &video_source->metadata, self->output_color_conversion);
+ }
+
+ gsr_capture_deps_cleanup_kms_fds(self->capture_deps);
+
+ if(self->plugins.num_plugins > 0) {
+ gsr_plugins_draw(&self->plugins);
+ gsr_color_conversion_draw(&self->color_conversion, self->plugins.texture,
+ (vec2i){0, 0}, self->video_size,
+ (vec2i){0, 0}, self->video_size,
+ self->video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false);
+ }
+
+ if(capture_has_synchronous_task) {
+ paused = false;
+ gsr_recording_clock_set_paused(self->recording_clock, false);
+ }
+
+ gsr_egl_swap_buffers(self->egl);
+ gsr_video_encoder_copy_textures_to_frame(self->video_encoder, self->video_frame, self->output_color_conversion);
+
+ for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) {
+ gsr_video_source *video_source = &self->video_sources->items[video_source_index];
+ if(self->hdr && !hdr_metadata_set && !self->settings.is_replaying && add_hdr_metadata_to_video_stream(video_source->capture, self->video_stream))
+ hdr_metadata_set = true;
+ }
+
+ // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again
+ const int num_frames_to_encode = self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1;
+ for(int i = 0; i < num_frames_to_encode; ++i) {
+ if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) {
+ self->video_frame->pts = video_pts_counter + i;
+ } else {
+ self->video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE;
+ const bool same_pts = self->video_frame->pts == video_prev_pts;
+ video_prev_pts = self->video_frame->pts;
+ if(same_pts)
+ continue;
+ }
+
+ if(self->force_iframe_frame) {
+ self->video_frame->pict_type = AV_PICTURE_TYPE_I;
+ }
+
+ int ret = avcodec_send_frame(self->video_codec_context, self->video_frame);
+ if(ret == 0) {
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ gsr_encoder_receive_packets(&self->encoder, self->video_codec_context, self->video_frame->pts, GSR_VIDEO_STREAM_INDEX);
+ } else {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret));
+ }
+
+ if(self->force_iframe_frame) {
+ self->force_iframe_frame = false;
+ self->video_frame->pict_type = AV_PICTURE_TYPE_NONE;
+ }
+ }
+
+ video_pts_counter += num_missed_frames;
+ }
+
+ if(self->toggle_pause == 1 && !self->settings.is_replaying) {
+ paused = !paused;
+ gsr_recording_clock_set_paused(self->recording_clock, paused);
+ gsr_log(GSR_LOG_LEVEL_INFO, paused ? "Paused" : "Unpaused");
+ self->toggle_pause = 0;
+ }
+
+ if(self->toggle_replay_recording && !self->settings.replay_recording_directory) {
+ 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;
+ const bool new_replay_recording_state = !self->replay_recording;
+ if(new_replay_recording_state) {
+ gsr_audio_capture_lock_filter(&self->audio_capture);
+ self->num_replay_recording_items = 0;
+ const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders);
+ if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) {
+ const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts);
+ if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) {
+ char ts_filepath[PATH_MAX];
+ snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath);
+ gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath);
+ }
+
+ if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) {
+ self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id;
+ ++self->num_replay_recording_items;
+ }
+
+ for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) {
+ const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i];
+ const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts);
+ if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) {
+ self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id;
+ ++self->num_replay_recording_items;
+ }
+ }
+
+ self->replay_recording = true;
+ self->force_iframe_frame = true;
+ gsr_log(GSR_LOG_LEVEL_INFO, "Started recording");
+ if(self->callbacks.recording_started)
+ self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata);
+ } else {
+ if(self->callbacks.recording_started)
+ self->callbacks.recording_started(NULL, self->callbacks.userdata);
+ }
+ gsr_audio_capture_unlock_filter(&self->audio_capture);
+ } else if(self->replay_recording_output.av_format_context) {
+ for(size_t i = 0; i < self->num_replay_recording_items; ++i) {
+ gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]);
+ }
+ self->num_replay_recording_items = 0;
+
+ if(gsr_recording_output_stop(&self->replay_recording_output)) {
+ gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording");
+ if(self->callbacks.recording_stopped)
+ self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata);
+ } else {
+ if(self->callbacks.recording_stopped)
+ self->callbacks.recording_stopped(NULL, self->callbacks.userdata);
+ }
+
+ self->replay_recording = false;
+ self->replay_recording_filepath[0] = '\0';
+ }
+ }
+
+ bool replay_save_result = false;
+ const char *replay_save_output_filepath = NULL;
+ if(gsr_replay_save_poll(&self->replay_save, &replay_save_result, &replay_save_output_filepath)) {
+ if(self->callbacks.replay_saved)
+ 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(current_save_replay_seconds > 0)
+ current_save_replay_seconds += self->settings.keyint;
+
+ 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);
+
+ if(self->settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) {
+ pthread_mutex_lock(&self->encoder.replay_mutex);
+ gsr_replay_buffer_clear(self->encoder.replay_buffer);
+ pthread_mutex_unlock(&self->encoder.replay_mutex);
+ }
+ }
+
+ const double time_at_frame_end = gsr_recording_clock_get_time(self->recording_clock);
+ const double time_elapsed_total = time_at_frame_end - record_start_time;
+ const int64_t frames_elapsed = floor(time_elapsed_total / self->target_fps);
+ const double time_at_next_frame = (frames_elapsed + 1) * self->target_fps;
+ double time_to_next_frame = time_at_next_frame - time_elapsed_total;
+ if(time_to_next_frame > self->target_fps)
+ time_to_next_frame = self->target_fps;
+ const int64_t end_num_missed_frames = frames_elapsed - video_pts_counter;
+
+ if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0)
+ av_usleep(time_to_next_frame * 1000.0 * 1000.0);
+ else {
+ if(paused)
+ av_usleep(20.0 * 1000.0); // 20 milliseconds
+ else if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT)
+ av_usleep(2.8 * 1000.0); // 2.8 milliseconds
+ }
+ }
+
+ gsr_recorder_stop_recording(self);
+ return self->should_stop_error ? GSR_ERROR_CAPTURE_FAILED : GSR_ERROR_OK;
+}
+
+static void gsr_recorder_stop_recording(gsr_recorder *self) {
+ self->running = 0;
+
+ bool final_replay_save_result = false;
+ const char *final_replay_save_output_filepath = NULL;
+ if(gsr_replay_save_join(&self->replay_save, &final_replay_save_result, &final_replay_save_output_filepath)) {
+ if(final_replay_save_output_filepath[0] != '\0' && self->callbacks.replay_saved)
+ self->callbacks.replay_saved(final_replay_save_output_filepath, self->callbacks.userdata);
+ }
+
+ gsr_plugins_deinit(&self->plugins);
+
+ if(self->replay_recording_output.av_format_context) {
+ for(size_t i = 0; i < self->num_replay_recording_items; ++i) {
+ gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]);
+ }
+ self->num_replay_recording_items = 0;
+
+ if(gsr_recording_output_stop(&self->replay_recording_output)) {
+ gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording");
+ if(self->callbacks.recording_stopped)
+ self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata);
+ } else {
+ if(self->callbacks.recording_stopped)
+ self->callbacks.recording_stopped(NULL, self->callbacks.userdata);
+ }
+ }
+
+ gsr_audio_capture_join_threads(&self->audio_capture);
+
+ // TODO: Replace this with start_recording_create_steams
+ if(!self->settings.is_replaying && av_write_trailer(self->av_format_context) != 0) {
+ //fprintf(stderr, "Failed to write trailer\n");
+ }
+
+ if(!self->settings.is_replaying && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) {
+ avio_close(self->av_format_context->pb);
+ avformat_free_context(self->av_format_context);
+ }
+
+ gsr_damage_deinit(&self->damage);
+ gsr_color_conversion_deinit(&self->color_conversion);
+ gsr_video_encoder_destroy(self->video_encoder, self->video_codec_context);
+ gsr_encoder_deinit(&self->encoder);
+ gsr_video_sources_deinit(self->video_sources);
+#ifdef GSR_APP_AUDIO
+ gsr_pipewire_audio_deinit(self->pipewire_audio);
+#endif
+ gsr_capture_deps_deinit(self->capture_deps);
+
+ if(!self->settings.is_replaying && self->callbacks.recording_stopped)
+ self->callbacks.recording_stopped(self->settings.filename, self->callbacks.userdata);
+
+ if(self->windowing->display) {
+ // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this???
+ //XCloseDisplay(dpy);
+ }
+
+ //gsr_egl_unload(self->egl);
+ //gsr_window_destroy(&window);
+
+ //av_frame_free(&self->video_frame);
+}
+
+void gsr_recorder_destroy(gsr_recorder *self) {
+ if(!self)
+ return;
+
+ gsr_recording_clock_destroy(self->recording_clock);
+ free(self);
+}
+
+void gsr_recorder_stop(gsr_recorder *self) {
+ self->running = 0;
+}
+
+void gsr_recorder_toggle_pause(gsr_recorder *self) {
+ self->toggle_pause = 1;
+}
+
+void gsr_recorder_toggle_replay_recording(gsr_recorder *self) {
+ self->toggle_replay_recording = 1;
+}
+
+void gsr_recorder_save_replay(gsr_recorder *self, int seconds) {
+ self->save_replay_seconds = seconds;
+}