diff options
Diffstat (limited to 'src/main.cpp')
| -rw-r--r-- | src/main.cpp | 981 |
1 files changed, 773 insertions, 208 deletions
diff --git a/src/main.cpp b/src/main.cpp index d678613..075e5dd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ extern "C" { #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" @@ -27,6 +28,7 @@ extern "C" { #include "../include/image_writer.h" #include "../include/args_parser.h" #include "../include/plugins.h" +#include "../kms/client/kms_client.h" } #include <assert.h> @@ -978,33 +980,6 @@ static void save_replay_30_minutes_handler(int) { save_replay_seconds = 60*30; } -static bool is_hex_num(char c) { - return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); -} - -static bool contains_non_hex_number(const char *str) { - bool hex_start = false; - size_t len = strlen(str); - if(len >= 2 && memcmp(str, "0x", 2) == 0) { - str += 2; - len -= 2; - hex_start = true; - } - - bool is_hex = false; - for(size_t i = 0; i < len; ++i) { - char c = str[i]; - if(c == '\0') - return false; - if(!is_hex_num(c)) - return true; - if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) - is_hex = true; - } - - return is_hex && !hex_start; -} - static std::string get_date_str() { char str[128]; time_t now = time(NULL); @@ -1186,7 +1161,39 @@ struct RecordingStartResult { std::vector<RecordingStartAudio> audio_inputs; }; -static RecordingStartResult start_recording_create_streams(const char *filename, const char *container_format, AVCodecContext *video_codec_context, const std::vector<AudioTrack> &audio_tracks, bool hdr, gsr_capture *capture) { +typedef enum { + VVEC2I_TYPE_PIXELS, + VVEC2I_TYPE_SCALAR +} vvec2i_type; + +typedef struct { + int x, y; + vvec2i_type x_type; + vvec2i_type y_type; +} vvec2i; + +struct CaptureSource { + std::string name; + CaptureSourceType type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + gsr_capture_alignment halign = GSR_CAPTURE_ALIGN_CENTER; + gsr_capture_alignment valign = GSR_CAPTURE_ALIGN_CENTER; + gsr_capture_v4l2_pixfmt v4l2_pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; + uint32_t flip = GSR_FLIP_NONE; + vvec2i pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + vvec2i size = {100, 100, VVEC2I_TYPE_SCALAR, VVEC2I_TYPE_SCALAR}; + vec2i region_pos = {0, 0}; + vec2i region_size = {0, 0}; + bool region_set = false; + int64_t window_id = 0; +}; + +struct VideoSource { + gsr_capture *capture; + gsr_capture_metadata metadata; + CaptureSource *capture_source; +}; + +static RecordingStartResult start_recording_create_streams(const char *filename, const char *container_format, AVCodecContext *video_codec_context, const std::vector<AudioTrack> &audio_tracks, bool hdr, std::vector<VideoSource> &video_sources) { AVFormatContext *av_format_context; avformat_alloc_output_context2(&av_format_context, nullptr, container_format, filename); @@ -1222,8 +1229,10 @@ static RecordingStartResult start_recording_create_streams(const char *filename, return result; } - if(hdr) - add_hdr_metadata_to_video_stream(capture, video_stream); + for(VideoSource &video_source : video_sources) { + if(hdr && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) + break; + } result.av_format_context = av_format_context; result.video_stream = video_stream; @@ -1273,7 +1282,7 @@ struct AudioPtsOffset { int stream_index = 0; }; -static void save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const std::vector<AudioTrack> &audio_tracks, gsr_replay_buffer *replay_buffer, std::string output_dir, const char *container_format, const std::string &file_extension, bool date_folders, bool hdr, gsr_capture *capture, int current_save_replay_seconds) { +static void save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const std::vector<AudioTrack> &audio_tracks, gsr_replay_buffer *replay_buffer, std::string output_dir, const char *container_format, const std::string &file_extension, bool date_folders, bool hdr, std::vector<VideoSource> &video_sources, int current_save_replay_seconds) { if(save_replay_thread.valid()) return; @@ -1302,7 +1311,7 @@ static void save_replay_async(AVCodecContext *video_codec_context, int video_str } std::string output_filepath = create_new_recording_filepath_from_timestamp(output_dir, "Replay", file_extension, date_folders); - RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), container_format, video_codec_context, audio_tracks, hdr, capture); + RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), container_format, video_codec_context, audio_tracks, hdr, video_sources); if(!recording_start_result.av_format_context) return; @@ -1395,9 +1404,13 @@ static void split_string(const std::string &str, char delimiter, std::function<b } } -static bool string_starts_with(const std::string &str, const char *substr) { +static bool string_starts_with(const char *str, size_t str_size, const char *substr) { int len = strlen(substr); - return (int)str.size() >= len && memcmp(str.data(), substr, len) == 0; + return (int)str_size >= len && memcmp(str, substr, len) == 0; +} + +static bool string_starts_with(const std::string &str, const char *substr) { + return string_starts_with(str.data(), str.size(), substr); } static bool string_ends_with(const char *str, const char *substr) { @@ -1844,6 +1857,20 @@ static void output_monitor_info(const gsr_monitor *monitor, void *userdata) { ++options->num_monitors; } +static void camera_query_callback(const char *path, gsr_capture_v4l2_supported_pixfmts supported_pixfmts, vec2i size, void *userdata) { + (void)userdata; + + char pixfmt_str[32]; + if(supported_pixfmts.yuyv && supported_pixfmts.mjpeg) + snprintf(pixfmt_str, sizeof(pixfmt_str), "yuyv,mjpeg"); + else if(supported_pixfmts.yuyv) + snprintf(pixfmt_str, sizeof(pixfmt_str), "yuyv"); + else if(supported_pixfmts.mjpeg) + snprintf(pixfmt_str, sizeof(pixfmt_str), "mjpeg"); + + printf("%s|%dx%d|%s\n", path, size.x, size.y, pixfmt_str); +} + static void list_supported_capture_options(const gsr_window *window, const char *card_path, bool list_monitors) { const bool wayland = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND; if(!wayland) { @@ -1863,6 +1890,8 @@ static void list_supported_capture_options(const gsr_window *window, const char if(options.num_monitors > 0) puts("region"); + gsr_capture_v4l2_list_devices(camera_query_callback, NULL); + #ifdef GSR_PORTAL // Desktop portal capture on x11 doesn't seem to be hardware accelerated if(!wayland) @@ -2001,6 +2030,14 @@ static void list_application_audio_command(void *userdata) { _exit(0); } +static void list_v4l2_devices(void *userdata) { + (void)userdata; + gsr_capture_v4l2_list_devices(camera_query_callback, NULL); + + fflush(stdout); + _exit(0); +} + // |card_path| can be NULL. If not NULL then |vendor| has to be valid static void list_capture_options_command(const char *card_path, void *userdata) { (void)userdata; @@ -2068,23 +2105,23 @@ static std::string validate_monitor_get_valid(const gsr_egl *egl, const char* wi const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; const bool capture_use_drm = monitor_capture_use_drm(egl->window, egl->gpu_info.vendor); - std::string window_result = window; - if(strcmp(window_result.c_str(), "screen") == 0) { + std::string capture_source_result = window; + if(strcmp(capture_source_result.c_str(), "screen") == 0) { FirstOutputCallback data; data.output_name = NULL; for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_first_output_callback, &data); if(data.output_name) { - window_result = data.output_name; + capture_source_result = data.output_name; free(data.output_name); } else { fprintf(stderr, "gsr error: no usable output found\n"); _exit(51); } - } else if(capture_use_drm || (strcmp(window_result.c_str(), "screen-direct") != 0 && strcmp(window_result.c_str(), "screen-direct-force") != 0)) { + } else if(capture_use_drm || (strcmp(capture_source_result.c_str(), "screen-direct") != 0 && strcmp(capture_source_result.c_str(), "screen-direct-force") != 0)) { gsr_monitor gmon; - if(!get_monitor_by_name(egl, connection_type, window_result.c_str(), &gmon)) { - fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", window_result.c_str()); + if(!get_monitor_by_name(egl, connection_type, capture_source_result.c_str(), &gmon)) { + fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", capture_source_result.c_str()); fprintf(stderr, " \"screen\"\n"); if(!capture_use_drm) fprintf(stderr, " \"screen-direct\"\n"); @@ -2095,7 +2132,7 @@ static std::string validate_monitor_get_valid(const gsr_egl *egl, const char* wi _exit(51); } } - return window_result; + return capture_source_result; } static std::string get_monitor_by_region_center(const gsr_egl *egl, vec2i region_position, vec2i region_size, vec2i *monitor_pos, vec2i *monitor_size) { @@ -2120,46 +2157,66 @@ static std::string get_monitor_by_region_center(const gsr_egl *egl, vec2i region return result; } -static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_egl *egl, bool prefer_ximage) { +static gsr_kms_client kms_client; +static bool kms_client_initialized = false; +static gsr_kms_response kms_response; + +static gsr_cursor x11_cursor; +static Display *x11_cursor_display = NULL; + +static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_egl *egl, const CaptureSource &capture_source, bool prefer_ximage) { if(gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11 && prefer_ximage) { gsr_capture_ximage_params ximage_params; + memset(&ximage_params, 0, sizeof(ximage_params)); ximage_params.egl = egl; - ximage_params.display_to_capture = arg_parser.window; + ximage_params.cursor = &x11_cursor; + ximage_params.display_to_capture = capture_source.name.c_str(); ximage_params.record_cursor = arg_parser.record_cursor; ximage_params.output_resolution = arg_parser.output_resolution; - ximage_params.region_size = arg_parser.region_size; - ximage_params.region_position = arg_parser.region_position; + ximage_params.region_size = capture_source.region_size; + ximage_params.region_position = capture_source.region_pos; return gsr_capture_ximage_create(&ximage_params); } if(monitor_capture_use_drm(egl->window, egl->gpu_info.vendor)) { + if(!kms_client_initialized) { + kms_client_initialized = true; + const int kms_init_res = gsr_kms_client_init(&kms_client, egl->card_path); + if(kms_init_res != 0) + _exit(kms_init_res < 0 ? 1 : kms_init_res); + } + gsr_capture_kms_params kms_params; + memset(&kms_params, 0, sizeof(kms_params)); kms_params.egl = egl; - kms_params.display_to_capture = arg_parser.window; + kms_params.x11_cursor = &x11_cursor; + kms_params.kms_response = &kms_response; + kms_params.display_to_capture = capture_source.name.c_str(); kms_params.record_cursor = arg_parser.record_cursor; kms_params.hdr = video_codec_is_hdr(arg_parser.video_codec); kms_params.fps = arg_parser.fps; kms_params.output_resolution = arg_parser.output_resolution; - kms_params.region_size = arg_parser.region_size; - kms_params.region_position = arg_parser.region_position; + kms_params.region_size = capture_source.region_size; + kms_params.region_position = capture_source.region_pos; return gsr_capture_kms_create(&kms_params); } else { - const char *capture_target = arg_parser.window; - const bool direct_capture = strcmp(arg_parser.window, "screen-direct") == 0 || strcmp(arg_parser.window, "screen-direct-force") == 0; + const char *capture_source_real = capture_source.name.c_str(); + const bool direct_capture = strcmp(capture_source.name.c_str(), "screen-direct") == 0 || strcmp(capture_source.name.c_str(), "screen-direct-force") == 0; if(direct_capture) { - capture_target = "screen"; - fprintf(stderr, "gsr warning: %s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.\n", arg_parser.window); + capture_source_real = "screen"; + fprintf(stderr, "gsr warning: %s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.\n", capture_source.name.c_str()); } gsr_capture_nvfbc_params nvfbc_params; + memset(&nvfbc_params, 0, sizeof(nvfbc_params)); nvfbc_params.egl = egl; - nvfbc_params.display_to_capture = capture_target; + nvfbc_params.display_to_capture = capture_source_real; nvfbc_params.fps = arg_parser.fps; nvfbc_params.direct_capture = direct_capture; nvfbc_params.record_cursor = arg_parser.record_cursor; nvfbc_params.output_resolution = arg_parser.output_resolution; - nvfbc_params.region_size = arg_parser.region_size; - nvfbc_params.region_position = arg_parser.region_position; + nvfbc_params.region_size = capture_source.region_size; + nvfbc_params.region_position = capture_source.region_pos; return gsr_capture_nvfbc_create(&nvfbc_params); } } @@ -2190,13 +2247,12 @@ static std::string region_get_data(gsr_egl *egl, vec2i *region_size, vec2i *regi return window; } -static gsr_capture* create_capture_impl(args_parser &arg_parser, gsr_egl *egl, bool prefer_ximage) { - Window src_window_id = None; +static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl *egl, CaptureSource &capture_source, bool prefer_ximage) { bool follow_focused = false; const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; gsr_capture *capture = nullptr; - if(strcmp(arg_parser.window, "focused") == 0) { + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { if(wayland) { fprintf(stderr, "gsr error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland\n"); _exit(2); @@ -2209,7 +2265,7 @@ static gsr_capture* create_capture_impl(args_parser &arg_parser, gsr_egl *egl, b } follow_focused = true; - } else if(strcmp(arg_parser.window, "portal") == 0) { + } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_PORTAL) { #ifdef GSR_PORTAL // Desktop portal capture on x11 doesn't seem to be hardware accelerated if(!wayland) { @@ -2218,6 +2274,7 @@ static gsr_capture* create_capture_impl(args_parser &arg_parser, gsr_egl *egl, b } gsr_capture_portal_params portal_params; + memset(&portal_params, 0, sizeof(portal_params)); portal_params.egl = egl; portal_params.record_cursor = arg_parser.record_cursor; portal_params.restore_portal_session = arg_parser.restore_portal_session; @@ -2230,16 +2287,25 @@ static gsr_capture* create_capture_impl(args_parser &arg_parser, gsr_egl *egl, b fprintf(stderr, "gsr error: option '-w portal' used but GPU Screen Recorder was compiled without desktop portal support. Please recompile GPU Screen recorder with the -Dportal=true option\n"); _exit(2); #endif - } else if(strcmp(arg_parser.window, "region") == 0) { - const std::string window = region_get_data(egl, &arg_parser.region_size, &arg_parser.region_position); - snprintf(arg_parser.window, sizeof(arg_parser.window), "%s", window.c_str()); - capture = create_monitor_capture(arg_parser, egl, prefer_ximage); + } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) { + capture_source.name = region_get_data(egl, &capture_source.region_size, &capture_source.region_pos); + capture = create_monitor_capture(arg_parser, egl, capture_source, prefer_ximage); if(!capture) _exit(1); - } else if(contains_non_hex_number(arg_parser.window)) { - const std::string window = validate_monitor_get_valid(egl, arg_parser.window); - snprintf(arg_parser.window, sizeof(arg_parser.window), "%s", window.c_str()); - capture = create_monitor_capture(arg_parser, egl, prefer_ximage); + } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { + capture_source.name = validate_monitor_get_valid(egl, capture_source.name.c_str()); + capture = create_monitor_capture(arg_parser, egl, capture_source, prefer_ximage); + if(!capture) + _exit(1); + } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_V4L2) { + gsr_capture_v4l2_params v4l2_params; + memset(&v4l2_params, 0, sizeof(v4l2_params)); + v4l2_params.egl = egl; + v4l2_params.output_resolution = {0, 0}; + v4l2_params.device_path = capture_source.name.c_str(); + v4l2_params.pixfmt = capture_source.v4l2_pixfmt; + v4l2_params.fps = arg_parser.fps; + capture = gsr_capture_v4l2_create(&v4l2_params); if(!capture) _exit(1); } else { @@ -2247,20 +2313,14 @@ static gsr_capture* create_capture_impl(args_parser &arg_parser, gsr_egl *egl, b fprintf(stderr, "gsr error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland or use -w portal option which supports window capture if your wayland compositor supports window capture\n"); _exit(2); } - - errno = 0; - src_window_id = strtol(arg_parser.window, nullptr, 0); - if(src_window_id == None || errno == EINVAL) { - fprintf(stderr, "gsr error: invalid window number %s\n", arg_parser.window); - args_parser_print_usage(); - _exit(1); - } } if(!capture) { gsr_capture_xcomposite_params xcomposite_params; + memset(&xcomposite_params, 0, sizeof(xcomposite_params)); xcomposite_params.egl = egl; - xcomposite_params.window = src_window_id; + xcomposite_params.cursor = &x11_cursor; + xcomposite_params.window = capture_source.window_id; xcomposite_params.follow_focused = follow_focused; xcomposite_params.record_cursor = arg_parser.record_cursor; xcomposite_params.output_resolution = arg_parser.output_resolution; @@ -2272,9 +2332,9 @@ static gsr_capture* create_capture_impl(args_parser &arg_parser, gsr_egl *egl, b return capture; } -static gsr_color_range image_format_to_color_range(gsr_image_format image_format) { +static gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality) { switch(image_format) { - case GSR_IMAGE_FORMAT_JPEG: return GSR_COLOR_RANGE_LIMITED; + case GSR_IMAGE_FORMAT_JPEG: return image_quality >= 91 ? GSR_COLOR_RANGE_FULL : GSR_COLOR_RANGE_LIMITED; case GSR_IMAGE_FORMAT_PNG: return GSR_COLOR_RANGE_FULL; } assert(false); @@ -2296,32 +2356,110 @@ static int video_quality_to_image_quality_value(gsr_video_quality video_quality) return 90; } -// TODO: 10-bit and hdr. -static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_image_format image_format) { - const gsr_color_range color_range = image_format_to_color_range(image_format); - const int fps = 60; - const bool prefer_ximage = true; - gsr_capture *capture = create_capture_impl(arg_parser, egl, prefer_ximage); +static bool any_video_sources_uses_external_image(std::vector<VideoSource> &video_sources) { + for(VideoSource &video_source : video_sources) { + if(gsr_capture_uses_external_image(video_source.capture)) + return true; + } + return false; +} + +static std::vector<VideoSource> create_video_sources(const args_parser &arg_parser, gsr_egl *egl, bool prefer_ximage, std::vector<CaptureSource> &capture_sources, vec2i &video_size) { + std::vector<VideoSource> video_sources; + video_sources.reserve(capture_sources.size()); + + for(CaptureSource &capture_source : capture_sources) { + gsr_capture_metadata capture_metadata; + memset(&capture_metadata, 0, sizeof(capture_metadata)); + capture_metadata.fps = arg_parser.fps; + capture_metadata.halign = capture_source.halign; + capture_metadata.valign = capture_source.valign; + capture_metadata.flip = (gsr_flip)capture_source.flip; + video_sources.push_back(VideoSource{create_capture_impl(arg_parser, egl, capture_source, prefer_ximage), capture_metadata, &capture_source}); + } + + for(VideoSource &video_source : video_sources) { + int capture_result = gsr_capture_start(video_source.capture, &video_source.metadata); + if(capture_result != 0) { + fprintf(stderr, "gsr error: gsr_capture_start failed\n"); + _exit(capture_result); + } + } + + video_size = {0, 0}; + for(const VideoSource &video_source : video_sources) { + video_size.x = std::max(video_size.x, video_source.metadata.video_size.x); + video_size.y = std::max(video_size.y, video_source.metadata.video_size.y); + } + + for(VideoSource &video_source : video_sources) { + video_source.metadata.video_size = video_size; + } + + return video_sources; +} + +static void video_sources_update_with_real_video_size(std::vector<CaptureSource> &capture_sources, std::vector<VideoSource> &video_sources, vec2i video_size) { + assert(capture_sources.size() == video_sources.size()); + for(size_t i = 0; i < capture_sources.size(); ++i) { + CaptureSource &capture_source = capture_sources[i]; + VideoSource &video_source = video_sources[i]; + + video_source.metadata.recording_size = video_source.metadata.video_size; + // TODO: What if this updated resolution is above max resolution? + video_source.metadata.video_size = video_size; + + if(capture_source.pos.x != 0 || capture_source.pos.y != 0) { + video_source.metadata.position.x = capture_source.pos.x; + video_source.metadata.position.y = capture_source.pos.y; - gsr_capture_metadata capture_metadata; - capture_metadata.video_width = 0; - capture_metadata.video_height = 0; - capture_metadata.recording_width = 0; - capture_metadata.recording_height = 0; - capture_metadata.fps = fps; + if(capture_source.pos.x_type == VVEC2I_TYPE_SCALAR) + video_source.metadata.position.x = video_source.metadata.video_size.x * ((double)video_source.metadata.position.x / 100.0); - int capture_result = gsr_capture_start(capture, &capture_metadata); - if(capture_result != 0) { - fprintf(stderr, "gsr error: capture_image_to_file_wayland: gsr_capture_start failed\n"); - _exit(capture_result); + if(capture_source.pos.y_type == VVEC2I_TYPE_SCALAR) + video_source.metadata.position.y = video_source.metadata.video_size.y * ((double)video_source.metadata.position.y / 100.0); + } + + if(capture_source.size.x != 0 || capture_source.size.y != 0) { + video_source.metadata.recording_size.x = capture_source.size.x; + video_source.metadata.recording_size.y = capture_source.size.y; + + if(capture_source.size.x_type == VVEC2I_TYPE_SCALAR) + video_source.metadata.recording_size.x = video_source.metadata.video_size.x * ((double)video_source.metadata.recording_size.x / 100.0); + + if(capture_source.size.y_type == VVEC2I_TYPE_SCALAR) + video_source.metadata.recording_size.y = video_source.metadata.video_size.y * ((double)video_source.metadata.recording_size.y / 100.0); + } + } +} + +static void gsr_capture_kms_cleanup_kms_fds() { + for(int i = 0; i < kms_response.num_items; ++i) { + for(int j = 0; j < kms_response.items[i].num_dma_bufs; ++j) { + gsr_kms_response_dma_buf *dma_buf = &kms_response.items[i].dma_buf[j]; + if(dma_buf->fd > 0) { + close(dma_buf->fd); + dma_buf->fd = -1; + } + } + kms_response.items[i].num_dma_bufs = 0; } + kms_response.num_items = 0; +} + +// TODO: 10-bit and hdr. +static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_window *window, gsr_image_format image_format, std::vector<CaptureSource> &capture_sources) { + const int image_quality = video_quality_to_image_quality_value(arg_parser.video_quality); + const gsr_color_range color_range = image_format_to_color_range(image_format, image_quality); + arg_parser.fps = 60; // We want to capture an image as soon as possible - capture_metadata.recording_width = capture_metadata.video_width; - capture_metadata.recording_height = capture_metadata.video_height; + vec2i video_size = {0, 0}; + std::vector<VideoSource> video_sources = create_video_sources(arg_parser, egl, true, capture_sources, video_size); + video_sources_update_with_real_video_size(capture_sources, video_sources, video_size); gsr_image_writer image_writer; - if(!gsr_image_writer_init_opengl(&image_writer, egl, capture_metadata.video_width, capture_metadata.video_height)) { - fprintf(stderr, "gsr error: capture_image_to_file_wayland: gsr_image_write_gl_init failed\n"); + if(!gsr_image_writer_init_opengl(&image_writer, egl, video_size.x, video_size.y)) { + fprintf(stderr, "gsr error: capture_image_to_file: gsr_image_write_gl_init failed\n"); _exit(1); } @@ -2329,16 +2467,16 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_ima memset(&color_conversion_params, 0, sizeof(color_conversion_params)); color_conversion_params.color_range = color_range; color_conversion_params.egl = egl; - color_conversion_params.load_external_image_shader = gsr_capture_uses_external_image(capture); + color_conversion_params.load_external_image_shader = any_video_sources_uses_external_image(video_sources); color_conversion_params.destination_textures[0] = image_writer.texture; - color_conversion_params.destination_textures_size[0] = { capture_metadata.video_width, capture_metadata.video_height }; + color_conversion_params.destination_textures_size[0] = video_size; color_conversion_params.num_destination_textures = 1; color_conversion_params.destination_color = GSR_DESTINATION_COLOR_RGB8; gsr_color_conversion color_conversion; if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { - fprintf(stderr, "gsr error: capture_image_to_file_wayland: failed to create color conversion\n"); + fprintf(stderr, "gsr error: capture_image_to_file: failed to create color conversion\n"); _exit(1); } @@ -2348,28 +2486,65 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_ima egl->glClear(0); while(running) { - should_stop_error = false; - if(gsr_capture_should_stop(capture, &should_stop_error)) { - running = 0; - break; + while(gsr_window_process_event(window)) { + if(x11_cursor_display && arg_parser.record_cursor) + gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); + + for(VideoSource &video_source : video_sources) { + gsr_capture_on_event(video_source.capture, egl); + } + } + + if(x11_cursor_display && arg_parser.record_cursor) + gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); + + gsr_capture_kms_cleanup_kms_fds(); + + if(kms_client_initialized) { + if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) + fprintf(stderr, "gsr error: failed to get kms, error: %d (%s)\n", kms_response.result, kms_response.err_msg); + } + + bool all_sources_captured = true; + for(VideoSource &video_source : video_sources) { + should_stop_error = false; + if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { + running = 0; + break; + } + } + + for(VideoSource &video_source : video_sources) { + if(video_source.capture->pre_capture) + video_source.capture->pre_capture(video_source.capture, &video_source.metadata, &color_conversion); + } + + if(color_conversion.schedule_clear) { + color_conversion.schedule_clear = false; + gsr_color_conversion_clear(&color_conversion); } - // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. - // Desktop portal wont refresh the image until there is an update. - // TODO: Find out if there is a way to force update desktop portal image. - // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. - if(gsr_capture_capture(capture, &capture_metadata, &color_conversion) == 0) + for(VideoSource &video_source : video_sources) { + // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. + // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. + if(gsr_capture_capture(video_source.capture, &video_source.metadata, &color_conversion) != 0) + all_sources_captured = false; + } + + gsr_capture_kms_cleanup_kms_fds(); + + if(all_sources_captured) break; - usleep(30 * 1000); // 30 ms + if(running) + usleep(30 * 1000); // 30 ms } gsr_egl_swap_buffers(egl); if(!should_stop_error) { - const int image_quality = video_quality_to_image_quality_value(arg_parser.video_quality); if(!gsr_image_writer_write_to_file(&image_writer, arg_parser.filename, image_format, image_quality)) { - fprintf(stderr, "gsr error: capture_image_to_file_wayland: failed to write opengl texture to image output file %s\n", arg_parser.filename); + fprintf(stderr, "gsr error: capture_image_to_file: failed to write opengl texture to image output file %s\n", arg_parser.filename); _exit(1); } @@ -2378,7 +2553,9 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_ima } gsr_image_writer_deinit(&image_writer); - gsr_capture_destroy(capture); + for(VideoSource &video_source : video_sources) { + gsr_capture_destroy(video_source.capture); + } _exit(should_stop_error ? 3 : 0); } @@ -2469,6 +2646,263 @@ static std::vector<MergedAudioInputs> parse_audio_inputs(const AudioDevices &aud return requested_audio_inputs; } +static bool is_hex_num(char c) { + return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); +} + +static bool contains_non_hex_number(const char *str) { + bool hex_start = false; + size_t len = strlen(str); + if(len >= 2 && memcmp(str, "0x", 2) == 0) { + str += 2; + len -= 2; + hex_start = true; + } + + bool is_hex = false; + for(size_t i = 0; i < len; ++i) { + char c = str[i]; + if(c == '\0') + return false; + if(!is_hex_num(c)) + return true; + if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) + is_hex = true; + } + + return is_hex && !hex_start; +} + +template <typename T> +static bool string_to_int(const char *str, size_t len, T *number) { + char number_str[32]; + snprintf(number_str, sizeof(number_str), "%.*s", (int)len, str); + + errno = 0; + *number = strtol(number_str, NULL, 0); + return errno == 0; +} + +static void capture_source_type_from_string(const char *capture_source_str, size_t size, CaptureSource &capture_source) { + char capture_source_str_n[64]; + snprintf(capture_source_str_n, sizeof(capture_source_str_n), "%.*s", (int)size, capture_source_str); + + if(size == 7 && memcmp(capture_source_str_n, "focused", 7) == 0) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW; + } else if(size == 6 && memcmp(capture_source_str_n, "portal", 6) == 0) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_PORTAL; + } else if(size == 6 && memcmp(capture_source_str_n, "region", 6) == 0) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_REGION; + } else if(size >= 10 && memcmp(capture_source_str_n, "/dev/video", 10) == 0) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + } else if(sscanf(capture_source_str_n, "%dx%d+%d+%d", &capture_source.region_size.x, &capture_source.region_size.y, &capture_source.region_pos.x, &capture_source.region_pos.y) == 4) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_REGION; + capture_source.region_set = true; + } else if(contains_non_hex_number(capture_source_str_n)) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + } else { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + } +} + +static bool string_to_capture_alignment(const char *str, size_t len, gsr_capture_alignment *alignment) { + if(len == 5 && memcmp(str, "start", 5) == 0) { + *alignment = GSR_CAPTURE_ALIGN_START; + return true; + } else if(len == 6 && memcmp(str, "center", 6) == 0) { + *alignment = GSR_CAPTURE_ALIGN_CENTER; + return true; + } else if(len == 3 && memcmp(str, "end", 3) == 0) { + *alignment = GSR_CAPTURE_ALIGN_END; + return true; + } else { + return false; + } +} + +static bool string_to_v4l2_pixfmt(const char *str, size_t len, gsr_capture_v4l2_pixfmt *pixfmt) { + if(len == 4 && memcmp(str, "auto", 4) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; + return true; + } else if(len == 4 && memcmp(str, "yuyv", 4) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; + return true; + } else if(len == 5 && memcmp(str, "mjpeg", 5) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_MJPEG; + return true; + } else { + return false; + } +} + +static bool string_to_bool(const char *str, size_t len, bool *value) { + if(len == 4 && memcmp(str, "true", 4) == 0) { + *value = true; + return true; + } else if(len == 5 && memcmp(str, "false", 5) == 0) { + *value = false; + return true; + } else { + return false; + } +} + +static void parse_capture_source_options(const std::string &capture_source_str, CaptureSource &capture_source) { + bool is_first_column = true; + + split_string(capture_source_str, ';', [&](const char *sub, size_t size) { + if(size == 0) + return true; + + // First column contains the capture target + if(is_first_column) { + is_first_column = false; + return true; + } + + if(string_starts_with(sub, size, "x=")) { + capture_source.pos.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!string_to_int(sub, size, &capture_source.pos.x)) { + fprintf(stderr, "gsr error: invalid capture target value for option x: \"%.*s\", expected a number\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "y=")) { + capture_source.pos.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!string_to_int(sub, size, &capture_source.pos.y)) { + fprintf(stderr, "gsr error: invalid capture target value for option y: \"%.*s\", expected a number\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "width=")) { + capture_source.size.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 6; + size -= 6; + if(!string_to_int(sub, size, &capture_source.size.x)) { + fprintf(stderr, "gsr error: invalid capture target value for option width: \"%.*s\", expected a number\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "height=")) { + capture_source.size.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 7; + size -= 7; + if(!string_to_int(sub, size, &capture_source.size.y)) { + fprintf(stderr, "gsr error: invalid capture target value for option height: \"%.*s\", expected a number\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "halign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source.halign)) { + fprintf(stderr, "gsr error: invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "valign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source.valign)) { + fprintf(stderr, "gsr error: invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "pixfmt=")) { + sub += 7; + size -= 7; + if(!string_to_v4l2_pixfmt(sub, size, &capture_source.v4l2_pixfmt)) { + fprintf(stderr, "gsr error: invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"\n", (int)size, sub); + _exit(1); + } + } else if(string_starts_with(sub, size, "hflip=")) { + sub += 6; + size -= 6; + bool hflip = false; + if(!string_to_bool(sub, size, &hflip)) { + fprintf(stderr, "gsr error: invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"\n", (int)size, sub); + _exit(1); + } + + if(hflip) + capture_source.flip |= GSR_FLIP_HORIZONTAL; + } else if(string_starts_with(sub, size, "vflip=")) { + sub += 6; + size -= 6; + bool vflip = false; + if(!string_to_bool(sub, size, &vflip)) { + fprintf(stderr, "gsr error: invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"\n", (int)size, sub); + _exit(1); + } + + if(vflip) + capture_source.flip |= GSR_FLIP_VERTICAL; + } else { + fprintf(stderr, "gsr error: invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip or vflip\n", (int)size, sub); + _exit(1); + } + + return true; + }); +} + +static std::vector<CaptureSource> parse_capture_source_arg(const char *capture_source_arg, const args_parser &arg_parser) { + std::vector<CaptureSource> requested_capture_sources; + + split_string(capture_source_arg, '|', [&](const char *sub, size_t size) { + if(size == 0) + return true; + + const char *substr_start = sub; + size_t capture_source_size = size; + const char *capture_source_end = (const char*)memchr(sub, ';', size); + if(capture_source_end) + capture_source_size = capture_source_end - sub; + + CaptureSource capture_source; + capture_source.region_pos = arg_parser.region_position; + capture_source.region_size = arg_parser.region_size; + + if(string_starts_with(sub, capture_source_size, "monitor:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + sub += 8; + capture_source_size -= 8; + } else if(string_starts_with(sub, capture_source_size, "window:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + sub += 7; + capture_source_size -= 7; + } else if(string_starts_with(sub, capture_source_size, "v4l2:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + sub += 5; + capture_source_size -= 5; + } else { + capture_source_type_from_string(sub, capture_source_size, capture_source); + } + + capture_source.name.assign(sub, capture_source_size); + + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { + if(!string_to_int(capture_source.name.c_str(), capture_source.name.size(), &capture_source.window_id)) { + fprintf(stderr, "gsr error: invalid window number %s\n", capture_source.name.c_str()); + args_parser_print_usage(); + _exit(1); + } + } + + /* We want good default values for v4l2 (webcam) capture, by setting webcam at bottom right, offset by -10%,-10% pixels and at size 30%,30% */ + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_V4L2 && !requested_capture_sources.empty()) { + capture_source.halign = GSR_CAPTURE_ALIGN_START; + capture_source.valign = GSR_CAPTURE_ALIGN_END; + capture_source.pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + capture_source.size = {30, 30, VVEC2I_TYPE_SCALAR, VVEC2I_TYPE_SCALAR}; + } + + parse_capture_source_options(std::string(substr_start, size), capture_source); + requested_capture_sources.push_back(capture_source); + return true; + }); + + return requested_capture_sources; +} + static bool audio_inputs_has_app_audio(const std::vector<AudioInput> &audio_inputs) { for(const auto &audio_input : audio_inputs) { if(audio_input.type == AudioInputType::APPLICATION) @@ -2818,27 +3252,26 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b } /* Returns -1 if none is available */ -static gsr_video_codec select_appropriate_video_codec_automatically(gsr_capture_metadata capture_metadata, const gsr_supported_video_codecs *supported_video_codecs) { - const vec2i capture_size = {capture_metadata.video_width, capture_metadata.video_height}; - if(supported_video_codecs->h264.supported && codec_supports_resolution(supported_video_codecs->h264.max_resolution, capture_size)) { +static gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs) { + if(supported_video_codecs->h264.supported && codec_supports_resolution(supported_video_codecs->h264.max_resolution, video_size)) { fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n"); return GSR_VIDEO_CODEC_H264; - } else if(supported_video_codecs->hevc.supported && codec_supports_resolution(supported_video_codecs->hevc.max_resolution, capture_size)) { + } else if(supported_video_codecs->hevc.supported && codec_supports_resolution(supported_video_codecs->hevc.max_resolution, video_size)) { fprintf(stderr, "gsr info: using hevc encoder because a codec was not specified and h264 supported max resolution (%dx%d) is less than capture resolution (%dx%d)\n", supported_video_codecs->h264.max_resolution.x, supported_video_codecs->h264.max_resolution.y, - capture_size.x, capture_size.y); + video_size.x, video_size.y); return GSR_VIDEO_CODEC_HEVC; - } else if(supported_video_codecs->av1.supported && codec_supports_resolution(supported_video_codecs->av1.max_resolution, capture_size)) { + } else if(supported_video_codecs->av1.supported && codec_supports_resolution(supported_video_codecs->av1.max_resolution, video_size)) { fprintf(stderr, "gsr info: using av1 encoder because a codec was not specified and hevc supported max resolution (%dx%d) is less than capture resolution (%dx%d)\n", supported_video_codecs->hevc.max_resolution.x, supported_video_codecs->hevc.max_resolution.y, - capture_size.x, capture_size.y); + video_size.x, video_size.y); return GSR_VIDEO_CODEC_AV1; } else { return (gsr_video_codec)-1; } } -static const AVCodec* select_video_codec_with_fallback(gsr_capture_metadata capture_metadata, args_parser *args_parser, const char *file_extension, gsr_egl *egl, bool *low_power) { +static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_parser *args_parser, const char *file_extension, gsr_egl *egl, bool *low_power) { gsr_supported_video_codecs supported_video_codecs; get_supported_video_codecs(egl, args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, true, &supported_video_codecs); // TODO: Use gsr_supported_video_codecs *supported_video_codecs_vulkan here to properly query vulkan video support @@ -2853,7 +3286,7 @@ static const AVCodec* select_video_codec_with_fallback(gsr_capture_metadata capt fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n"); args_parser->video_codec = GSR_VIDEO_CODEC_H264; } else if(args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { - args_parser->video_codec = select_appropriate_video_codec_automatically(capture_metadata, &supported_video_codecs); + args_parser->video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs); if(args_parser->video_codec == (gsr_video_codec)-1) { if(args_parser->fallback_cpu_encoding) { fprintf(stderr, "gsr warning: gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues\n"); @@ -2883,11 +3316,10 @@ static const AVCodec* select_video_codec_with_fallback(gsr_capture_metadata capt const AVCodec *codec = pick_video_codec(egl, args_parser, true, low_power, &supported_video_codecs); const vec2i codec_max_resolution = codec_get_max_resolution(args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, &supported_video_codecs); - const vec2i capture_size = {capture_metadata.video_width, capture_metadata.video_height}; - if(!codec_supports_resolution(codec_max_resolution, capture_size)) { + if(!codec_supports_resolution(codec_max_resolution, video_size)) { const char *video_codec_name = video_codec_to_string(args_parser->video_codec); fprintf(stderr, "gsr error: The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again\n", - video_codec_name, codec_max_resolution.x, codec_max_resolution.y, capture_size.x, capture_size.y); + video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); _exit(53); } @@ -3058,6 +3490,71 @@ static void set_display_server_environment_variables() { } } +static bool is_capturing_damage_tracked_target(const std::vector<CaptureSource> &capture_sources) { + for(const CaptureSource &capture_source : capture_sources) { + if(capture_source.type != GSR_CAPTURE_SOURCE_TYPE_V4L2) + return true; + } + return false; +} + +static bool is_capturing_type(const std::vector<CaptureSource> &capture_sources, CaptureSourceType target_type) { + for(const CaptureSource &capture_source : capture_sources) { + if(capture_source.type == target_type) + return true; + } + return false; +} + +static bool has_capture_source_with_region_set(const std::vector<CaptureSource> &capture_sources) { + for(const CaptureSource &capture_source : capture_sources) { + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION && capture_source.region_set) + return true; + } + return false; +} + +static bool is_capturing_monitor_or_region(const std::vector<CaptureSource> &capture_sources) { + for(const CaptureSource &capture_source : capture_sources) { + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_MONITOR || capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) + return true; + } + return false; +} + +static void validate_args_with_capture_sources(args_parser &arg_parser, const std::vector<CaptureSource> &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(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) { + fprintf(stderr, "gsr error: option -s is required when using '-w focused' option\n"); + args_parser_print_usage(); + _exit(1); + } + + if(region_arg->num_values == 0) { + if(!has_capture_source_with_region_set(capture_sources)) { + fprintf(stderr, "gsr error: option -region is required when '-w region' is used\n"); + args_parser_print_usage(); + _exit(1); + } + } else { + if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION)) { + fprintf(stderr, "gsr warning: option -region is deprecated, use -w with region directly instead, for example: -w %s\n", region_arg->values[0]); + } else { + fprintf(stderr, "gsr error: option -region can only be used when option '-w region' is used\n"); + args_parser_print_usage(); + _exit(1); + } + } + + if(!arg_parser.restore_portal_session && is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) + fprintf(stderr, "gsr info: option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored\n"); +} + int main(int argc, char **argv) { setlocale(LC_ALL, "C"); // Sigh... stupid C #ifdef __GLIBC__ @@ -3097,7 +3594,7 @@ int main(int argc, char **argv) { setenv("AMD_DEBUG", "lowlatencyenc", 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 match vaapi driver instead of forcing a specific one. + // 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"); @@ -3114,12 +3611,22 @@ int main(int argc, char **argv) { 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; args_parser arg_parser; if(!args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL)) _exit(1); + std::vector<CaptureSource> capture_sources = parse_capture_source_arg(arg_parser.capture_source, arg_parser); + if(capture_sources.empty()) { + fprintf(stderr, "gsr error: option -w can't be empty. You need to capture video from at least one source\n"); + args_parser_print_usage(); + _exit(1); + } + validate_args_with_capture_sources(arg_parser, capture_sources); + + // TODO: Remove, this isn't true if(arg_parser.overclock) { int driver_major_version = 0; int driver_minor_version = 0; @@ -3172,7 +3679,6 @@ int main(int argc, char **argv) { validate_merged_audio_inputs_app_audio(requested_audio_inputs, app_audio_names); const bool is_replaying = arg_parser.replay_buffer_size_secs != -1; - const bool is_portal_capture = strcmp(arg_parser.window, "portal") == 0; bool wayland = false; Display *dpy = XOpenDisplay(nullptr); @@ -3201,14 +3707,20 @@ int main(int argc, char **argv) { _exit(1); } - if(is_portal_capture && is_using_prime_run()) { - fprintf(stderr, "gsr warning: use of prime-run with -w portal option is currently not supported. Disabling prime-run\n"); - disable_prime_run(); + if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) { + if(is_using_prime_run()) { + fprintf(stderr, "gsr warning: use of prime-run with -w portal option is currently not supported. Disabling prime-run\n"); + disable_prime_run(); + } + + if(video_codec_is_hdr(arg_parser.video_codec)) { + fprintf(stderr, "gsr warning: portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr\n"); + arg_parser.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser.video_codec); + } } - const bool is_monitor_capture = strcmp(arg_parser.window, "focused") != 0 && strcmp(arg_parser.window, "region") != 0 && !is_portal_capture && contains_non_hex_number(arg_parser.window); gsr_egl egl; - if(!gsr_egl_load(&egl, window, is_monitor_capture, arg_parser.gl_debug)) { + if(!gsr_egl_load(&egl, window, is_capturing_monitor_or_region(capture_sources), arg_parser.gl_debug)) { fprintf(stderr, "gsr error: failed to load opengl\n"); _exit(1); } @@ -3224,13 +3736,20 @@ int main(int argc, char **argv) { egl.card_path[0] = '\0'; if(monitor_capture_use_drm(window, egl.gpu_info.vendor)) { // TODO: Allow specifying another card, and in other places - if(!gsr_get_valid_card_path(&egl, egl.card_path, is_monitor_capture)) { + if(!gsr_get_valid_card_path(&egl, egl.card_path, is_capturing_monitor_or_region(capture_sources))) { fprintf(stderr, "gsr 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\n"); _exit(2); } } - // if(wayland && is_monitor_capture) { + memset(&x11_cursor, 0, sizeof(x11_cursor)); + x11_cursor_display = NULL; + if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11 && arg_parser.record_cursor) { + x11_cursor_display = (Display*)gsr_window_get_display(egl.window); + gsr_cursor_init(&x11_cursor, &egl, x11_cursor_display); + } + + // 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"); // } @@ -3242,7 +3761,7 @@ int main(int argc, char **argv) { _exit(1); } - capture_image_to_file(arg_parser, &egl, image_format); + capture_image_to_file(arg_parser, &egl, window, image_format, capture_sources); _exit(0); } @@ -3275,20 +3794,8 @@ int main(int argc, char **argv) { const bool uses_amix = merged_audio_inputs_should_use_amix(requested_audio_inputs); arg_parser.audio_codec = select_audio_codec_with_fallback(arg_parser.audio_codec, file_extension, uses_amix); - gsr_capture *capture = create_capture_impl(arg_parser, &egl, false); - - gsr_capture_metadata capture_metadata; - capture_metadata.video_width = 0; - capture_metadata.video_height = 0; - capture_metadata.recording_width = 0; - capture_metadata.recording_height = 0; - capture_metadata.fps = arg_parser.fps; - - int capture_result = gsr_capture_start(capture, &capture_metadata); - if(capture_result != 0) { - fprintf(stderr, "gsr error: gsr_capture_start failed\n"); - _exit(capture_result); - } + vec2i video_size = {0, 0}; + std::vector<VideoSource> video_sources = create_video_sources(arg_parser, &egl, false, capture_sources, video_size); // (Some?) livestreaming services require at least one audio track to work. // If not audio is provided then create one silent audio track. @@ -3308,10 +3815,10 @@ int main(int argc, char **argv) { } bool low_power = false; - const AVCodec *video_codec_f = select_video_codec_with_fallback(capture_metadata, &arg_parser, file_extension.c_str(), &egl, &low_power); + const AVCodec *video_codec_f = select_video_codec_with_fallback(video_size, &arg_parser, file_extension.c_str(), &egl, &low_power); const enum AVPixelFormat video_pix_fmt = get_pixel_format(arg_parser.video_codec, egl.gpu_info.vendor, arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); - AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, egl, arg_parser, capture_metadata.video_width, capture_metadata.video_height); + AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, egl, arg_parser, video_size.x, video_size.y); if(!is_replaying) video_stream = create_stream(av_format_context, video_codec_context); @@ -3321,8 +3828,8 @@ int main(int argc, char **argv) { _exit(1); } video_frame->format = video_codec_context->pix_fmt; - video_frame->width = capture_metadata.video_width; - video_frame->height = capture_metadata.video_height; + 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; @@ -3347,12 +3854,9 @@ int main(int argc, char **argv) { _exit(1); } - capture_metadata.recording_width = capture_metadata.video_width; - capture_metadata.recording_height = capture_metadata.video_height; - - // TODO: What if this updated resolution is above max resolution? - capture_metadata.video_width = video_codec_context->width; - capture_metadata.video_height = video_codec_context->height; + video_size.x = video_codec_context->width; + video_size.y = video_codec_context->height; + video_sources_update_with_real_video_size(capture_sources, video_sources, video_size); const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); assert(plugin_arg); @@ -3365,8 +3869,8 @@ int main(int argc, char **argv) { assert(color_depth == GSR_COLOR_DEPTH_8_BITS || color_depth == GSR_COLOR_DEPTH_10_BITS); const gsr_plugin_init_params plugin_init_params = { - (unsigned int)capture_metadata.video_width, - (unsigned int)capture_metadata.video_height, + (unsigned int)video_size.x, + (unsigned int)video_size.y, (unsigned int)arg_parser.fps, color_depth == GSR_COLOR_DEPTH_8_BITS ? GSR_PLUGIN_COLOR_DEPTH_8_BITS : GSR_PLUGIN_COLOR_DEPTH_10_BITS, egl.context_type == GSR_GL_CONTEXT_TYPE_GLX ? GSR_PLUGIN_GRAPHICS_API_GLX : GSR_PLUGIN_GRAPHICS_API_EGL_ES, @@ -3385,7 +3889,7 @@ int main(int argc, char **argv) { memset(&color_conversion_params, 0, sizeof(color_conversion_params)); color_conversion_params.color_range = arg_parser.color_range; color_conversion_params.egl = &egl; - color_conversion_params.load_external_image_shader = gsr_capture_uses_external_image(capture); + color_conversion_params.load_external_image_shader = any_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; @@ -3721,48 +4225,77 @@ int main(int argc, char **argv) { bool use_damage_tracking = false; gsr_damage damage; memset(&damage, 0, sizeof(damage)); - if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) { + if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && is_capturing_damage_tracked_target(capture_sources)) { if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11) { - gsr_damage_init(&damage, &egl, arg_parser.record_cursor); + gsr_damage_init(&damage, &egl, &x11_cursor, arg_parser.record_cursor); use_damage_tracking = true; - } else if(!capture->is_damaged) { + + for(const CaptureSource &capture_source : capture_sources) { + 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.c_str()); + break; + default: + break; + } + } + } else if(is_capturing_monitor_or_region(capture_sources)) { fprintf(stderr, "gsr 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)\n"); } - - if(is_monitor_capture) - gsr_damage_set_target_monitor(&damage, arg_parser.window); } while(running) { while(gsr_window_process_event(window)) { + if(x11_cursor_display && arg_parser.record_cursor) + gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); + gsr_damage_on_event(&damage, gsr_window_get_event_data(window)); - gsr_capture_on_event(capture, &egl); + for(VideoSource &video_source : video_sources) { + gsr_capture_on_event(video_source.capture, &egl); + } } - gsr_damage_tick(&damage); - gsr_capture_tick(capture); - if(!is_monitor_capture) { - Window damage_target_window = 0; - if(capture->get_window_id) - damage_target_window = capture->get_window_id(capture); + if(x11_cursor_display && arg_parser.record_cursor) + gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); - if(damage_target_window != 0) - gsr_damage_set_target_window(&damage, damage_target_window); - } + gsr_damage_tick(&damage); should_stop_error = false; - if(gsr_capture_should_stop(capture, &should_stop_error)) { - running = 0; - break; - } - bool damaged = false; - if(use_damage_tracking) - damaged = gsr_damage_is_damaged(&damage); - else if(capture->is_damaged) - damaged = capture->is_damaged(capture); - else - damaged = true; + + for(VideoSource &video_source : video_sources) { + 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(use_damage_tracking) + damaged = gsr_damage_is_damaged(&damage); + else if(video_source.capture->is_damaged) + damaged = video_source.capture->is_damaged(video_source.capture); + else + damaged = true; + } // TODO: Readd wayland sync warning when removing this if(arg_parser.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) @@ -3789,30 +4322,53 @@ int main(int argc, char **argv) { const int64_t num_missed_frames = expected_frames - video_pts_counter; if(damaged && num_missed_frames >= 1 && !paused) { - gsr_damage_clear(&damage); - if(capture->clear_damage) - capture->clear_damage(capture); - // TODO: Dont do this if no damage? egl.glClear(0); + gsr_damage_clear(&damage); + gsr_capture_kms_cleanup_kms_fds(); + + if(kms_client_initialized) { + if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) + fprintf(stderr, "gsr error: failed to get kms, error: %d (%s)\n", kms_response.result, kms_response.err_msg); + } + bool capture_has_synchronous_task = false; - if(capture->capture_has_synchronous_task) { - capture_has_synchronous_task = capture->capture_has_synchronous_task(capture); - if(capture_has_synchronous_task) { - paused_time_start = clock_get_monotonic_seconds(); - paused = true; + for(VideoSource &video_source : video_sources) { + 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_time_start = clock_get_monotonic_seconds(); + paused = true; + } } } - gsr_capture_capture(capture, &capture_metadata, output_color_conversion); + for(VideoSource &video_source : video_sources) { + if(video_source.capture->pre_capture) + video_source.capture->pre_capture(video_source.capture, &video_source.metadata, &color_conversion); + } + + if(color_conversion.schedule_clear) { + color_conversion.schedule_clear = false; + gsr_color_conversion_clear(&color_conversion); + } + + for(VideoSource &video_source : video_sources) { + gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion); + } + + gsr_capture_kms_cleanup_kms_fds(); if(plugins.num_plugins > 0) { gsr_plugins_draw(&plugins); gsr_color_conversion_draw(&color_conversion, plugins.texture, - {0, 0}, {capture_metadata.video_width, capture_metadata.video_height}, - {0, 0}, {capture_metadata.video_width, capture_metadata.video_height}, - {capture_metadata.video_width, capture_metadata.video_height}, GSR_ROT_0, GSR_SOURCE_COLOR_RGB, false); + {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) { @@ -3823,8 +4379,10 @@ int main(int argc, char **argv) { gsr_egl_swap_buffers(&egl); gsr_video_encoder_copy_textures_to_frame(video_encoder, video_frame, output_color_conversion); - if(hdr && !hdr_metadata_set && !is_replaying && add_hdr_metadata_to_video_stream(capture, video_stream)) - hdr_metadata_set = true; + for(VideoSource &video_source : video_sources) { + if(hdr && !hdr_metadata_set && !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.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; @@ -3887,7 +4445,7 @@ int main(int argc, char **argv) { std::lock_guard<std::mutex> lock(audio_filter_mutex); replay_recording_items.clear(); replay_recording_filepath = create_new_recording_filepath_from_timestamp(arg_parser.replay_recording_directory, "Video", file_extension, arg_parser.date_folders); - replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser.container_format, video_codec_context, audio_tracks, hdr, capture); + replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser.container_format, video_codec_context, audio_tracks, hdr, video_sources); if(replay_recording_start_result.av_format_context) { const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_start_result.av_format_context, replay_recording_start_result.video_stream, video_frame->pts); if(video_recording_destination_id != (size_t)-1) @@ -3949,7 +4507,7 @@ int main(int argc, char **argv) { save_replay_seconds = 0; save_replay_output_filepath.clear(); - save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, encoder.replay_buffer, arg_parser.filename, arg_parser.container_format, file_extension, arg_parser.date_folders, hdr, capture, current_save_replay_seconds); + save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, encoder.replay_buffer, arg_parser.filename, arg_parser.container_format, file_extension, arg_parser.date_folders, hdr, video_sources, current_save_replay_seconds); if(arg_parser.restart_replay_on_save && current_save_replay_seconds == save_replay_seconds_full) gsr_replay_buffer_clear(encoder.replay_buffer); @@ -4028,14 +4586,21 @@ int main(int argc, char **argv) { avformat_free_context(av_format_context); } + gsr_cursor_deinit(&x11_cursor); gsr_damage_deinit(&damage); gsr_color_conversion_deinit(&color_conversion); gsr_video_encoder_destroy(video_encoder, video_codec_context); gsr_encoder_deinit(&encoder); - gsr_capture_destroy(capture); + for(VideoSource &video_source : video_sources) { + gsr_capture_destroy(video_source.capture); + } #ifdef GSR_APP_AUDIO gsr_pipewire_audio_deinit(&pipewire_audio); #endif + if(kms_client_initialized) { + gsr_capture_kms_cleanup_kms_fds(); + gsr_kms_client_deinit(&kms_client); + } if(!is_replaying && arg_parser.recording_saved_script) run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "regular"); |
