diff options
| -rw-r--r-- | include/recorder/recorder.h | 54 | ||||
| -rw-r--r-- | meson.build | 19 | ||||
| -rw-r--r-- | src/cli/main.c | 562 | ||||
| -rw-r--r-- | src/main.cpp | 1158 | ||||
| -rw-r--r-- | src/recorder/recorder.c | 826 |
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 = ¶ms->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; +} |
