aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2025-12-23 18:05:48 +0100
committerdec05eba <dec05eba@protonmail.com>2025-12-23 18:57:43 +0100
commit62e180903e376f8492bcc1705a97566ecc718f2b (patch)
tree37776e1d3a7c54d1a2968544616f39f54d7d20ef
parent390f2708f4f9be766ad0ac4b22ba2770d2aed199 (diff)
Add support for camera (yuyv, mjpeg) and multiple capture sources
-rw-r--r--README.md7
-rw-r--r--TODO32
-rw-r--r--gpu-screen-recorder.1138
-rw-r--r--include/args_parser.h13
-rw-r--r--include/capture/capture.h28
-rw-r--r--include/capture/kms.h4
-rw-r--r--include/capture/v4l2.h30
-rw-r--r--include/capture/xcomposite.h2
-rw-r--r--include/capture/ximage.h2
-rw-r--r--include/color_conversion.h10
-rw-r--r--include/damage.h54
-rw-r--r--include/egl.h1
-rw-r--r--include/utils.h2
-rw-r--r--kms/client/kms_client.c1
-rw-r--r--kms/server/kms_server.c1
-rw-r--r--meson.build1
-rw-r--r--project.conf2
-rw-r--r--src/args_parser.c54
-rw-r--r--src/capture/kms.c158
-rw-r--r--src/capture/nvfbc.c18
-rw-r--r--src/capture/portal.c50
-rw-r--r--src/capture/v4l2.c656
-rw-r--r--src/capture/xcomposite.c64
-rw-r--r--src/capture/ximage.c46
-rw-r--r--src/color_conversion.c22
-rw-r--r--src/cursor.c8
-rw-r--r--src/damage.c416
-rw-r--r--src/main.cpp981
-rw-r--r--src/sound.cpp14
-rw-r--r--src/utils.c37
30 files changed, 2224 insertions, 628 deletions
diff --git a/README.md b/README.md
index 698153f..7d7fb48 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ These are the dependencies needed to build GPU Screen Recorder:
* libdrm
* libcap
* vulkan-headers
+* linux-api-headers
## Optional dependencies
When building GPU Screen Recorder with portal support (`-Dportal=true` meson option, which is enabled by default) these dependencies are also needed:
@@ -71,6 +72,7 @@ When building GPU Screen Recorder with portal support (`-Dportal=true` meson opt
## Runtime dependencies
* libglvnd (which provides libgl, libglx and libegl) is needed. Your system needs to support at least OpenGL ES 3.0 (released in 2012)
+* libturbojpeg (aka libjpeg-turbo) is needed when capturing camera with mjpeg pixel format option
There are also additional dependencies needed at runtime depending on your GPU vendor:
@@ -252,4 +254,7 @@ This issue hasn't been observed on X11 yet, but if you do observe it you can eit
## Password prompt shows up when I try to record my screen
If GPU Screen Recorder is installed with -Dcapabilities=true (which is the default option) then `gsr-kms-server` is installed with admin capabilities.
This removes a password prompt when recording a monitor with the `-w monitor` option (for example `-w screen`). However if the root user is disabled on the system then the password prompt will show up anyways.
-If the root user is disabled on your system then you can instead record with `-w focused` or `-w window_id` on X11 or `-w portal` on Wayland. \ No newline at end of file
+If the root user is disabled on your system then you can instead record with `-w focused` or `-w window_id` on X11 or `-w portal` on Wayland.
+## GPU usage is high on my laptop
+GPU usage on battery powered devices is misleading. For example Intel iGPUs has multiple performance levels and the GPU usage reported on the system is the GPU usage at the current performance level.
+The performance level changes depending on the GPU load, so it may say that GPU usage is 80%, but the actual GPU usage may be 5%.
diff --git a/TODO b/TODO
index cb275f7..c17bcf9 100644
--- a/TODO
+++ b/TODO
@@ -4,12 +4,8 @@ See https://trac.ffmpeg.org/wiki/EncodingForStreamingSites for optimizing stream
Look at VK_EXT_external_memory_dma_buf.
Use mov+faststart.
Allow recording all monitors/selected monitor without nvfbc by recording the compositor proxy window and only recording the part that matches the monitor(s).
-Support amf and qsv.
Disable flipping on nvidia? this might fix some stuttering issues on some setups. See NvCtrlGetAttribute/NvCtrlSetAttributeAndGetStatus NV_CTRL_SYNC_TO_VBLANK https://github.com/NVIDIA/nvidia-settings/blob/d5f022976368cbceb2f20b838ddb0bf992f0cfb9/src/gtk%2B-2.x/ctkopengl.c.
-Replays seem to have some issues with audio/video. Why?
Cleanup unused gl/egl functions, macro, etc.
-Set audio track name to audio device name (if not merge of multiple audio devices).
-Add support for webcam, but only really for amd/intel because amd/intel can get drm fd access to webcam, nvidia cant. This allows us to create an opengl texture directly from the webcam fd for optimal performance.
Reverse engineer nvapi so we can disable "force p2 state" on linux too (nvapi profile api with the settings id 0x50166c5e).
Support yuv444p on amd/intel.
fix yuv444 for hevc.
@@ -94,7 +90,7 @@ Support vfr matching games exact fps all the time. On x11 use damage tracking, o
Support selecting which gpu to use. This can be done in egl with eglQueryDevicesEXT and then eglGetPlatformDisplayEXT. This will automatically work on AMD and Intel as vaapi uses the same device. On nvidia we need to use eglQueryDeviceAttribEXT with EGL_CUDA_DEVICE_NV.
Maybe on glx (nvidia x11 nvfbc) we need to use __NV_PRIME_RENDER_OFFLOAD, __NV_PRIME_RENDER_OFFLOAD_PROVIDER, __GLX_VENDOR_LIBRARY_NAME, __VK_LAYER_NV_optimus, VK_ICD_FILENAMES instead. Just look at prime-run /usr/bin/prime-run.
-When adding support for steam deck, add option to send video to another computer.
+Add option to send video to another computer.
New gpu screen recorder gui should have the option to cut the video directly, maybe running an ffmpeg command or implementing that ourselves. Only support gpu screen recorder video files.
Check if is software renderer by using eglQueryDisplayAttribEXT(egl_display, EGL_DEVICE_EXT..) eglQueryDeviceStringEXT(egl_device, EGL_EXTENSIONS) and check for "EGL_MESA_device_software".
@@ -131,8 +127,6 @@ Enable 2-pass encoding.
Restart replay/update video resolution if monitor resolution changes.
-Fix pure vaapi copy on intel.
-
Use nvidia low latency options for better encoding times.
Test ideal async_depth value. Increasing async_depth also increased gpu memory usage a lot (from 100mb to 500mb when moving from async_depth 2 to 16) at 4k resolution. Setting it to 8 increases it by 200mb which might be ok.
@@ -354,3 +348,27 @@ There is a leak in nvfbc. When a monitor is turned off and then on there will be
Right now a mitigation has been added to not try to recreate the nvfbc session if the capture target (monitor) isn't connected (predict if nvfbc session create will fail).
One possible reason this happens is because bExternallyManagedContext is set to true.
This also means that nvfbc leaks connection when destroying nvfbc, even if the monitor is connected (this is not an issue right now because exit is done, but if gsr was turned into a library it would be).
+
+Add option to set audio source volume, maybe by doing for example: -a "default_input|app:firefox;volume=50"
+
+Optimize v4l2 mjpeg by decompressing to yuv (tjDecompressToYUV) instead of rgb. This would allow removing the yuv to rgb conversion on cpu step (and rgb to yuv on the gpu as well) as well as reducing the cpu->gpu image data bandwidth as yuv is compressed.
+
+Do jpeg decoding on the gpu for v4l2 mjpeg capture (see https://github.com/negge/jpeg_gpu). Or use jpeg decoding from vaapi/nvdec.
+
+Support other v4l2 pixel formats, such as h264 and rgb/bgr. High-end cameras use h264 for high resolution high framerate option (4k 60fps or higher).
+
+AV1 medium quality on nvidia seems to be very high quality. The quality needs to be rescaled. Need to test on nvidia av1 gpu, but I dont have any such gpu.
+
+Support multiple -w, to record different sources to different video tracks. For example gameplay to video track 1 and webcam to video track 2.
+
+Update man page with info about v4l2 and new region capture format. Mention that -region is deprecated.
+
+Make multiple -w portal work with -restore-portal-session.
+
+Test if webcam capture works on intel and nvidia. Especially nvidia on x11 because it uses glx there for nvfbc while v4l2 needs egl.
+
+Play with DRM_FORMAT_MOD_LINEAR or other linear formats to avoid cuda copy. If it's already in linear format then instead check the format of the target texture and use the same format.
+
+Give early error when using invalid v4l2 path like with monitor. Use gsr_capture_v4l2_list_devices for query. Or maybe remove the early error for monitor to simplify the code.
+
+Support camera capture with pipewire to support multiple applications recording camera at the same time. Might not be as efficient as V4L2. \ No newline at end of file
diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1
index c237ca3..0be3188 100644
--- a/gpu-screen-recorder.1
+++ b/gpu-screen-recorder.1
@@ -3,9 +3,8 @@
gpu-screen-recorder \- The fastest screen recording tool for Linux
.SH SYNOPSIS
.B gpu-screen-recorder
-.RI [ options ]
.B \-w
-.I window_id|monitor|focused|portal|region
+.I window_id|monitor|focused|portal|region|v4l2_device_path
.RI [ options ]
.B \-o
.I output_file
@@ -50,8 +49,9 @@ Minimal performance impact compared to traditional screen recorders
.SH OPTIONS
.SS Capture Options
.TP
-.BI \-w " window_id|monitor|focused|portal|region"
-Specify what to record. Valid values are:
+.BI \-w " window_id|monitor|focused|portal|region|v4l2_device_path"
+Specify what to record.
+Formats:
.RS
.IP \(bu 3
.B window
@@ -77,10 +77,99 @@ option)
Monitor name (e.g.,
.BR DP\-1 )
- Record specific monitor
+.IP \(bu 3
+V4L2 device path (e.g.,
+.BR /dev/video0 )
+- Record camera device (V4L2).
+
+Other applications can't use the camera when GPU Screen Recorder is using the camera and GPU Screen Recorder may not be able to use the camera if another application is already using the camera
+.IP \(bu 3
+Combine sources with | (e.g.,
+.BR "monitor:screen|v4l2:/dev/video0" )
.RE
+.PP
Run
.B \-\-list\-capture\-options
-to see available options.
+to list available capture sources.
+.PP
+Run
+.B \-\-list\-v4l2\-devices
+to list available camera devices (V4L2).
+.PP
+Additional options can be passed to each capture source by splitting capture source with
+.B ;
+for example
+.BR "screen;x=50;y=50".
+.br
+These are the available options for capture sources:
+.RS
+.IP \(bu 3
+.B x
+- The X position in pixels. If the number ends with % and is a number between 0 and 100 then it's a position relative to the video size
+.IP \(bu 3
+.B y
+- The Y position in pixels. If the number ends with % and is a number between 0 and 100 then it's a position relative to the video size
+.IP \(bu 3
+.B width
+- The width in pixels. If the number ends with % and is a number between 0 and 100 then it's a size relative to the video size
+.IP \(bu 3
+.B height
+- The height in pixels. If the number ends with % and is a number between 0 and 100 then it's a size relative to the video size
+.IP \(bu 3
+.B halign
+- The horizontal alignment, should be either
+.BR "start",
+.B center
+or
+.BR "end".
+
+Set to
+.B center
+by default, except for camera (V4L2) when capturing the camera above something else in which case this is set to
+.B start
+by default
+.IP \(bu 3
+.B valign
+- The vertical alignment, should be either
+.BR "start",
+.B center
+or
+.BR "end".
+
+Set to
+.B center
+by default, except for camera (V4L2) when capturing the camera above something else in which case this is set to
+.B end
+by default
+.IP \(bu 3
+.B hflip
+- If the source should be flipped horizontally, should be either
+.B "true"
+or
+.BR "false".
+Set to
+.B false
+by default
+.IP \(bu 3
+.B vflip
+- If the source should be flipped vertically, should be either
+.B "true"
+or
+.BR "false".
+Set to
+.B false
+by default
+.IP \(bu 3
+.B pixfmt
+- The pixel format for cameras (V4L2), should be either
+.BR "auto",
+.B yuyv
+or
+.BR "mjpeg".
+Set to
+.B auto
+by default
+.RE
.TP
.BI \-region " WxH+X+Y"
Specify region to capture when using
@@ -132,6 +221,14 @@ Formats:
Combine sources with | (e.g.,
.BR "default_output|app:firefox" )
.RE
+.PP
+Run
+.B \-\-list\-audio\-devices
+to list available audio devices.
+.br
+Run
+.B \-\-list\-application\-audio
+to list available applications for audio capture.
.TP
.BI \-ac " aac|opus|flac"
Audio codec (default: opus for mp4/mkv, aac otherwise). FLAC temporarily disabled.
@@ -224,13 +321,16 @@ Show version (5.10.2).
Show system info (codecs, capture options).
.TP
.B \-\-list\-capture\-options
-List available windows and monitors.
+List available capture sources (window, monitors, portal, v4l2 device path).
+.TP
+.B \-\-list\-v4l2\-devices
+List available cameras devices (V4L2).
.TP
.B \-\-list\-audio\-devices
-List PulseAudio devices.
+List available audio devices.
.TP
.B \-\-list\-application\-audio
-List applications for audio capture.
+List available applications for audio capture.
.SH SIGNALS
GPU Screen Recorder can be controlled via signals:
.TP
@@ -305,7 +405,7 @@ Record region using slop:
.PP
.nf
.RS
-gpu-screen-recorder -w region -region $(slop) -o video.mp4
+gpu-screen-recorder -w $(slop) -o video.mp4
.RE
.fi
.PP
@@ -313,11 +413,11 @@ Record region using slurp:
.PP
.nf
.RS
-gpu-screen-recorder -w region -region $(slurp -f "%wx%h+%x+%y") -o video.mp4
+gpu-screen-recorder -w $(slurp -f "%wx%h+%x+%y") -o video.mp4
.RE
.fi
.PP
-Instant replay and launch a script when saving replay
+Instant replay and launch a script when saving replay:
.PP
.nf
.RS
@@ -338,6 +438,22 @@ Take screenshot:
.nf
.RS
gpu-screen-recorder -w screen -o screenshot.jpg
+.PP
+.RE
+.fi
+Record screen and camera:
+.PP
+.nf
+.RS
+gpu-screen-recorder -w "screen|/dev/video0" -o video.mp4
+.PP
+.RE
+.fi
+Record screen and camera. The camera is located at the bottom right flipped horizontally:
+.PP
+.nf
+.RS
+gpu-screen-recorder -w "monitor:screen|v4l2:/dev/video0;halign=end;valign=end;hflip=true" -o video.mp4
.RE
.fi
.SH FILES
diff --git a/include/args_parser.h b/include/args_parser.h
index 7de962a..316b5e1 100644
--- a/include/args_parser.h
+++ b/include/args_parser.h
@@ -11,6 +11,15 @@ typedef struct gsr_egl gsr_egl;
#define NUM_ARGS 32
typedef enum {
+ GSR_CAPTURE_SOURCE_TYPE_WINDOW,
+ GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW,
+ GSR_CAPTURE_SOURCE_TYPE_MONITOR,
+ GSR_CAPTURE_SOURCE_TYPE_REGION,
+ GSR_CAPTURE_SOURCE_TYPE_PORTAL,
+ GSR_CAPTURE_SOURCE_TYPE_V4L2
+} CaptureSourceType;
+
+typedef enum {
ARG_TYPE_STRING,
ARG_TYPE_BOOLEAN,
ARG_TYPE_ENUM,
@@ -52,6 +61,7 @@ typedef struct {
void (*info)(void *userdata);
void (*list_audio_devices)(void *userdata);
void (*list_application_audio)(void *userdata);
+ void (*list_v4l2_devices)(void *userdata);
void (*list_capture_options)(const char *card_path, void *userdata);
} args_handlers;
@@ -68,7 +78,8 @@ typedef struct {
gsr_bitrate_mode bitrate_mode;
gsr_video_quality video_quality;
gsr_replay_storage replay_storage;
- char window[64];
+
+ const char *capture_source;
const char *container_format;
const char *filename;
const char *replay_recording_directory;
diff --git a/include/capture/capture.h b/include/capture/capture.h
index db80d4f..56cf909 100644
--- a/include/capture/capture.h
+++ b/include/capture/capture.h
@@ -12,24 +12,34 @@ typedef struct AVFrame AVFrame;
typedef struct AVMasteringDisplayMetadata AVMasteringDisplayMetadata;
typedef struct AVContentLightMetadata AVContentLightMetadata;
typedef struct gsr_capture gsr_capture;
+typedef struct gsr_capture_metadata gsr_capture_metadata;
-typedef struct {
- // Width and height of the video
- int video_width;
- int video_height;
- // Width and height of the frame at the start of capture, the target size
- int recording_width;
- int recording_height;
+typedef enum {
+ GSR_CAPTURE_ALIGN_START,
+ GSR_CAPTURE_ALIGN_CENTER,
+ GSR_CAPTURE_ALIGN_END
+} gsr_capture_alignment;
+
+struct gsr_capture_metadata {
+ // Size of the video
+ vec2i video_size;
+ // The captured output gets scaled to this size. By default this will be the same size as the captured target
+ vec2i recording_size;
+ vec2i position;
int fps;
-} gsr_capture_metadata;
+ gsr_capture_alignment halign;
+ gsr_capture_alignment valign;
+ gsr_flip flip;
+};
struct gsr_capture {
- /* These methods should not be called manually. Call gsr_capture_* instead. |capture_metadata->video_width| and |capture_metadata->video_height| should be set by this function */
+ /* These methods should not be called manually. Call gsr_capture_* instead. |capture_metadata->video_size| should be set by this function */
int (*start)(gsr_capture *cap, gsr_capture_metadata *capture_metadata);
void (*on_event)(gsr_capture *cap, gsr_egl *egl); /* can be NULL */
void (*tick)(gsr_capture *cap); /* can be NULL. If there is an event then |on_event| is called before this */
bool (*should_stop)(gsr_capture *cap, bool *err); /* can be NULL. If NULL, return false */
bool (*capture_has_synchronous_task)(gsr_capture *cap); /* can be NULL. If this returns true then the time spent in |capture| is ignored for video/audio (capture is paused while the synchronous task happens) */
+ void (*pre_capture)(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion); /* can be NULL */
int (*capture)(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion); /* Return 0 if the frame was captured */
bool (*uses_external_image)(gsr_capture *cap); /* can be NULL. If NULL, return false */
bool (*set_hdr_metadata)(gsr_capture *cap, AVMasteringDisplayMetadata *mastering_display_metadata, AVContentLightMetadata *light_metadata); /* can be NULL. If NULL, return false */
diff --git a/include/capture/kms.h b/include/capture/kms.h
index ce09817..deb12a0 100644
--- a/include/capture/kms.h
+++ b/include/capture/kms.h
@@ -2,9 +2,13 @@
#define GSR_CAPTURE_KMS_H
#include "capture.h"
+#include "../cursor.h"
+#include "../../kms/kms_shared.h"
typedef struct {
gsr_egl *egl;
+ gsr_cursor *x11_cursor;
+ gsr_kms_response *kms_response;
const char *display_to_capture; /* A copy is made of this */
bool hdr;
bool record_cursor;
diff --git a/include/capture/v4l2.h b/include/capture/v4l2.h
new file mode 100644
index 0000000..b7665aa
--- /dev/null
+++ b/include/capture/v4l2.h
@@ -0,0 +1,30 @@
+#ifndef GSR_CAPTURE_V4L2_H
+#define GSR_CAPTURE_V4L2_H
+
+#include "capture.h"
+
+typedef enum {
+ GSR_CAPTURE_V4L2_PIXFMT_AUTO,
+ GSR_CAPTURE_V4L2_PIXFMT_YUYV,
+ GSR_CAPTURE_V4L2_PIXFMT_MJPEG
+} gsr_capture_v4l2_pixfmt;
+
+typedef struct {
+ bool yuyv;
+ bool mjpeg;
+} gsr_capture_v4l2_supported_pixfmts;
+
+typedef struct {
+ gsr_egl *egl;
+ vec2i output_resolution;
+ const char *device_path;
+ gsr_capture_v4l2_pixfmt pixfmt;
+ int fps;
+} gsr_capture_v4l2_params;
+
+gsr_capture* gsr_capture_v4l2_create(const gsr_capture_v4l2_params *params);
+
+typedef void (*v4l2_devices_query_callback)(const char *path, gsr_capture_v4l2_supported_pixfmts supported_pixfmts, vec2i size, void *userdata);
+void gsr_capture_v4l2_list_devices(v4l2_devices_query_callback callback, void *userdata);
+
+#endif /* GSR_CAPTURE_V4L2_H */
diff --git a/include/capture/xcomposite.h b/include/capture/xcomposite.h
index bf6532e..15ab2a8 100644
--- a/include/capture/xcomposite.h
+++ b/include/capture/xcomposite.h
@@ -3,9 +3,11 @@
#include "capture.h"
#include "../vec2.h"
+#include "../cursor.h"
typedef struct {
gsr_egl *egl;
+ gsr_cursor *cursor;
unsigned long window;
bool follow_focused; /* If this is set then |window| is ignored */
bool record_cursor;
diff --git a/include/capture/ximage.h b/include/capture/ximage.h
index e6c3607..8b86618 100644
--- a/include/capture/ximage.h
+++ b/include/capture/ximage.h
@@ -3,9 +3,11 @@
#include "capture.h"
#include "../vec2.h"
+#include "../cursor.h"
typedef struct {
gsr_egl *egl;
+ gsr_cursor *cursor;
const char *display_to_capture; /* A copy is made of this */
bool record_cursor;
vec2i output_resolution;
diff --git a/include/color_conversion.h b/include/color_conversion.h
index e536352..cc1f9ca 100644
--- a/include/color_conversion.h
+++ b/include/color_conversion.h
@@ -27,6 +27,12 @@ typedef enum {
GSR_ROT_270
} gsr_rotation;
+typedef enum {
+ GSR_FLIP_NONE = 0,
+ GSR_FLIP_HORIZONTAL = (1 << 0),
+ GSR_FLIP_VERTICAL = (1 << 1)
+} gsr_flip;
+
typedef struct {
int rotation_matrix;
int offset;
@@ -55,12 +61,14 @@ typedef struct {
unsigned int vertex_array_object_id;
unsigned int vertex_buffer_object_id;
+
+ bool schedule_clear;
} gsr_color_conversion;
int gsr_color_conversion_init(gsr_color_conversion *self, const gsr_color_conversion_params *params);
void gsr_color_conversion_deinit(gsr_color_conversion *self);
-void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i destination_pos, vec2i destination_size, vec2i source_pos, vec2i source_size, vec2i texture_size, gsr_rotation rotation, gsr_source_color source_color, bool external_texture);
+void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i destination_pos, vec2i destination_size, vec2i source_pos, vec2i source_size, vec2i texture_size, gsr_rotation rotation, gsr_flip flip, gsr_source_color source_color, bool external_texture);
void gsr_color_conversion_clear(gsr_color_conversion *self);
void gsr_color_conversion_read_destination_texture(gsr_color_conversion *self, int destination_texture_index, int x, int y, int width, int height, unsigned int color_format, unsigned int data_format, void *pixels);
diff --git a/include/damage.h b/include/damage.h
index 4b10e58..9f8bc64 100644
--- a/include/damage.h
+++ b/include/damage.h
@@ -9,40 +9,62 @@
typedef struct _XDisplay Display;
typedef union _XEvent XEvent;
-typedef enum {
- GSR_DAMAGE_TRACK_NONE,
- GSR_DAMAGE_TRACK_WINDOW,
- GSR_DAMAGE_TRACK_MONITOR
-} gsr_damage_track_type;
+#define GSR_DAMAGE_MAX_MONITORS 32
+#define GSR_DAMAGE_MAX_TRACKED_TARGETS 12
+
+typedef struct {
+ int64_t window_id;
+ vec2i window_pos;
+ vec2i window_size;
+ uint64_t damage;
+ int refcount;
+} gsr_damage_window;
+
+typedef struct {
+ char *monitor_name;
+ gsr_monitor *monitor;
+ int refcount;
+} gsr_damage_monitor;
typedef struct {
gsr_egl *egl;
Display *display;
bool track_cursor;
- gsr_damage_track_type track_type;
int damage_event;
int damage_error;
- uint64_t damage;
bool damaged;
+ uint64_t monitor_damage;
+
int randr_event;
int randr_error;
- uint64_t window;
- //vec2i window_pos;
- vec2i window_size;
+ gsr_cursor *cursor;
+ gsr_monitor monitors[GSR_DAMAGE_MAX_MONITORS];
+ int num_monitors;
+
+ gsr_damage_window windows_tracked[GSR_DAMAGE_MAX_TRACKED_TARGETS];
+ int num_windows_tracked;
- gsr_cursor cursor; /* Relative to |window| */
- gsr_monitor monitor;
- char monitor_name[32];
+ gsr_damage_monitor monitors_tracked[GSR_DAMAGE_MAX_TRACKED_TARGETS];
+ int num_monitors_tracked;
+
+ int all_monitors_tracked_refcount;
+ vec2i cursor_pos;
} gsr_damage;
-bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, bool track_cursor);
+bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, gsr_cursor *cursor, bool track_cursor);
void gsr_damage_deinit(gsr_damage *self);
-bool gsr_damage_set_target_window(gsr_damage *self, uint64_t window);
-bool gsr_damage_set_target_monitor(gsr_damage *self, const char *monitor_name);
+/* This is reference counted */
+bool gsr_damage_start_tracking_window(gsr_damage *self, int64_t window);
+void gsr_damage_stop_tracking_window(gsr_damage *self, int64_t window);
+
+/* This is reference counted. If |monitor_name| is NULL then all monitors are tracked */
+bool gsr_damage_start_tracking_monitor(gsr_damage *self, const char *monitor_name);
+void gsr_damage_stop_tracking_monitor(gsr_damage *self, const char *monitor_name);
+
void gsr_damage_on_event(gsr_damage *self, XEvent *xev);
void gsr_damage_tick(gsr_damage *self);
/* Also returns true if damage tracking is not available */
diff --git a/include/egl.h b/include/egl.h
index d895658..dab062e 100644
--- a/include/egl.h
+++ b/include/egl.h
@@ -140,6 +140,7 @@ typedef void(*__GLXextFuncPtr)(void);
#define GL_TEXTURE1 0x84C1
#define GL_SHADER_IMAGE_ACCESS_BARRIER_BIT 0x00000020
#define GL_ALL_BARRIER_BITS 0xFFFFFFFF
+#define GL_PIXEL_UNPACK_BUFFER 0x88EC
#define GL_VENDOR 0x1F00
#define GL_RENDERER 0x1F01
diff --git a/include/utils.h b/include/utils.h
index e0af1ea..ff8cb90 100644
--- a/include/utils.h
+++ b/include/utils.h
@@ -9,6 +9,7 @@
typedef struct AVCodecContext AVCodecContext;
typedef struct AVFrame AVFrame;
+typedef struct gsr_capture_metadata gsr_capture_metadata;
typedef struct {
const char *name;
@@ -55,6 +56,7 @@ int create_directory_recursive(char *path);
void setup_dma_buf_attrs(intptr_t *img_attr, uint32_t format, uint32_t width, uint32_t height, const int *fds, const uint32_t *offsets, const uint32_t *pitches, const uint64_t *modifiers, int num_planes, bool use_modifier);
vec2i scale_keep_aspect_ratio(vec2i from, vec2i to);
+vec2i gsr_capture_get_target_position(vec2i output_size, gsr_capture_metadata *capture_metadata);
unsigned int gl_create_texture(gsr_egl *egl, int width, int height, int internal_format, unsigned int format, int filter);
diff --git a/kms/client/kms_client.c b/kms/client/kms_client.c
index d7ddc78..45d979c 100644
--- a/kms/client/kms_client.c
+++ b/kms/client/kms_client.c
@@ -470,6 +470,7 @@ int gsr_kms_client_get_kms(gsr_kms_client *self, gsr_kms_response *response) {
response->version = 0;
response->result = KMS_RESULT_FAILED_TO_SEND;
response->err_msg[0] = '\0';
+ response->num_items = 0;
gsr_kms_request request;
request.version = GSR_KMS_PROTOCOL_VERSION;
diff --git a/kms/server/kms_server.c b/kms/server/kms_server.c
index c27a3b5..6714486 100644
--- a/kms/server/kms_server.c
+++ b/kms/server/kms_server.c
@@ -7,7 +7,6 @@
#include <stdio.h>
#include <string.h>
#include <errno.h>
-#include <stdlib.h>
#include <locale.h>
#include <unistd.h>
diff --git a/meson.build b/meson.build
index 43b6457..8eed3d5 100644
--- a/meson.build
+++ b/meson.build
@@ -14,6 +14,7 @@ src = [
'src/capture/xcomposite.c',
'src/capture/ximage.c',
'src/capture/kms.c',
+ 'src/capture/v4l2.c',
'src/encoder/encoder.c',
'src/encoder/video/video.c',
'src/encoder/video/nvenc.c',
diff --git a/project.conf b/project.conf
index 5671309..bd23f90 100644
--- a/project.conf
+++ b/project.conf
@@ -33,4 +33,4 @@ wayland-client = ">=1"
dbus-1 = ">=1"
libpipewire-0.3 = ">=1"
libspa-0.2 = ">=0"
-vulkan = ">=1"
+vulkan = ">=1" \ No newline at end of file
diff --git a/src/args_parser.c b/src/args_parser.c
index 7b119a6..897b918 100644
--- a/src/args_parser.c
+++ b/src/args_parser.c
@@ -9,6 +9,7 @@
#include <inttypes.h>
#include <limits.h>
#include <assert.h>
+#include <errno.h>
#include <libgen.h>
#include <sys/stat.h>
@@ -187,20 +188,22 @@ static double args_get_double_by_key(Arg *args, int num_args, const char *key, d
}
}
-static void usage_header() {
+static void usage_header(void) {
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
const char *program_name = inside_flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder" : "gpu-screen-recorder";
- printf("usage: %s -w <window_id|monitor|focused|portal|region> [-c <container_format>] [-s WxH] [-region WxH+X+Y] [-f <fps>] [-a <audio_input>] "
+ printf("usage: %s -w <window_id|monitor|focused|portal|region|v4l2_device_path> [-c <container_format>] [-s WxH] [-region WxH+X+Y] [-f <fps>] [-a <audio_input>] "
"[-q <quality>] [-r <replay_buffer_size_sec>] [-replay-storage ram|disk] [-restart-replay-on-save yes|no] "
"[-k h264|hevc|av1|vp8|vp9|hevc_hdr|av1_hdr|hevc_10bit|av1_10bit] [-ac aac|opus|flac] [-ab <bitrate>] [-oc yes|no] [-fm cfr|vfr|content] "
"[-bm auto|qp|vbr|cbr] [-cr limited|full] [-tune performance|quality] [-df yes|no] [-sc <script_path>] [-p <plugin_path>] "
"[-cursor yes|no] [-keyint <value>] [-restore-portal-session yes|no] [-portal-session-token-filepath filepath] [-encoder gpu|cpu] "
- "[-fallback-cpu-encoding yes|no] [-o <output_file>] [-ro <output_directory>] [--list-capture-options [card_path]] [--list-audio-devices] [--list-application-audio] "
- "[-v yes|no] [-gl-debug yes|no] [--version] [-h|--help]\n", program_name);
+ "[-fallback-cpu-encoding yes|no] [-o <output_file>] [-ro <output_directory>] [--list-capture-options [card_path]] [--list-audio-devices] "
+ "[--list-application-audio] [--list-v4l2-devices] [-v yes|no] [-gl-debug yes|no] [--version] [-h|--help]\n", program_name);
fflush(stdout);
}
-static void usage_full() {
+// TODO: Add --list-v4l2-devices option
+
+static void usage_full(void) {
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
usage_header();
printf("\n");
@@ -212,7 +215,7 @@ static void usage_full() {
fflush(stdout);
}
-static void usage() {
+static void usage(void) {
usage_header();
}
@@ -246,8 +249,7 @@ static bool args_parser_set_values(args_parser *self) {
self->bitrate_mode = (gsr_bitrate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-bm", GSR_BITRATE_MODE_AUTO);
self->replay_storage = (gsr_replay_storage)args_get_enum_by_key(self->args, NUM_ARGS, "-replay-storage", GSR_REPLAY_STORAGE_RAM);
- const char *window = args_get_value_by_key(self->args, NUM_ARGS, "-w");
- snprintf(self->window, sizeof(self->window), "%s", window);
+ self->capture_source = args_get_value_by_key(self->args, NUM_ARGS, "-w");
self->verbose = args_get_boolean_by_key(self->args, NUM_ARGS, "-v", true);
self->gl_debug = args_get_boolean_by_key(self->args, NUM_ARGS, "-gl-debug", false);
self->record_cursor = args_get_boolean_by_key(self->args, NUM_ARGS, "-cursor", true);
@@ -335,14 +337,9 @@ static bool args_parser_set_values(args_parser *self) {
}
}
- const char *output_resolution_str = args_get_value_by_key(self->args, NUM_ARGS, "-s");
- if(!output_resolution_str && strcmp(self->window, "focused") == 0) {
- fprintf(stderr, "gsr error: option -s is required when using '-w focused' option\n");
- usage();
- return false;
- }
-
self->output_resolution = (vec2i){0, 0};
+
+ const char *output_resolution_str = args_get_value_by_key(self->args, NUM_ARGS, "-s");
if(output_resolution_str) {
if(sscanf(output_resolution_str, "%dx%d", &self->output_resolution.x, &self->output_resolution.y) != 2) {
fprintf(stderr, "gsr error: invalid value for option -s '%s', expected a value in format WxH\n", output_resolution_str);
@@ -361,12 +358,6 @@ static bool args_parser_set_values(args_parser *self) {
self->region_position = (vec2i){0, 0};
const char *region_str = args_get_value_by_key(self->args, NUM_ARGS, "-region");
if(region_str) {
- if(strcmp(self->window, "region") != 0) {
- fprintf(stderr, "gsr error: option -region can only be used when option '-w region' is used\n");
- usage();
- return false;
- }
-
if(sscanf(region_str, "%dx%d+%d+%d", &self->region_size.x, &self->region_size.y, &self->region_position.x, &self->region_position.y) != 4) {
fprintf(stderr, "gsr error: invalid value for option -region '%s', expected a value in format WxH+X+Y\n", region_str);
usage();
@@ -378,12 +369,6 @@ static bool args_parser_set_values(args_parser *self) {
usage();
return false;
}
- } else {
- if(strcmp(self->window, "region") == 0) {
- fprintf(stderr, "gsr error: option -region is required when '-w region' is used\n");
- usage();
- return false;
- }
}
self->fps = args_get_i64_by_key(self->args, NUM_ARGS, "-f", 60);
@@ -452,10 +437,6 @@ static bool args_parser_set_values(args_parser *self) {
self->replay_recording_directory = args_get_value_by_key(self->args, NUM_ARGS, "-ro");
- const bool is_portal_capture = strcmp(self->window, "portal") == 0;
- if(!self->restore_portal_session && is_portal_capture)
- fprintf(stderr, "gsr info: option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored\n");
-
if(self->is_livestream && self->recording_saved_script) {
fprintf(stderr, "gsr warning: live stream detected, -sc script is ignored\n");
self->recording_saved_script = NULL;
@@ -493,6 +474,11 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand
return true;
}
+ if(argc == 2 && strcmp(argv[1], "--list-v4l2-devices") == 0) {
+ arg_handlers->list_v4l2_devices(userdata);
+ return true;
+ }
+
if(strcmp(argv[1], "--list-capture-options") == 0) {
if(argc == 2) {
arg_handlers->list_capture_options(NULL, userdata);
@@ -706,12 +692,6 @@ bool args_parser_validate_with_gl_info(args_parser *self, gsr_egl *egl) {
return false;
}
- const bool is_portal_capture = strcmp(self->window, "portal") == 0;
- if(video_codec_is_hdr(self->video_codec) && is_portal_capture) {
- 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");
- self->video_codec = hdr_video_codec_to_sdr_video_codec(self->video_codec);
- }
-
return true;
}
diff --git a/src/capture/kms.c b/src/capture/kms.c
index 23d7840..4f2eb7d 100644
--- a/src/capture/kms.c
+++ b/src/capture/kms.c
@@ -3,7 +3,6 @@
#include "../../include/color_conversion.h"
#include "../../include/cursor.h"
#include "../../include/window/window.h"
-#include "../../kms/client/kms_client.h"
#include <stdlib.h>
#include <string.h>
@@ -30,9 +29,6 @@ typedef struct {
typedef struct {
gsr_capture_kms_params params;
-
- gsr_kms_client kms_client;
- gsr_kms_response kms_response;
vec2i capture_pos;
vec2i capture_size;
@@ -51,7 +47,6 @@ typedef struct {
bool hdr_metadata_set;
bool is_x11;
- gsr_cursor x11_cursor;
//int drm_fd;
//uint64_t prev_sequence;
@@ -61,21 +56,12 @@ typedef struct {
vec2i prev_plane_size;
double last_time_monitor_check;
-} gsr_capture_kms;
-static void gsr_capture_kms_cleanup_kms_fds(gsr_capture_kms *self) {
- for(int i = 0; i < self->kms_response.num_items; ++i) {
- for(int j = 0; j < self->kms_response.items[i].num_dma_bufs; ++j) {
- gsr_kms_response_dma_buf *dma_buf = &self->kms_response.items[i].dma_buf[j];
- if(dma_buf->fd > 0) {
- close(dma_buf->fd);
- dma_buf->fd = -1;
- }
- }
- self->kms_response.items[i].num_dma_bufs = 0;
- }
- self->kms_response.num_items = 0;
-}
+ bool capture_is_combined_plane;
+ gsr_kms_response_item *drm_fd;
+ vec2i output_size;
+ vec2i target_pos;
+} gsr_capture_kms;
static void gsr_capture_kms_stop(gsr_capture_kms *self) {
if(self->input_texture_id) {
@@ -97,10 +83,6 @@ static void gsr_capture_kms_stop(gsr_capture_kms *self) {
// close(self->drm_fd);
// self->drm_fd = -1;
// }
-
- gsr_capture_kms_cleanup_kms_fds(self);
- gsr_kms_client_deinit(&self->kms_client);
- gsr_cursor_deinit(&self->x11_cursor);
}
static int max_int(int a, int b) {
@@ -172,16 +154,8 @@ static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture
gsr_monitor monitor;
self->monitor_id.num_connector_ids = 0;
- int kms_init_res = gsr_kms_client_init(&self->kms_client, self->params.egl->card_path);
- if(kms_init_res != 0)
- return kms_init_res;
-
self->is_x11 = gsr_window_get_display_server(self->params.egl->window) == GSR_DISPLAY_SERVER_X11;
const gsr_connection_type connection_type = self->is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM;
- if(self->is_x11) {
- Display *display = gsr_window_get_display(self->params.egl->window);
- gsr_cursor_init(&self->x11_cursor, self->params.egl, display);
- }
MonitorCallbackUserdata monitor_callback_userdata = {
&self->monitor_id,
@@ -209,29 +183,17 @@ static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture
if(self->params.output_resolution.x > 0 && self->params.output_resolution.y > 0) {
self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution);
- capture_metadata->video_width = self->params.output_resolution.x;
- capture_metadata->video_height = self->params.output_resolution.y;
+ capture_metadata->video_size = self->params.output_resolution;
} else if(self->params.region_size.x > 0 && self->params.region_size.y > 0) {
- capture_metadata->video_width = self->params.region_size.x;
- capture_metadata->video_height = self->params.region_size.y;
+ capture_metadata->video_size = self->params.region_size;
} else {
- capture_metadata->video_width = self->capture_size.x;
- capture_metadata->video_height = self->capture_size.y;
+ capture_metadata->video_size = self->capture_size;
}
self->last_time_monitor_check = clock_get_monotonic_seconds();
return 0;
}
-static void gsr_capture_kms_on_event(gsr_capture *cap, gsr_egl *egl) {
- gsr_capture_kms *self = cap->priv;
- if(!self->is_x11)
- return;
-
- XEvent *xev = gsr_window_get_event_data(egl->window);
- gsr_cursor_on_event(&self->x11_cursor, xev);
-}
-
// TODO: This is disabled for now because we want to be able to record at a framerate higher than the monitor framerate
// static void gsr_capture_kms_tick(gsr_capture *cap) {
// gsr_capture_kms *self = cap->priv;
@@ -397,14 +359,14 @@ static gsr_kms_response_item* find_monitor_drm(gsr_capture_kms *self, bool *capt
gsr_kms_response_item *drm_fd = NULL;
for(int i = 0; i < self->monitor_id.num_connector_ids; ++i) {
- drm_fd = find_drm_by_connector_id(&self->kms_response, self->monitor_id.connector_ids[i]);
+ drm_fd = find_drm_by_connector_id(self->params.kms_response, self->monitor_id.connector_ids[i]);
if(drm_fd)
break;
}
// Will never happen on wayland unless the target monitor has been disconnected
if(!drm_fd && self->is_x11) {
- drm_fd = find_largest_drm(&self->kms_response);
+ drm_fd = find_largest_drm(self->params.kms_response);
*capture_is_combined_plane = true;
}
@@ -412,7 +374,7 @@ static gsr_kms_response_item* find_monitor_drm(gsr_capture_kms *self, bool *capt
}
static gsr_kms_response_item* find_cursor_drm_if_on_monitor(gsr_capture_kms *self, uint32_t monitor_connector_id, bool capture_is_combined_plane) {
- gsr_kms_response_item *cursor_drm_fd = find_cursor_drm(&self->kms_response, monitor_connector_id);
+ gsr_kms_response_item *cursor_drm_fd = find_cursor_drm(self->params.kms_response, monitor_connector_id);
if(!capture_is_combined_plane && cursor_drm_fd && cursor_drm_fd->connector_id != monitor_connector_id)
cursor_drm_fd = NULL;
return cursor_drm_fd;
@@ -431,7 +393,7 @@ static gsr_monitor_rotation sub_rotations(gsr_monitor_rotation rot1, gsr_monitor
return remainder_int(rot1 - rot2, 4);
}
-static void render_drm_cursor(gsr_capture_kms *self, gsr_color_conversion *color_conversion, const gsr_kms_response_item *cursor_drm_fd, vec2i target_pos, vec2i output_size, vec2i framebuffer_size) {
+static void render_drm_cursor(gsr_capture_kms *self, gsr_color_conversion *color_conversion, gsr_capture_metadata *capture_metadata, const gsr_kms_response_item *cursor_drm_fd, vec2i target_pos, vec2i output_size, vec2i framebuffer_size) {
const vec2d scale = {
self->capture_size.x == 0 ? 0 : (double)output_size.x / (double)self->capture_size.x,
self->capture_size.y == 0 ? 0 : (double)output_size.y / (double)self->capture_size.y
@@ -508,13 +470,13 @@ static void render_drm_cursor(gsr_capture_kms *self, gsr_color_conversion *color
gsr_color_conversion_draw(color_conversion, self->cursor_texture_id,
cursor_pos, (vec2i){cursor_size.x * scale.x, cursor_size.y * scale.y},
(vec2i){0, 0}, cursor_size, cursor_size,
- gsr_monitor_rotation_to_rotation(rotation), GSR_SOURCE_COLOR_RGB, cursor_texture_id_is_external);
+ gsr_monitor_rotation_to_rotation(rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, cursor_texture_id_is_external);
self->params.egl->glDisable(GL_SCISSOR_TEST);
}
-static void render_x11_cursor(gsr_capture_kms *self, gsr_color_conversion *color_conversion, vec2i capture_pos, vec2i target_pos, vec2i output_size) {
- if(!self->x11_cursor.visible)
+static void render_x11_cursor(gsr_capture_kms *self, gsr_color_conversion *color_conversion, gsr_capture_metadata *capture_metadata, vec2i capture_pos, vec2i target_pos, vec2i output_size) {
+ if(!self->params.x11_cursor->visible)
return;
const vec2d scale = {
@@ -522,21 +484,18 @@ static void render_x11_cursor(gsr_capture_kms *self, gsr_color_conversion *color
self->capture_size.y == 0 ? 0 : (double)output_size.y / (double)self->capture_size.y
};
- Display *display = gsr_window_get_display(self->params.egl->window);
- gsr_cursor_tick(&self->x11_cursor, DefaultRootWindow(display));
-
const vec2i cursor_pos = {
- target_pos.x + (self->x11_cursor.position.x - self->x11_cursor.hotspot.x - capture_pos.x) * scale.x,
- target_pos.y + (self->x11_cursor.position.y - self->x11_cursor.hotspot.y - capture_pos.y) * scale.y
+ target_pos.x + (self->params.x11_cursor->position.x - self->params.x11_cursor->hotspot.x - capture_pos.x) * scale.x,
+ target_pos.y + (self->params.x11_cursor->position.y - self->params.x11_cursor->hotspot.y - capture_pos.y) * scale.y
};
self->params.egl->glEnable(GL_SCISSOR_TEST);
self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y);
- gsr_color_conversion_draw(color_conversion, self->x11_cursor.texture_id,
- cursor_pos, (vec2i){self->x11_cursor.size.x * scale.x, self->x11_cursor.size.y * scale.y},
- (vec2i){0, 0}, self->x11_cursor.size, self->x11_cursor.size,
- GSR_ROT_0, GSR_SOURCE_COLOR_RGB, false);
+ gsr_color_conversion_draw(color_conversion, self->params.x11_cursor->texture_id,
+ cursor_pos, (vec2i){self->params.x11_cursor->size.x * scale.x, self->params.x11_cursor->size.y * scale.y},
+ (vec2i){0, 0}, self->params.x11_cursor->size, self->params.x11_cursor->size,
+ GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false);
self->params.egl->glDisable(GL_SCISSOR_TEST);
}
@@ -545,7 +504,7 @@ static void gsr_capture_kms_update_capture_size_change(gsr_capture_kms *self, gs
if(target_pos.x != self->prev_target_pos.x || target_pos.y != self->prev_target_pos.y || drm_fd->src_w != self->prev_plane_size.x || drm_fd->src_h != self->prev_plane_size.y) {
self->prev_target_pos = target_pos;
self->prev_plane_size = self->capture_size;
- gsr_color_conversion_clear(color_conversion);
+ color_conversion->schedule_clear = true;
}
}
@@ -590,48 +549,47 @@ static void gsr_capture_kms_update_connector_ids(gsr_capture_kms *self) {
self->capture_size = rotate_capture_size_if_rotated(self, monitor.size);
}
-static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
+static void gsr_capture_kms_pre_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
gsr_capture_kms *self = cap->priv;
- gsr_capture_kms_cleanup_kms_fds(self);
-
- if(gsr_kms_client_get_kms(&self->kms_client, &self->kms_response) != 0) {
- fprintf(stderr, "gsr error: gsr_capture_kms_capture: failed to get kms, error: %d (%s)\n", self->kms_response.result, self->kms_response.err_msg);
- return -1;
- }
-
- if(self->kms_response.num_items == 0) {
+ if(self->params.kms_response->num_items == 0) {
static bool error_shown = false;
if(!error_shown) {
error_shown = true;
- fprintf(stderr, "gsr error: no drm found, capture will fail\n");
+ fprintf(stderr, "gsr error: gsr_capture_kms_pre_capture: no drm found, capture will fail\n");
}
- return -1;
+ return;
}
gsr_capture_kms_update_connector_ids(self);
- bool capture_is_combined_plane = false;
- const gsr_kms_response_item *drm_fd = find_monitor_drm(self, &capture_is_combined_plane);
- if(!drm_fd) {
- gsr_capture_kms_cleanup_kms_fds(self);
- return -1;
- }
+ self->capture_is_combined_plane = false;
+ self->drm_fd = find_monitor_drm(self, &self->capture_is_combined_plane);
+ if(!self->drm_fd)
+ return;
- if(drm_fd->has_hdr_metadata && self->params.hdr && hdr_metadata_is_supported_format(&drm_fd->hdr_metadata))
- gsr_kms_set_hdr_metadata(self, drm_fd);
+ if(self->drm_fd->has_hdr_metadata && self->params.hdr && hdr_metadata_is_supported_format(&self->drm_fd->hdr_metadata))
+ gsr_kms_set_hdr_metadata(self, self->drm_fd);
- self->capture_size = rotate_capture_size_if_rotated(self, (vec2i){ drm_fd->src_w, drm_fd->src_h });
+ self->capture_size = rotate_capture_size_if_rotated(self, (vec2i){ self->drm_fd->src_w, self->drm_fd->src_h });
if(self->params.region_size.x > 0 && self->params.region_size.y > 0)
self->capture_size = self->params.region_size;
- const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, (vec2i){capture_metadata->recording_width, capture_metadata->recording_height});
- const vec2i target_pos = { max_int(0, capture_metadata->video_width / 2 - output_size.x / 2), max_int(0, capture_metadata->video_height / 2 - output_size.y / 2) };
- gsr_capture_kms_update_capture_size_change(self, color_conversion, target_pos, drm_fd);
+ self->output_size = scale_keep_aspect_ratio(self->capture_size, capture_metadata->recording_size);
+ self->target_pos = gsr_capture_get_target_position(self->output_size, capture_metadata);
+ gsr_capture_kms_update_capture_size_change(self, color_conversion, self->target_pos, self->drm_fd);
+}
+
+static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
+ (void)capture_metadata;
+ gsr_capture_kms *self = cap->priv;
+
+ if(self->params.kms_response->num_items == 0)
+ return -1;
vec2i capture_pos = self->capture_pos;
- if(!capture_is_combined_plane)
- capture_pos = (vec2i){drm_fd->x, drm_fd->y};
+ if(!self->capture_is_combined_plane)
+ capture_pos = (vec2i){self->drm_fd->x, self->drm_fd->y};
capture_pos.x += self->params.region_position.x;
capture_pos.y += self->params.region_position.y;
@@ -639,22 +597,22 @@ static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *captu
//self->params.egl->glFlush();
//self->params.egl->glFinish();
- EGLImage image = gsr_capture_kms_create_egl_image_with_fallback(self, drm_fd);
+ EGLImage image = gsr_capture_kms_create_egl_image_with_fallback(self, self->drm_fd);
if(image) {
gsr_capture_kms_bind_image_to_input_texture_with_fallback(self, image);
self->params.egl->eglDestroyImage(self->params.egl->egl_display, image);
}
- const gsr_monitor_rotation plane_rotation = kms_rotation_to_gsr_monitor_rotation(drm_fd->rotation);
- const gsr_monitor_rotation rotation = capture_is_combined_plane ? GSR_MONITOR_ROT_0 : sub_rotations(self->monitor_rotation, plane_rotation);
+ const gsr_monitor_rotation plane_rotation = kms_rotation_to_gsr_monitor_rotation(self->drm_fd->rotation);
+ const gsr_monitor_rotation rotation = self->capture_is_combined_plane ? GSR_MONITOR_ROT_0 : sub_rotations(self->monitor_rotation, plane_rotation);
gsr_color_conversion_draw(color_conversion, self->external_texture_fallback ? self->external_input_texture_id : self->input_texture_id,
- target_pos, output_size,
- capture_pos, self->capture_size, (vec2i){ drm_fd->width, drm_fd->height },
- gsr_monitor_rotation_to_rotation(rotation), GSR_SOURCE_COLOR_RGB, self->external_texture_fallback);
+ self->target_pos, self->output_size,
+ capture_pos, self->capture_size, (vec2i){ self->drm_fd->width, self->drm_fd->height },
+ gsr_monitor_rotation_to_rotation(rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, self->external_texture_fallback);
if(self->params.record_cursor) {
- gsr_kms_response_item *cursor_drm_fd = find_cursor_drm_if_on_monitor(self, drm_fd->connector_id, capture_is_combined_plane);
+ gsr_kms_response_item *cursor_drm_fd = find_cursor_drm_if_on_monitor(self, self->drm_fd->connector_id, self->capture_is_combined_plane);
// The cursor is handled by x11 on x11 instead of using the cursor drm plane because on prime systems with a dedicated nvidia gpu
// the cursor plane is not available when the cursor is on the monitor controlled by the nvidia device.
// TODO: This doesn't work properly with software cursor on x11 since it will draw the x11 cursor on top of the cursor already in the framebuffer.
@@ -663,18 +621,16 @@ static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *captu
vec2i cursor_monitor_offset = self->capture_pos;
cursor_monitor_offset.x += self->params.region_position.x;
cursor_monitor_offset.y += self->params.region_position.y;
- render_x11_cursor(self, color_conversion, cursor_monitor_offset, target_pos, output_size);
+ render_x11_cursor(self, color_conversion, capture_metadata, cursor_monitor_offset, self->target_pos, self->output_size);
} else if(cursor_drm_fd) {
- const vec2i framebuffer_size = rotate_capture_size_if_rotated(self, (vec2i){ drm_fd->src_w, drm_fd->src_h });
- render_drm_cursor(self, color_conversion, cursor_drm_fd, target_pos, output_size, framebuffer_size);
+ const vec2i framebuffer_size = rotate_capture_size_if_rotated(self, (vec2i){ self->drm_fd->src_w, self->drm_fd->src_h });
+ render_drm_cursor(self, color_conversion, capture_metadata, cursor_drm_fd, self->target_pos, self->output_size, framebuffer_size);
}
}
//self->params.egl->glFlush();
//self->params.egl->glFinish();
- gsr_capture_kms_cleanup_kms_fds(self);
-
return 0;
}
@@ -766,9 +722,9 @@ gsr_capture* gsr_capture_kms_create(const gsr_capture_kms_params *params) {
*cap = (gsr_capture) {
.start = gsr_capture_kms_start,
- .on_event = gsr_capture_kms_on_event,
//.tick = gsr_capture_kms_tick,
.should_stop = gsr_capture_kms_should_stop,
+ .pre_capture = gsr_capture_kms_pre_capture,
.capture = gsr_capture_kms_capture,
.uses_external_image = gsr_capture_kms_uses_external_image,
.set_hdr_metadata = gsr_capture_kms_set_hdr_metadata,
diff --git a/src/capture/nvfbc.c b/src/capture/nvfbc.c
index bd6c2b1..144f88a 100644
--- a/src/capture/nvfbc.c
+++ b/src/capture/nvfbc.c
@@ -284,16 +284,14 @@ static int gsr_capture_nvfbc_start(gsr_capture *cap, gsr_capture_metadata *captu
goto error_cleanup;
}
- capture_metadata->video_width = self->tracking_width;
- capture_metadata->video_height = self->tracking_height;
+ capture_metadata->video_size.x = self->tracking_width;
+ capture_metadata->video_size.y = self->tracking_height;
if(self->params.output_resolution.x > 0 && self->params.output_resolution.y > 0) {
- self->params.output_resolution = scale_keep_aspect_ratio((vec2i){capture_metadata->video_width, capture_metadata->video_height}, self->params.output_resolution);
- capture_metadata->video_width = self->params.output_resolution.x;
- capture_metadata->video_height = self->params.output_resolution.y;
+ self->params.output_resolution = scale_keep_aspect_ratio(capture_metadata->video_size, self->params.output_resolution);
+ capture_metadata->video_size = self->params.output_resolution;
} else if(self->params.region_size.x > 0 && self->params.region_size.y > 0) {
- capture_metadata->video_width = self->params.region_size.x;
- capture_metadata->video_height = self->params.region_size.y;
+ capture_metadata->video_size = self->params.region_size;
}
return 0;
@@ -371,8 +369,8 @@ static int gsr_capture_nvfbc_capture(gsr_capture *cap, gsr_capture_metadata *cap
if(self->params.region_size.x > 0 && self->params.region_size.y > 0)
frame_size = self->params.region_size;
- const vec2i output_size = scale_keep_aspect_ratio(frame_size, (vec2i){capture_metadata->recording_width, capture_metadata->recording_height});
- const vec2i target_pos = { max_int(0, capture_metadata->video_width / 2 - output_size.x / 2), max_int(0, capture_metadata->video_height / 2 - output_size.y / 2) };
+ const vec2i output_size = scale_keep_aspect_ratio(frame_size, capture_metadata->recording_size);
+ const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata);
NVFBC_FRAME_GRAB_INFO frame_info;
memset(&frame_info, 0, sizeof(frame_info));
@@ -398,7 +396,7 @@ static int gsr_capture_nvfbc_capture(gsr_capture *cap, gsr_capture_metadata *cap
gsr_color_conversion_draw(color_conversion, self->setup_params.dwTextures[grab_params.dwTextureIndex],
target_pos, (vec2i){output_size.x, output_size.y},
self->params.region_position, frame_size, original_frame_size,
- GSR_ROT_0, GSR_SOURCE_COLOR_BGR, false);
+ GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_BGR, false);
//self->params.egl->glFlush();
//self->params.egl->glFinish();
diff --git a/src/capture/portal.c b/src/capture/portal.c
index 2d4eff1..c6f058d 100644
--- a/src/capture/portal.c
+++ b/src/capture/portal.c
@@ -35,6 +35,7 @@ typedef struct {
bool should_stop;
bool stop_is_error;
+ bool do_capture;
} gsr_capture_portal;
static void gsr_capture_portal_cleanup_plane_fds(gsr_capture_portal *self) {
@@ -293,12 +294,10 @@ static int gsr_capture_portal_start(gsr_capture *cap, gsr_capture_metadata *capt
}
if(self->params.output_resolution.x == 0 && self->params.output_resolution.y == 0) {
- capture_metadata->video_width = self->capture_size.x;
- capture_metadata->video_height = self->capture_size.y;
+ capture_metadata->video_size = self->capture_size;
} else {
self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution);
- capture_metadata->video_width = self->params.output_resolution.x;
- capture_metadata->video_height = self->params.output_resolution.y;
+ capture_metadata->video_size = self->params.output_resolution;
}
return 0;
@@ -322,22 +321,22 @@ static bool fourcc_has_alpha(uint32_t fourcc) {
return false;
}
-static int gsr_capture_portal_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
- (void)color_conversion;
+static void gsr_capture_portal_pre_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
gsr_capture_portal *self = cap->priv;
+ self->do_capture = false;
if(self->should_stop)
- return -1;
+ return;
if(gsr_pipewire_video_should_restart(&self->pipewire)) {
- fprintf(stderr, "gsr info: gsr_capture_portal_capture: pipewire capture was paused, trying to start capture again\n");
+ fprintf(stderr, "gsr info: gsr_capture_portal_pre_capture: pipewire capture was paused, trying to start capture again\n");
gsr_capture_portal_stop(self);
const int result = gsr_capture_portal_setup(self, capture_metadata->fps);
if(result != 0) {
self->stop_is_error = result != PORTAL_CAPTURE_CANCELED_BY_USER_EXIT_CODE;
self->should_stop = true;
}
- return -1;
+ return;
}
/* TODO: Handle formats other than RGB(A) */
@@ -346,15 +345,29 @@ static int gsr_capture_portal_capture(gsr_capture *cap, gsr_capture_metadata *ca
if(self->pipewire_data.region.width != self->capture_size.x || self->pipewire_data.region.height != self->capture_size.y) {
self->capture_size.x = self->pipewire_data.region.width;
self->capture_size.y = self->pipewire_data.region.height;
- gsr_color_conversion_clear(color_conversion);
+ color_conversion->schedule_clear = true;
}
} else {
- return -1;
+ return;
}
}
- const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, (vec2i){capture_metadata->recording_width, capture_metadata->recording_height});
- const vec2i target_pos = { max_int(0, capture_metadata->video_width / 2 - output_size.x / 2), max_int(0, capture_metadata->video_height / 2 - output_size.y / 2) };
+ const bool fourcc_alpha = fourcc_has_alpha(self->pipewire_data.fourcc);
+ if(fourcc_alpha)
+ color_conversion->schedule_clear = true;
+
+ self->do_capture = true;
+}
+
+static int gsr_capture_portal_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
+ (void)color_conversion;
+ gsr_capture_portal *self = cap->priv;
+
+ if(self->should_stop || !self->do_capture)
+ return -1;
+
+ const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, capture_metadata->recording_size);
+ const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata);
const vec2i actual_texture_size = {self->pipewire_data.texture_width, self->pipewire_data.texture_height};
@@ -363,14 +376,10 @@ static int gsr_capture_portal_capture(gsr_capture *cap, gsr_capture_metadata *ca
// TODO: Handle region crop
- const bool fourcc_alpha = fourcc_has_alpha(self->pipewire_data.fourcc);
- if(fourcc_alpha)
- gsr_color_conversion_clear(color_conversion);
-
gsr_color_conversion_draw(color_conversion, self->pipewire_data.using_external_image ? self->texture_map.external_texture_id : self->texture_map.texture_id,
target_pos, output_size,
(vec2i){self->pipewire_data.region.x, self->pipewire_data.region.y}, (vec2i){self->pipewire_data.region.width, self->pipewire_data.region.height}, actual_texture_size,
- gsr_monitor_rotation_to_rotation(self->pipewire_data.rotation), GSR_SOURCE_COLOR_RGB, self->pipewire_data.using_external_image);
+ gsr_monitor_rotation_to_rotation(self->pipewire_data.rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, self->pipewire_data.using_external_image);
if(self->params.record_cursor && self->texture_map.cursor_texture_id > 0 && self->pipewire_data.cursor_region.width > 0) {
const vec2d scale = {
@@ -385,13 +394,15 @@ static int gsr_capture_portal_capture(gsr_capture *cap, gsr_capture_metadata *ca
self->params.egl->glEnable(GL_SCISSOR_TEST);
self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y);
+
gsr_color_conversion_draw(color_conversion, self->texture_map.cursor_texture_id,
(vec2i){cursor_pos.x, cursor_pos.y},
(vec2i){self->pipewire_data.cursor_region.width * scale.x, self->pipewire_data.cursor_region.height * scale.y},
(vec2i){0, 0},
(vec2i){self->pipewire_data.cursor_region.width, self->pipewire_data.cursor_region.height},
(vec2i){self->pipewire_data.cursor_region.width, self->pipewire_data.cursor_region.height},
- gsr_monitor_rotation_to_rotation(self->pipewire_data.rotation), GSR_SOURCE_COLOR_RGB, false);
+ gsr_monitor_rotation_to_rotation(self->pipewire_data.rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false);
+
self->params.egl->glDisable(GL_SCISSOR_TEST);
}
@@ -458,6 +469,7 @@ gsr_capture* gsr_capture_portal_create(const gsr_capture_portal_params *params)
.tick = NULL,
.should_stop = gsr_capture_portal_should_stop,
.capture_has_synchronous_task = gsr_capture_portal_capture_has_synchronous_task,
+ .pre_capture = gsr_capture_portal_pre_capture,
.capture = gsr_capture_portal_capture,
.uses_external_image = gsr_capture_portal_uses_external_image,
.is_damaged = gsr_capture_portal_is_damaged,
diff --git a/src/capture/v4l2.c b/src/capture/v4l2.c
new file mode 100644
index 0000000..1dade2f
--- /dev/null
+++ b/src/capture/v4l2.c
@@ -0,0 +1,656 @@
+#include "../../include/capture/v4l2.h"
+#include "../../include/color_conversion.h"
+#include "../../include/egl.h"
+#include "../../include/utils.h"
+
+#include <dlfcn.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <linux/videodev2.h>
+#include <linux/dma-buf.h>
+#include <drm_fourcc.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <assert.h>
+
+#define TJPF_RGB 0
+#define TJPF_RGBA 7
+#define TJFLAG_FASTDCT 2048
+
+#define NUM_BUFFERS 2
+#define NUM_PBOS 2
+
+typedef void* tjhandle;
+typedef tjhandle (*FUNC_tjInitDecompress)(void);
+typedef int (*FUNC_tjDestroy)(tjhandle handle);
+typedef int (*FUNC_tjDecompressHeader2)(tjhandle handle,
+ unsigned char *jpegBuf, unsigned long jpegSize,
+ int *width, int *height, int *jpegSubsamp);
+typedef int (*FUNC_tjDecompress2)(tjhandle handle, const unsigned char *jpegBuf,
+ unsigned long jpegSize, unsigned char *dstBuf,
+ int width, int pitch, int height, int pixelFormat,
+ int flags);
+typedef char* (*FUNC_tjGetErrorStr2)(tjhandle handle);
+
+typedef enum {
+ V4L2_BUFFER_TYPE_DMABUF,
+ V4L2_BUFFER_TYPE_MMAP
+} v4l2_buffer_type;
+
+typedef struct {
+ gsr_capture_v4l2_params params;
+ vec2i capture_size;
+
+ bool should_stop;
+ bool stop_is_error;
+
+ int fd;
+ int dmabuf_fd[NUM_BUFFERS];
+ EGLImage dma_image[NUM_BUFFERS];
+ unsigned int texture_id;
+ bool got_first_frame;
+
+ void *dmabuf_map[NUM_BUFFERS];
+ size_t dmabuf_size[NUM_BUFFERS];
+ unsigned int pbos[NUM_PBOS];
+ unsigned int pbo_index;
+
+ v4l2_buffer_type buffer_type;
+
+ void *libturbojpeg_lib;
+ FUNC_tjInitDecompress tjInitDecompress;
+ FUNC_tjDestroy tjDestroy;
+ FUNC_tjDecompressHeader2 tjDecompressHeader2;
+ FUNC_tjDecompress2 tjDecompress2;
+ FUNC_tjGetErrorStr2 tjGetErrorStr2;
+ tjhandle jpeg_decompressor;
+
+ double capture_start_time;
+} gsr_capture_v4l2;
+
+static int xioctl(int fd, unsigned long request, void *arg) {
+ int r;
+
+ do {
+ r = ioctl(fd, request, arg);
+ } while (-1 == r && EINTR == errno);
+
+ return r;
+}
+
+static void gsr_capture_v4l2_stop(gsr_capture_v4l2 *self) {
+ self->params.egl->glDeleteBuffers(NUM_PBOS, self->pbos);
+ for(int i = 0; i < NUM_PBOS; ++i) {
+ self->pbos[i] = 0;
+ }
+
+ if(self->texture_id) {
+ self->params.egl->glDeleteTextures(1, &self->texture_id);
+ self->texture_id = 0;
+ }
+
+ for(int i = 0; i < NUM_BUFFERS; ++i) {
+ if(self->dmabuf_map[i]) {
+ munmap(self->dmabuf_map[i], self->dmabuf_size[i]);
+ self->dmabuf_map[i] = NULL;
+ }
+
+ if(self->dma_image[i]) {
+ self->params.egl->eglDestroyImage(self->params.egl->egl_display, self->dma_image[i]);
+ self->dma_image[i] = NULL;
+ }
+
+ if(self->dmabuf_fd[i] > 0) {
+ close(self->dmabuf_fd[i]);
+ self->dmabuf_fd[i] = 0;
+ }
+ }
+
+ if(self->fd > 0) {
+ xioctl(self->fd, VIDIOC_STREAMOFF, &(enum v4l2_buf_type){V4L2_BUF_TYPE_VIDEO_CAPTURE});
+ close(self->fd);
+ self->fd = 0;
+ }
+
+ if(self->jpeg_decompressor) {
+ self->tjDestroy(self->jpeg_decompressor);
+ self->jpeg_decompressor = NULL;
+ }
+
+ if(self->libturbojpeg_lib) {
+ dlclose(self->libturbojpeg_lib);
+ self->libturbojpeg_lib = NULL;
+ }
+}
+
+static void gsr_capture_v4l2_reset_cropping(gsr_capture_v4l2 *self) {
+ struct v4l2_cropcap cropcap = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE
+ };
+ if(xioctl(self->fd, VIDIOC_CROPCAP, &cropcap) == 0) {
+ struct v4l2_crop crop = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ .c = cropcap.defrect /* reset to default */
+ };
+
+ if(xioctl(self->fd, VIDIOC_S_CROP, &crop) == -1) {
+ switch (errno) {
+ case EINVAL:
+ /* Cropping not supported. */
+ break;
+ default:
+ /* Errors ignored. */
+ break;
+ }
+ }
+ } else {
+ /* Errors ignored. */
+ }
+}
+
+gsr_capture_v4l2_supported_pixfmts gsr_capture_v4l2_get_supported_pixfmts(int fd) {
+ gsr_capture_v4l2_supported_pixfmts result = {0};
+
+ struct v4l2_fmtdesc fmt = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE
+ };
+ while(xioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0) {
+ //fprintf(stderr, "fmt: %d, desc: %s, flags: %d\n", fmt.pixelformat, fmt.description, fmt.flags);
+ switch(fmt.pixelformat) {
+ case V4L2_PIX_FMT_YUYV:
+ result.yuyv = true;
+ break;
+ case V4L2_PIX_FMT_MJPEG:
+ result.mjpeg = true;
+ break;
+ }
+ ++fmt.index;
+ }
+
+ return result;
+}
+
+static uint32_t gsr_pixfmt_to_v4l2_pixfmt(gsr_capture_v4l2_pixfmt pixfmt) {
+ switch(pixfmt) {
+ case GSR_CAPTURE_V4L2_PIXFMT_AUTO:
+ assert(false);
+ break;
+ case GSR_CAPTURE_V4L2_PIXFMT_YUYV:
+ return V4L2_PIX_FMT_YUYV;
+ case GSR_CAPTURE_V4L2_PIXFMT_MJPEG:
+ return V4L2_PIX_FMT_MJPEG;
+ }
+ assert(false);
+ return V4L2_PIX_FMT_YUYV;
+}
+
+static bool gsr_capture_v4l2_validate_pixfmt(gsr_capture_v4l2 *self, const gsr_capture_v4l2_supported_pixfmts supported_pixfmts) {
+ switch(self->params.pixfmt) {
+ case GSR_CAPTURE_V4L2_PIXFMT_AUTO: {
+ if(supported_pixfmts.yuyv) {
+ self->params.pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV;
+ } else if(supported_pixfmts.mjpeg) {
+ self->params.pixfmt = GSR_CAPTURE_V4L2_PIXFMT_MJPEG;
+ } else {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support yuyv nor mjpeg. GPU Screen Recorder supports only yuyv and mjpeg at the moment. Report this as an issue, see: https://git.dec05eba.com/?p=about\n", self->params.device_path);
+ return false;
+ }
+ break;
+ }
+ case GSR_CAPTURE_V4L2_PIXFMT_YUYV: {
+ if(!supported_pixfmts.yuyv) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support yuyv. Try recording with -pixfmt mjpeg or -pixfmt auto instead\n", self->params.device_path);
+ return false;
+ }
+ break;
+ }
+ case GSR_CAPTURE_V4L2_PIXFMT_MJPEG: {
+ if(!supported_pixfmts.mjpeg) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support mjpeg. Try recording with -pixfmt yuyv or -pixfmt auto instead\n", self->params.device_path);
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+static bool gsr_capture_v4l2_create_pbos(gsr_capture_v4l2 *self, int width, int height) {
+ self->pbo_index = 0;
+
+ self->params.egl->glGenBuffers(NUM_PBOS, self->pbos);
+ for(int i = 0; i < NUM_PBOS; ++i) {
+ if(self->pbos[i] == 0) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create_pbos: failed to create pixel buffer objects\n");
+ return false;
+ }
+
+ self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, self->pbos[i]);
+ self->params.egl->glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4, 0, GL_DYNAMIC_DRAW);
+ }
+
+ self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ return true;
+}
+
+static bool gsr_capture_v4l2_map_buffer(gsr_capture_v4l2 *self, const struct v4l2_format *fmt) {
+ switch(self->params.pixfmt) {
+ case GSR_CAPTURE_V4L2_PIXFMT_AUTO: {
+ assert(false);
+ return false;
+ }
+ case GSR_CAPTURE_V4L2_PIXFMT_YUYV: {
+ for(int i = 0; i < NUM_BUFFERS; ++i) {
+ self->dma_image[i] = self->params.egl->eglCreateImage(self->params.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, (intptr_t[]) {
+ EGL_WIDTH, fmt->fmt.pix.width,
+ EGL_HEIGHT, fmt->fmt.pix.height,
+ EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_YUYV,
+ EGL_DMA_BUF_PLANE0_FD_EXT, self->dmabuf_fd[i],
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, fmt->fmt.pix.bytesperline,
+ EGL_NONE
+ });
+ if(!self->dma_image[i]) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: eglCreateImage failed, error: %d\n", self->params.egl->eglGetError());
+ return false;
+ }
+ }
+
+ self->params.egl->glGenTextures(1, &self->texture_id);
+ self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->texture_id);
+ self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+ if(self->texture_id == 0) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: failed to create texture\n");
+ return false;
+ }
+
+ self->buffer_type = V4L2_BUFFER_TYPE_DMABUF;
+ break;
+ }
+ case GSR_CAPTURE_V4L2_PIXFMT_MJPEG: {
+ for(int i = 0; i < NUM_BUFFERS; ++i) {
+ self->dmabuf_size[i] = fmt->fmt.pix.sizeimage;
+ self->dmabuf_map[i] = mmap(NULL, fmt->fmt.pix.sizeimage, PROT_READ, MAP_SHARED, self->dmabuf_fd[i], 0);
+ if(self->dmabuf_map[i] == MAP_FAILED) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: mmap failed, error: %s\n", strerror(errno));
+ return false;
+ }
+ }
+
+ // GL_RGBA is intentionally used here instead of GL_RGB, because the performance is much better when using glTexSubImage2D (22% cpu usage compared to 38% cpu usage)
+ self->texture_id = gl_create_texture(self->params.egl, fmt->fmt.pix.width, fmt->fmt.pix.height, GL_RGBA8, GL_RGBA, GL_LINEAR);
+ if(self->texture_id == 0) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: failed to create texture\n");
+ return false;
+ }
+
+ if(!gsr_capture_v4l2_create_pbos(self, fmt->fmt.pix.width, fmt->fmt.pix.height))
+ return false;
+
+ self->buffer_type = V4L2_BUFFER_TYPE_MMAP;
+ break;
+ }
+ }
+ return true;
+}
+
+static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) {
+ self->fd = open(self->params.device_path, O_RDWR | O_NONBLOCK);
+ if(self->fd < 0) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to open %s, error: %s\n", self->params.device_path, strerror(errno));
+ return -1;
+ }
+
+ struct v4l2_capability cap = {0};
+ if(xioctl(self->fd, VIDIOC_QUERYCAP, &cap) == -1) {
+ if(EINVAL == errno) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s isn't a v4l2 device\n", self->params.device_path);
+ return -1;
+ } else {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_QUERYCAP failed, error: %s\n", strerror(errno));
+ return -1;
+ }
+ }
+
+ if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s isn't a video capture device\n", self->params.device_path);
+ return -1;
+ }
+
+ if(!(cap.capabilities & V4L2_CAP_STREAMING)) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support streaming i/o\n", self->params.device_path);
+ return -1;
+ }
+
+ gsr_capture_v4l2_reset_cropping(self);
+
+ const gsr_capture_v4l2_supported_pixfmts supported_pixfmts = gsr_capture_v4l2_get_supported_pixfmts(self->fd);
+ if(!gsr_capture_v4l2_validate_pixfmt(self, supported_pixfmts))
+ return -1;
+
+ if(self->params.pixfmt == GSR_CAPTURE_V4L2_PIXFMT_MJPEG) {
+ dlerror(); /* clear */
+ self->libturbojpeg_lib = dlopen("libturbojpeg.so.0", RTLD_LAZY);
+ if(!self->libturbojpeg_lib) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to load libturbojpeg.so.0 which is required for camera mjpeg capture, error: %s\n", dlerror());
+ return -1;
+ }
+
+ self->tjInitDecompress = (FUNC_tjInitDecompress)dlsym(self->libturbojpeg_lib, "tjInitDecompress");
+ self->tjDestroy = (FUNC_tjDestroy)dlsym(self->libturbojpeg_lib, "tjDestroy");
+ self->tjDecompressHeader2 = (FUNC_tjDecompressHeader2)dlsym(self->libturbojpeg_lib, "tjDecompressHeader2");
+ self->tjDecompress2 = (FUNC_tjDecompress2)dlsym(self->libturbojpeg_lib, "tjDecompress2");
+ self->tjGetErrorStr2 = (FUNC_tjGetErrorStr2)dlsym(self->libturbojpeg_lib, "tjGetErrorStr2");
+
+ if(!self->tjInitDecompress || !self->tjDestroy || !self->tjDecompressHeader2 || !self->tjDecompress2 || !self->tjGetErrorStr2) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: libturbojpeg.so.0 is missing functions. The libturbojpeg version installed on your system might be outdated\n");
+ return -1;
+ }
+
+ self->jpeg_decompressor = self->tjInitDecompress();
+ if(!self->jpeg_decompressor) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to create jpeg decompressor\n");
+ return -1;
+ }
+ }
+
+ const uint32_t v4l2_pixfmt = gsr_pixfmt_to_v4l2_pixfmt(self->params.pixfmt);
+ struct v4l2_format fmt = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ .fmt.pix.pixelformat = v4l2_pixfmt
+ };
+ if(xioctl(self->fd, VIDIOC_S_FMT, &fmt) == -1) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_S_FMT failed, error: %s\n", strerror(errno));
+ return -1;
+ }
+
+ if(fmt.fmt.pix.pixelformat != v4l2_pixfmt) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: pixel format isn't as requested (got pixel format: %u, requested: %u), error: %s\n", fmt.fmt.pix.pixelformat, v4l2_pixfmt, strerror(errno));
+ return -1;
+ }
+
+ /* Buggy driver paranoia */
+ const uint32_t min_stride = fmt.fmt.pix.width * 2; // * 2 because the stride is width (Y) + width/2 (U) + width/2 (V)
+ if(fmt.fmt.pix.bytesperline < min_stride)
+ fmt.fmt.pix.bytesperline = min_stride;
+
+ const uint32_t min_size = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height;
+ if(fmt.fmt.pix.sizeimage < min_size)
+ fmt.fmt.pix.sizeimage = min_size;
+
+ self->capture_size.x = fmt.fmt.pix.width;
+ self->capture_size.y = fmt.fmt.pix.height;
+
+ struct v4l2_requestbuffers reqbuf = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ .memory = V4L2_MEMORY_MMAP,
+ .count = NUM_BUFFERS
+ };
+ if(xioctl(self->fd, VIDIOC_REQBUFS, &reqbuf) == -1) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_REQBUFS failed, error: %s\n", strerror(errno));
+ return -1;
+ }
+
+ for(int i = 0; i < NUM_BUFFERS; ++i) {
+ struct v4l2_exportbuffer expbuf = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ .index = i,
+ .flags = O_RDONLY
+ };
+ if(xioctl(self->fd, VIDIOC_EXPBUF, &expbuf) == -1) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_EXPBUF failed, error: %s\n", strerror(errno));
+ return -1;
+ }
+ self->dmabuf_fd[i] = expbuf.fd;
+ }
+
+ if(!gsr_capture_v4l2_map_buffer(self, &fmt))
+ return -1;
+
+ for(int i = 0; i < NUM_BUFFERS; ++i) {
+ struct v4l2_buffer buf = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ .index = i,
+ .memory = V4L2_MEMORY_MMAP
+ };
+ xioctl(self->fd, VIDIOC_QBUF, &buf);
+ }
+
+ if(xioctl(self->fd, VIDIOC_STREAMON, &(enum v4l2_buf_type){V4L2_BUF_TYPE_VIDEO_CAPTURE})) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_STREAMON failed, error: %s\n", strerror(errno));
+ return -1;
+ }
+
+ fprintf(stderr, "gsr info: gsr_capture_v4l2_create: waiting for camera %s to be ready\n", self->params.device_path);
+ return 0;
+}
+
+static int gsr_capture_v4l2_start(gsr_capture *cap, gsr_capture_metadata *capture_metadata) {
+ gsr_capture_v4l2 *self = cap->priv;
+
+ const int result = gsr_capture_v4l2_setup(self);
+ if(result != 0) {
+ gsr_capture_v4l2_stop(self);
+ return result;
+ }
+
+ if(self->params.output_resolution.x == 0 && self->params.output_resolution.y == 0) {
+ capture_metadata->video_size = self->capture_size;
+ } else {
+ self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution);
+ capture_metadata->video_size = self->params.output_resolution;
+ }
+
+ self->capture_start_time = clock_get_monotonic_seconds();
+ return 0;
+}
+
+static void gsr_capture_v4l2_tick(gsr_capture *cap) {
+ gsr_capture_v4l2 *self = cap->priv;
+ if(!self->got_first_frame && !self->should_stop) {
+ const double timeout_sec = 5.0;
+ if(clock_get_monotonic_seconds() - self->capture_start_time >= timeout_sec) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: didn't receive camera data in %f seconds\n", timeout_sec);
+ self->should_stop = true;
+ self->stop_is_error = true;
+ }
+ }
+}
+
+static void gsr_capture_v4l2_decode_jpeg_to_texture(gsr_capture_v4l2 *self, const struct v4l2_buffer *buf) {
+ int jpeg_subsamp = 0;
+ int jpeg_width = 0;
+ int jpeg_height = 0;
+ if(self->tjDecompressHeader2(self->jpeg_decompressor, self->dmabuf_map[buf->index], buf->bytesused, &jpeg_width, &jpeg_height, &jpeg_subsamp) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: failed to decompress camera jpeg header data, error: %s\n", self->tjGetErrorStr2(self->jpeg_decompressor));
+ return;
+ }
+
+ if(jpeg_width != self->capture_size.x || jpeg_height != self->capture_size.y) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: got jpeg data of incorrect dimensions. Expected %dx%d, got %dx%d\n", self->capture_size.x, self->capture_size.y, jpeg_width, jpeg_height);
+ return;
+ }
+
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, self->texture_id);
+
+ self->pbo_index = (self->pbo_index + 1) % NUM_PBOS;
+ const unsigned int next_pbo_index = (self->pbo_index + 1) % NUM_PBOS;
+
+ self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, self->pbos[self->pbo_index]);
+ self->params.egl->glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, self->capture_size.x, self->capture_size.y, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+
+ self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, self->pbos[next_pbo_index]);
+ self->params.egl->glBufferData(GL_PIXEL_UNPACK_BUFFER, self->capture_size.x * self->capture_size.y * 4, 0, GL_DYNAMIC_DRAW);
+
+ void *mapped_buffer = self->params.egl->glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
+ if(mapped_buffer) {
+ if(self->tjDecompress2(self->jpeg_decompressor, self->dmabuf_map[buf->index], buf->bytesused, mapped_buffer, jpeg_width, 0, jpeg_height, TJPF_RGBA, TJFLAG_FASTDCT) != 0)
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: failed to decompress camera jpeg data, error: %s\n", self->tjGetErrorStr2(self->jpeg_decompressor));
+ self->params.egl->glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ }
+
+ self->params.egl->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+static int gsr_capture_v4l2_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
+ (void)color_conversion;
+ gsr_capture_v4l2 *self = cap->priv;
+
+ struct v4l2_buffer buf = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ .memory = V4L2_MEMORY_MMAP
+ };
+
+ xioctl(self->fd, VIDIOC_DQBUF, &buf);
+ if(buf.bytesused > 0 && !(buf.flags & V4L2_BUF_FLAG_ERROR)) {
+ if(!self->got_first_frame)
+ fprintf(stderr, "gsr info: gsr_capture_v4l2_capture: camera %s is now ready\n", self->params.device_path);
+ self->got_first_frame = true;
+
+ switch(self->buffer_type) {
+ case V4L2_BUFFER_TYPE_DMABUF: {
+ self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->texture_id);
+ self->params.egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, self->dma_image[buf.index]);
+ self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+ break;
+ }
+ case V4L2_BUFFER_TYPE_MMAP: {
+ //xioctl(self->dmabuf_fd[buf.index], DMA_BUF_IOCTL_SYNC, &(struct dma_buf_sync){ .flags = DMA_BUF_SYNC_START });
+ gsr_capture_v4l2_decode_jpeg_to_texture(self, &buf);
+ //xioctl(self->dmabuf_fd[buf.index], DMA_BUF_IOCTL_SYNC, &(struct dma_buf_sync){ .flags = DMA_BUF_SYNC_END });
+ break;
+ }
+ }
+ }
+ xioctl(self->fd, VIDIOC_QBUF, &buf);
+
+ const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, capture_metadata->recording_size);
+ const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata);
+
+ //if(self->got_first_frame) {
+ gsr_color_conversion_draw(color_conversion, self->texture_id,
+ target_pos, output_size,
+ (vec2i){0, 0}, self->capture_size, self->capture_size,
+ GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, self->buffer_type == V4L2_BUFFER_TYPE_DMABUF);
+ //}
+
+ return self->got_first_frame ? 0 : -1;
+}
+
+static bool gsr_capture_v4l2_uses_external_image(gsr_capture *cap) {
+ (void)cap;
+ return true;
+}
+
+static bool gsr_capture_v4l2_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_v4l2 *self = cap->priv;
+ if(err)
+ *err = self->stop_is_error;
+ return self->should_stop;
+}
+
+static bool gsr_capture_v4l2_is_damaged(gsr_capture *cap) {
+ gsr_capture_v4l2 *self = cap->priv;
+ struct pollfd poll_data = {
+ .fd = self->fd,
+ .events = POLLIN,
+ .revents = 0
+ };
+ return poll(&poll_data, 1, 0) > 0 && (poll_data.revents & POLLIN);
+}
+
+static void gsr_capture_v4l2_clear_damage(gsr_capture *cap) {
+ gsr_capture_v4l2 *self = cap->priv;
+ (void)self;
+}
+
+static void gsr_capture_v4l2_destroy(gsr_capture *cap) {
+ gsr_capture_v4l2 *self = cap->priv;
+ if(cap->priv) {
+ gsr_capture_v4l2_stop(self);
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_v4l2_create(const gsr_capture_v4l2_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_v4l2_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_v4l2 *cap_camera = calloc(1, sizeof(gsr_capture_v4l2));
+ if(!cap_camera) {
+ free(cap);
+ return NULL;
+ }
+
+ cap_camera->params = *params;
+
+ *cap = (gsr_capture) {
+ .start = gsr_capture_v4l2_start,
+ .tick = gsr_capture_v4l2_tick,
+ .should_stop = gsr_capture_v4l2_should_stop,
+ .capture = gsr_capture_v4l2_capture,
+ .uses_external_image = gsr_capture_v4l2_uses_external_image,
+ .is_damaged = gsr_capture_v4l2_is_damaged,
+ .clear_damage = gsr_capture_v4l2_clear_damage,
+ .destroy = gsr_capture_v4l2_destroy,
+ .priv = cap_camera
+ };
+
+ return cap;
+}
+
+void gsr_capture_v4l2_list_devices(v4l2_devices_query_callback callback, void *userdata) {
+ void *libturbojpeg_lib = dlopen("libturbojpeg.so.0", RTLD_LAZY);
+ const bool has_libturbojpeg_lib = libturbojpeg_lib != NULL;
+ if(libturbojpeg_lib)
+ dlclose(libturbojpeg_lib);
+
+ char v4l2_device_path[128];
+ for(int i = 0; i < 8; ++i) {
+ snprintf(v4l2_device_path, sizeof(v4l2_device_path), "/dev/video%d", i);
+
+ const int fd = open(v4l2_device_path, O_RDWR | O_NONBLOCK);
+ if(fd < 0)
+ continue;
+
+ struct v4l2_capability cap = {0};
+ if(xioctl(fd, VIDIOC_QUERYCAP, &cap) == -1)
+ goto next;
+
+ if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
+ goto next;
+
+ if(!(cap.capabilities & V4L2_CAP_STREAMING))
+ goto next;
+
+ struct v4l2_format fmt = {
+ .type = V4L2_BUF_TYPE_VIDEO_CAPTURE
+ };
+ if(xioctl(fd, VIDIOC_G_FMT, &fmt) == -1)
+ goto next;
+
+ const gsr_capture_v4l2_supported_pixfmts supported_pixfmts = gsr_capture_v4l2_get_supported_pixfmts(fd);
+ if(supported_pixfmts.yuyv || (supported_pixfmts.mjpeg && has_libturbojpeg_lib))
+ callback(v4l2_device_path, supported_pixfmts, (vec2i){ fmt.fmt.pix.width, fmt.fmt.pix.height }, userdata);
+
+ next:
+ close(fd);
+ }
+}
diff --git a/src/capture/xcomposite.c b/src/capture/xcomposite.c
index 6814fe8..8539b77 100644
--- a/src/capture/xcomposite.c
+++ b/src/capture/xcomposite.c
@@ -23,6 +23,7 @@ typedef struct {
bool init_new_window;
Window window;
+ vec2i window_pos;
vec2i window_size;
vec2i texture_size;
double window_resize_timer;
@@ -31,14 +32,11 @@ typedef struct {
Atom net_active_window_atom;
- gsr_cursor cursor;
-
bool clear_background;
} gsr_capture_xcomposite;
static void gsr_capture_xcomposite_stop(gsr_capture_xcomposite *self) {
window_texture_deinit(&self->window_texture);
- gsr_cursor_deinit(&self->cursor);
}
static int max_int(int a, int b) {
@@ -81,6 +79,9 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata *
return -1;
}
+ self->window_pos.x = attr.x;
+ self->window_pos.y = attr.y;
+
self->window_size.x = max_int(attr.width, 0);
self->window_size.y = max_int(attr.height, 0);
@@ -95,20 +96,13 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata *
return -1;
}
- if(gsr_cursor_init(&self->cursor, self->params.egl, self->display) != 0) {
- gsr_capture_xcomposite_stop(self);
- return -1;
- }
-
self->texture_size.x = self->window_texture.window_width;
self->texture_size.y = self->window_texture.window_height;
if(self->params.output_resolution.x == 0 && self->params.output_resolution.y == 0) {
- capture_metadata->video_width = self->texture_size.x;
- capture_metadata->video_height = self->texture_size.y;
+ capture_metadata->video_size = self->texture_size;
} else {
- capture_metadata->video_width = self->params.output_resolution.x;
- capture_metadata->video_height = self->params.output_resolution.y;
+ capture_metadata->video_size = self->params.output_resolution;
}
self->window_resize_timer = clock_get_monotonic_seconds();
@@ -137,6 +131,9 @@ static void gsr_capture_xcomposite_tick(gsr_capture *cap) {
if(!XGetWindowAttributes(self->display, self->window, &attr))
fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick failed: invalid window id: %lu\n", self->window);
+ self->window_pos.x = attr.x;
+ self->window_pos.y = attr.y;
+
self->window_size.x = max_int(attr.width, 0);
self->window_size.y = max_int(attr.height, 0);
@@ -190,6 +187,9 @@ static void gsr_capture_xcomposite_on_event(gsr_capture *cap, gsr_egl *egl) {
break;
}
case ConfigureNotify: {
+ self->window_pos.x = xev->xconfigure.x;
+ self->window_pos.y = xev->xconfigure.y;
+
/* Window resized */
if(xev->xconfigure.window == self->window && (xev->xconfigure.width != self->window_size.x || xev->xconfigure.height != self->window_size.y)) {
self->window_size.x = max_int(xev->xconfigure.width, 0);
@@ -207,8 +207,6 @@ static void gsr_capture_xcomposite_on_event(gsr_capture *cap, gsr_egl *egl) {
break;
}
}
-
- gsr_cursor_on_event(&self->cursor, xev);
}
static bool gsr_capture_xcomposite_should_stop(gsr_capture *cap, bool *err) {
@@ -224,16 +222,21 @@ static bool gsr_capture_xcomposite_should_stop(gsr_capture *cap, bool *err) {
return false;
}
-static int gsr_capture_xcomposite_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
+static void gsr_capture_xcomposite_pre_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
+ (void)capture_metadata;
gsr_capture_xcomposite *self = cap->priv;
if(self->clear_background) {
self->clear_background = false;
- gsr_color_conversion_clear(color_conversion);
+ color_conversion->schedule_clear = true;
}
+}
+
+static int gsr_capture_xcomposite_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
+ gsr_capture_xcomposite *self = cap->priv;
- const vec2i output_size = scale_keep_aspect_ratio(self->texture_size, (vec2i){capture_metadata->recording_width, capture_metadata->recording_height});
- const vec2i target_pos = { max_int(0, capture_metadata->video_width / 2 - output_size.x / 2), max_int(0, capture_metadata->video_height / 2 - output_size.y / 2) };
+ const vec2i output_size = scale_keep_aspect_ratio(self->texture_size, capture_metadata->recording_size);
+ const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata);
//self->params.egl->glFlush();
//self->params.egl->glFinish();
@@ -241,28 +244,28 @@ static int gsr_capture_xcomposite_capture(gsr_capture *cap, gsr_capture_metadata
gsr_color_conversion_draw(color_conversion, window_texture_get_opengl_texture_id(&self->window_texture),
target_pos, output_size,
(vec2i){0, 0}, self->texture_size, self->texture_size,
- GSR_ROT_0, GSR_SOURCE_COLOR_RGB, false);
+ GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false);
- if(self->params.record_cursor && self->cursor.visible) {
+ if(self->params.record_cursor && self->params.cursor->visible) {
const vec2d scale = {
self->texture_size.x == 0 ? 0 : (double)output_size.x / (double)self->texture_size.x,
self->texture_size.y == 0 ? 0 : (double)output_size.y / (double)self->texture_size.y
};
- gsr_cursor_tick(&self->cursor, self->window);
-
const vec2i cursor_pos = {
- target_pos.x + (self->cursor.position.x - self->cursor.hotspot.x) * scale.x,
- target_pos.y + (self->cursor.position.y - self->cursor.hotspot.y) * scale.y
+ target_pos.x + (self->params.cursor->position.x - self->params.cursor->hotspot.x - self->window_pos.x) * scale.x,
+ target_pos.y + (self->params.cursor->position.y - self->params.cursor->hotspot.y - self->window_pos.y) * scale.y
};
- if(cursor_pos.x < target_pos.x || cursor_pos.x + self->cursor.size.x > target_pos.x + output_size.x || cursor_pos.y < target_pos.y || cursor_pos.y + self->cursor.size.y > target_pos.y + output_size.y)
- self->clear_background = true;
+ self->params.egl->glEnable(GL_SCISSOR_TEST);
+ self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y);
+
+ gsr_color_conversion_draw(color_conversion, self->params.cursor->texture_id,
+ cursor_pos, (vec2i){self->params.cursor->size.x * scale.x, self->params.cursor->size.y * scale.y},
+ (vec2i){0, 0}, self->params.cursor->size, self->params.cursor->size,
+ GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false);
- gsr_color_conversion_draw(color_conversion, self->cursor.texture_id,
- cursor_pos, (vec2i){self->cursor.size.x * scale.x, self->cursor.size.y * scale.y},
- (vec2i){0, 0}, self->cursor.size, self->cursor.size,
- GSR_ROT_0, GSR_SOURCE_COLOR_RGB, false);
+ self->params.egl->glDisable(GL_SCISSOR_TEST);
}
//self->params.egl->glFlush();
@@ -309,6 +312,7 @@ gsr_capture* gsr_capture_xcomposite_create(const gsr_capture_xcomposite_params *
.on_event = gsr_capture_xcomposite_on_event,
.tick = gsr_capture_xcomposite_tick,
.should_stop = gsr_capture_xcomposite_should_stop,
+ .pre_capture = gsr_capture_xcomposite_pre_capture,
.capture = gsr_capture_xcomposite_capture,
.uses_external_image = NULL,
.get_window_id = gsr_capture_xcomposite_get_window_id,
diff --git a/src/capture/ximage.c b/src/capture/ximage.c
index 61aed9c..b0cf364 100644
--- a/src/capture/ximage.c
+++ b/src/capture/ximage.c
@@ -16,7 +16,6 @@
typedef struct {
gsr_capture_ximage_params params;
Display *display;
- gsr_cursor cursor;
gsr_monitor monitor;
vec2i capture_pos;
vec2i capture_size;
@@ -25,7 +24,6 @@ typedef struct {
} gsr_capture_ximage;
static void gsr_capture_ximage_stop(gsr_capture_ximage *self) {
- gsr_cursor_deinit(&self->cursor);
if(self->texture_id) {
self->params.egl->glDeleteTextures(1, &self->texture_id);
self->texture_id = 0;
@@ -40,11 +38,6 @@ static int gsr_capture_ximage_start(gsr_capture *cap, gsr_capture_metadata *capt
gsr_capture_ximage *self = cap->priv;
self->root_window = DefaultRootWindow(self->display);
- if(gsr_cursor_init(&self->cursor, self->params.egl, self->display) != 0) {
- gsr_capture_ximage_stop(self);
- return -1;
- }
-
if(!get_monitor_by_name(self->params.egl, GSR_CONNECTION_X11, self->params.display_to_capture, &self->monitor)) {
fprintf(stderr, "gsr error: gsr_capture_ximage_start: failed to find monitor by name \"%s\"\n", self->params.display_to_capture);
gsr_capture_ximage_stop(self);
@@ -59,14 +52,11 @@ static int gsr_capture_ximage_start(gsr_capture *cap, gsr_capture_metadata *capt
if(self->params.output_resolution.x > 0 && self->params.output_resolution.y > 0) {
self->params.output_resolution = scale_keep_aspect_ratio(self->capture_size, self->params.output_resolution);
- capture_metadata->video_width = self->params.output_resolution.x;
- capture_metadata->video_height = self->params.output_resolution.y;
+ capture_metadata->video_size = self->params.output_resolution;
} else if(self->params.region_size.x > 0 && self->params.region_size.y > 0) {
- capture_metadata->video_width = self->params.region_size.x;
- capture_metadata->video_height = self->params.region_size.y;
+ capture_metadata->video_size = self->params.region_size;
} else {
- capture_metadata->video_width = self->capture_size.x;
- capture_metadata->video_height = self->capture_size.y;
+ capture_metadata->video_size = self->capture_size;
}
self->texture_id = gl_create_texture(self->params.egl, self->capture_size.x, self->capture_size.y, GL_RGB8, GL_RGB, GL_LINEAR);
@@ -79,12 +69,6 @@ static int gsr_capture_ximage_start(gsr_capture *cap, gsr_capture_metadata *capt
return 0;
}
-static void gsr_capture_ximage_on_event(gsr_capture *cap, gsr_egl *egl) {
- gsr_capture_ximage *self = cap->priv;
- XEvent *xev = gsr_window_get_event_data(egl->window);
- gsr_cursor_on_event(&self->cursor, xev);
-}
-
static bool gsr_capture_ximage_upload_to_texture(gsr_capture_ximage *self, int x, int y, int width, int height) {
const int max_width = XWidthOfScreen(DefaultScreenOfDisplay(self->display));
const int max_height = XHeightOfScreen(DefaultScreenOfDisplay(self->display));
@@ -137,6 +121,7 @@ static bool gsr_capture_ximage_upload_to_texture(gsr_capture_ximage *self, int x
}
self->params.egl->glBindTexture(GL_TEXTURE_2D, self->texture_id);
+ // TODO: Change to GL_RGBA for better performance? image_data needs alpha then as well
self->params.egl->glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image->width, image->height, GL_RGB, GL_UNSIGNED_BYTE, image_data);
self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
success = true;
@@ -150,35 +135,33 @@ static bool gsr_capture_ximage_upload_to_texture(gsr_capture_ximage *self, int x
static int gsr_capture_ximage_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
gsr_capture_ximage *self = cap->priv;
- const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, (vec2i){capture_metadata->recording_width, capture_metadata->recording_height});
- const vec2i target_pos = { max_int(0, capture_metadata->video_width / 2 - output_size.x / 2), max_int(0, capture_metadata->video_height / 2 - output_size.y / 2) };
+ const vec2i output_size = scale_keep_aspect_ratio(self->capture_size, capture_metadata->recording_size);
+ const vec2i target_pos = gsr_capture_get_target_position(output_size, capture_metadata);
gsr_capture_ximage_upload_to_texture(self, self->capture_pos.x + self->params.region_position.x, self->capture_pos.y + self->params.region_position.y, self->capture_size.x, self->capture_size.y);
gsr_color_conversion_draw(color_conversion, self->texture_id,
target_pos, output_size,
(vec2i){0, 0}, self->capture_size, self->capture_size,
- GSR_ROT_0, GSR_SOURCE_COLOR_RGB, false);
+ GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false);
- if(self->params.record_cursor && self->cursor.visible) {
+ if(self->params.record_cursor && self->params.cursor->visible) {
const vec2d scale = {
self->capture_size.x == 0 ? 0 : (double)output_size.x / (double)self->capture_size.x,
self->capture_size.y == 0 ? 0 : (double)output_size.y / (double)self->capture_size.y
};
- gsr_cursor_tick(&self->cursor, self->root_window);
-
const vec2i cursor_pos = {
- target_pos.x + (self->cursor.position.x - self->cursor.hotspot.x) * scale.x - self->capture_pos.x - self->params.region_position.x,
- target_pos.y + (self->cursor.position.y - self->cursor.hotspot.y) * scale.y - self->capture_pos.y - self->params.region_position.y
+ target_pos.x + (self->params.cursor->position.x - self->params.cursor->hotspot.x) * scale.x - self->capture_pos.x - self->params.region_position.x,
+ target_pos.y + (self->params.cursor->position.y - self->params.cursor->hotspot.y) * scale.y - self->capture_pos.y - self->params.region_position.y
};
self->params.egl->glEnable(GL_SCISSOR_TEST);
self->params.egl->glScissor(target_pos.x, target_pos.y, output_size.x, output_size.y);
- gsr_color_conversion_draw(color_conversion, self->cursor.texture_id,
- cursor_pos, (vec2i){self->cursor.size.x * scale.x, self->cursor.size.y * scale.y},
- (vec2i){0, 0}, self->cursor.size, self->cursor.size,
- GSR_ROT_0, GSR_SOURCE_COLOR_RGB, false);
+ gsr_color_conversion_draw(color_conversion, self->params.cursor->texture_id,
+ cursor_pos, (vec2i){self->params.cursor->size.x * scale.x, self->params.cursor->size.y * scale.y},
+ (vec2i){0, 0}, self->params.cursor->size, self->params.cursor->size,
+ GSR_ROT_0, capture_metadata->flip, GSR_SOURCE_COLOR_RGB, false);
self->params.egl->glDisable(GL_SCISSOR_TEST);
}
@@ -230,7 +213,6 @@ gsr_capture* gsr_capture_ximage_create(const gsr_capture_ximage_params *params)
*cap = (gsr_capture) {
.start = gsr_capture_ximage_start,
- .on_event = gsr_capture_ximage_on_event,
.tick = NULL,
.should_stop = NULL,
.capture = gsr_capture_ximage_capture,
diff --git a/src/color_conversion.c b/src/color_conversion.c
index 4c327df..2ff4a59 100644
--- a/src/color_conversion.c
+++ b/src/color_conversion.c
@@ -472,7 +472,7 @@ static void gsr_color_conversion_swizzle_reset(gsr_color_conversion *self, gsr_s
}
}
-static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsigned int texture_id, bool external_texture, gsr_rotation rotation, float rotation_matrix[2][2], vec2i source_position, vec2i source_size, vec2i destination_pos, vec2i texture_size, vec2f scale, gsr_source_color source_color) {
+static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsigned int texture_id, bool external_texture, gsr_rotation rotation, gsr_flip flip, float rotation_matrix[2][2], vec2i source_position, vec2i source_size, vec2i destination_pos, vec2i texture_size, vec2f scale, gsr_source_color source_color) {
if(source_size.x == 0 || source_size.y == 0)
return;
@@ -508,7 +508,7 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
(float)source_size.y / (texture_size.y == 0 ? 1.0f : (float)texture_size.y),
};
- const float vertices[] = {
+ float vertices[] = {
-1.0f + 0.0f, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x, texture_pos_norm.y + texture_size_norm.y,
-1.0f + 0.0f, -1.0f + 0.0f, texture_pos_norm.x, texture_pos_norm.y,
-1.0f + 0.0f + size_norm.x, -1.0f + 0.0f, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y,
@@ -518,6 +518,20 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
-1.0f + 0.0f + size_norm.x, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y + texture_size_norm.y
};
+ if(flip & GSR_FLIP_HORIZONTAL) {
+ for(int i = 0; i < 6; ++i) {
+ const float prev_x = vertices[i*4 + 2];
+ vertices[i*4 + 2] = texture_pos_norm.x + texture_size_norm.x - prev_x;
+ }
+ }
+
+ if(flip & GSR_FLIP_VERTICAL) {
+ for(int i = 0; i < 6; ++i) {
+ const float prev_y = vertices[i*4 + 3];
+ vertices[i*4 + 3] = texture_pos_norm.y + texture_size_norm.y - prev_y;
+ }
+ }
+
self->params.egl->glBindVertexArray(self->vertex_array_object_id);
self->params.egl->glViewport(0, 0, dest_texture_size.x, dest_texture_size.y);
@@ -569,7 +583,7 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig
self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
-void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i destination_pos, vec2i destination_size, vec2i source_pos, vec2i source_size, vec2i texture_size, gsr_rotation rotation, gsr_source_color source_color, bool external_texture) {
+void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i destination_pos, vec2i destination_size, vec2i source_pos, vec2i source_size, vec2i texture_size, gsr_rotation rotation, gsr_flip flip, gsr_source_color source_color, bool external_texture) {
assert(!external_texture || self->params.load_external_image_shader);
if(external_texture && !self->params.load_external_image_shader) {
fprintf(stderr, "gsr error: gsr_color_conversion_draw: external texture not loaded\n");
@@ -590,7 +604,7 @@ void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_
source_position.x += source_pos.x;
source_position.y += source_pos.y;
- gsr_color_conversion_draw_graphics(self, texture_id, external_texture, rotation, rotation_matrix, source_position, source_size, destination_pos, texture_size, scale, source_color);
+ gsr_color_conversion_draw_graphics(self, texture_id, external_texture, rotation, flip, rotation_matrix, source_position, source_size, destination_pos, texture_size, scale, source_color);
self->params.egl->glFlush();
// TODO: Use the minimal barrier required
diff --git a/src/cursor.c b/src/cursor.c
index e818d72..19047ba 100644
--- a/src/cursor.c
+++ b/src/cursor.c
@@ -96,7 +96,7 @@ int gsr_cursor_init(gsr_cursor *self, gsr_egl *egl, Display *display) {
}
void gsr_cursor_deinit(gsr_cursor *self) {
- if(!self->egl)
+ if(!self->display)
return;
if(self->texture_id) {
@@ -112,6 +112,9 @@ void gsr_cursor_deinit(gsr_cursor *self) {
}
bool gsr_cursor_on_event(gsr_cursor *self, XEvent *xev) {
+ if(!self->display)
+ return false;
+
bool updated = false;
if(xev->type == self->x_fixes_event_base + XFixesCursorNotify) {
@@ -131,6 +134,9 @@ bool gsr_cursor_on_event(gsr_cursor *self, XEvent *xev) {
}
void gsr_cursor_tick(gsr_cursor *self, Window relative_to) {
+ if(!self->display)
+ return;
+
Window dummy_window;
int dummy_i;
unsigned int dummy_u;
diff --git a/src/damage.c b/src/damage.c
index 25a2225..caae80a 100644
--- a/src/damage.c
+++ b/src/damage.c
@@ -4,6 +4,7 @@
#include <stdio.h>
#include <string.h>
+#include <stdlib.h>
#include <X11/extensions/Xdamage.h>
#include <X11/extensions/Xrandr.h>
@@ -26,51 +27,115 @@ static bool xrandr_is_supported(Display *display) {
return major_version > 1 || (major_version == 1 && minor_version >= 2);
}
-bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, bool track_cursor) {
+static int gsr_damage_get_tracked_monitor_index(const gsr_damage *self, const char *monitor_name) {
+ for(int i = 0; i < self->num_monitors_tracked; ++i) {
+ if(strcmp(self->monitors_tracked[i].monitor_name, monitor_name) == 0)
+ return i;
+ }
+ return -1;
+}
+
+static void add_monitor_callback(const gsr_monitor *monitor, void *userdata) {
+ gsr_damage *self = userdata;
+
+ const int damage_monitor_index = gsr_damage_get_tracked_monitor_index(self, monitor->name);
+ gsr_damage_monitor *damage_monitor = NULL;
+ if(damage_monitor_index != -1) {
+ damage_monitor = &self->monitors_tracked[damage_monitor_index];
+ damage_monitor->monitor = NULL;
+ }
+
+ if(self->num_monitors + 1 > GSR_DAMAGE_MAX_MONITORS) {
+ fprintf(stderr, "gsr error: gsr_damage_on_output_change: max monitors reached\n");
+ return;
+ }
+
+ char *monitor_name_copy = strdup(monitor->name);
+ if(!monitor_name_copy) {
+ fprintf(stderr, "gsr error: gsr_damage_on_output_change: strdup failed for monitor: %s\n", monitor->name);
+ return;
+ }
+
+ self->monitors[self->num_monitors] = *monitor;
+ self->monitors[self->num_monitors].name = monitor_name_copy;
+ ++self->num_monitors;
+
+ if(damage_monitor)
+ damage_monitor->monitor = &self->monitors[self->num_monitors - 1];
+}
+
+bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, gsr_cursor *cursor, bool track_cursor) {
memset(self, 0, sizeof(*self));
self->egl = egl;
self->track_cursor = track_cursor;
+ self->cursor = cursor;
if(gsr_window_get_display_server(egl->window) != GSR_DISPLAY_SERVER_X11) {
- fprintf(stderr, "gsr warning: gsr_damage_init: damage tracking is not supported on wayland\n");
+ fprintf(stderr, "gsr error: gsr_damage_init: damage tracking is not supported on wayland\n");
return false;
}
self->display = gsr_window_get_display(egl->window);
if(!XDamageQueryExtension(self->display, &self->damage_event, &self->damage_error)) {
- fprintf(stderr, "gsr warning: gsr_damage_init: XDamage is not supported by your X11 server\n");
+ fprintf(stderr, "gsr error: gsr_damage_init: XDamage is not supported by your X11 server\n");
gsr_damage_deinit(self);
return false;
}
if(!XRRQueryExtension(self->display, &self->randr_event, &self->randr_error)) {
- fprintf(stderr, "gsr warning: gsr_damage_init: XRandr is not supported by your X11 server\n");
+ fprintf(stderr, "gsr error: gsr_damage_init: XRandr is not supported by your X11 server\n");
gsr_damage_deinit(self);
return false;
}
if(!xrandr_is_supported(self->display)) {
- fprintf(stderr, "gsr warning: gsr_damage_init: your X11 randr version is too old\n");
+ fprintf(stderr, "gsr error: gsr_damage_init: your X11 randr version is too old\n");
gsr_damage_deinit(self);
return false;
}
- if(self->track_cursor)
- self->track_cursor = gsr_cursor_init(&self->cursor, self->egl, self->display) == 0;
-
XRRSelectInput(self->display, DefaultRootWindow(self->display), RRScreenChangeNotifyMask | RRCrtcChangeNotifyMask | RROutputChangeNotifyMask);
+ self->monitor_damage = XDamageCreate(self->display, DefaultRootWindow(self->display), XDamageReportNonEmpty);
+ if(!self->monitor_damage) {
+ fprintf(stderr, "gsr error: gsr_damage_init: XDamageCreate failed\n");
+ gsr_damage_deinit(self);
+ return false;
+ }
+ XDamageSubtract(self->display, self->monitor_damage, None, None);
+
+ for_each_active_monitor_output_x11_not_cached(self->display, add_monitor_callback, self);
+
self->damaged = true;
return true;
}
+static void gsr_damage_deinit_monitors(gsr_damage *self) {
+ for(int i = 0; i < self->num_monitors; ++i) {
+ free((char*)self->monitors[i].name);
+ }
+ self->num_monitors = 0;
+}
+
void gsr_damage_deinit(gsr_damage *self) {
- if(self->damage) {
- XDamageDestroy(self->display, self->damage);
- self->damage = None;
+ if(self->monitor_damage) {
+ XDamageDestroy(self->display, self->monitor_damage);
+ self->monitor_damage = None;
+ }
+
+ for(int i = 0; i < self->num_monitors_tracked; ++i) {
+ free(self->monitors_tracked[i].monitor_name);
+ }
+ self->num_monitors_tracked = 0;
+
+ for(int i = 0; i < self->num_windows_tracked; ++i) {
+ XSelectInput(self->display, self->windows_tracked[i].window_id, 0);
+ XDamageDestroy(self->display, self->windows_tracked[i].damage);
}
+ self->num_windows_tracked = 0;
- gsr_cursor_deinit(&self->cursor);
+ self->all_monitors_tracked_refcount = 0;
+ gsr_damage_deinit_monitors(self);
self->damage_event = 0;
self->damage_error = 0;
@@ -79,139 +144,191 @@ void gsr_damage_deinit(gsr_damage *self) {
self->randr_error = 0;
}
-bool gsr_damage_set_target_window(gsr_damage *self, uint64_t window) {
- if(self->damage_event == 0)
+static int gsr_damage_get_tracked_window_index(const gsr_damage *self, int64_t window) {
+ for(int i = 0; i < self->num_windows_tracked; ++i) {
+ if(self->windows_tracked[i].window_id == window)
+ return i;
+ }
+ return -1;
+}
+
+bool gsr_damage_start_tracking_window(gsr_damage *self, int64_t window) {
+ if(self->damage_event == 0 || window == None)
return false;
- if(window == self->window)
+ const int damage_window_index = gsr_damage_get_tracked_window_index(self, window);
+ if(damage_window_index != -1) {
+ ++self->windows_tracked[damage_window_index].refcount;
return true;
-
- if(self->damage) {
- XDamageDestroy(self->display, self->damage);
- self->damage = None;
}
- if(self->window)
- XSelectInput(self->display, self->window, 0);
-
- self->window = window;
- XSelectInput(self->display, self->window, StructureNotifyMask | ExposureMask);
+ if(self->num_windows_tracked + 1 > GSR_DAMAGE_MAX_TRACKED_TARGETS) {
+ fprintf(stderr, "gsr error: gsr_damage_start_tracking_window: max window targets reached\n");
+ return false;
+ }
XWindowAttributes win_attr;
win_attr.x = 0;
win_attr.y = 0;
win_attr.width = 0;
win_attr.height = 0;
- if(!XGetWindowAttributes(self->display, self->window, &win_attr))
- fprintf(stderr, "gsr warning: gsr_damage_set_target_window failed: failed to get window attributes: %ld\n", (long)self->window);
+ if(!XGetWindowAttributes(self->display, window, &win_attr))
+ fprintf(stderr, "gsr warning: gsr_damage_start_tracking_window failed: failed to get window attributes: %ld\n", (long)window);
- //self->window_pos.x = win_attr.x;
- //self->window_pos.y = win_attr.y;
+ const Damage damage = XDamageCreate(self->display, window, XDamageReportNonEmpty);
+ if(!damage) {
+ fprintf(stderr, "gsr error: gsr_damage_start_tracking_window: XDamageCreate failed\n");
+ return false;
+ }
+ XDamageSubtract(self->display, damage, None, None);
- self->window_size.x = win_attr.width;
- self->window_size.y = win_attr.height;
+ XSelectInput(self->display, window, StructureNotifyMask | ExposureMask);
- self->damage = XDamageCreate(self->display, window, XDamageReportNonEmpty);
- if(self->damage) {
- XDamageSubtract(self->display, self->damage, None, None);
- self->damaged = true;
- self->track_type = GSR_DAMAGE_TRACK_WINDOW;
- return true;
- } else {
- fprintf(stderr, "gsr warning: gsr_damage_set_target_window: XDamageCreate failed\n");
- self->track_type = GSR_DAMAGE_TRACK_NONE;
- return false;
+ gsr_damage_window *damage_window = &self->windows_tracked[self->num_windows_tracked];
+ ++self->num_windows_tracked;
+
+ damage_window->window_id = window;
+ damage_window->window_pos.x = win_attr.x;
+ damage_window->window_pos.y = win_attr.y;
+ damage_window->window_size.x = win_attr.width;
+ damage_window->window_size.y = win_attr.height;
+ damage_window->damage = damage;
+ damage_window->refcount = 1;
+ return true;
+}
+
+void gsr_damage_stop_tracking_window(gsr_damage *self, int64_t window) {
+ if(window == None)
+ return;
+
+ const int damage_window_index = gsr_damage_get_tracked_window_index(self, window);
+ if(damage_window_index == -1)
+ return;
+
+ gsr_damage_window *damage_window = &self->windows_tracked[damage_window_index];
+ --damage_window->refcount;
+ if(damage_window->refcount <= 0) {
+ XSelectInput(self->display, damage_window->window_id, 0);
+ XDamageDestroy(self->display, damage_window->damage);
+ self->windows_tracked[damage_window_index] = self->windows_tracked[self->num_windows_tracked - 1];
+ --self->num_windows_tracked;
}
}
-bool gsr_damage_set_target_monitor(gsr_damage *self, const char *monitor_name) {
+static gsr_monitor* gsr_damage_get_monitor_by_id(gsr_damage *self, RRCrtc id) {
+ for(int i = 0; i < self->num_monitors; ++i) {
+ if(self->monitors[i].monitor_identifier == id)
+ return &self->monitors[i];
+ }
+ return NULL;
+}
+
+static gsr_monitor* gsr_damage_get_monitor_by_name(gsr_damage *self, const char *name) {
+ for(int i = 0; i < self->num_monitors; ++i) {
+ if(strcmp(self->monitors[i].name, name) == 0)
+ return &self->monitors[i];
+ }
+ return NULL;
+}
+
+bool gsr_damage_start_tracking_monitor(gsr_damage *self, const char *monitor_name) {
if(self->damage_event == 0)
return false;
- if(strcmp(self->monitor_name, monitor_name) == 0)
+ if(strcmp(monitor_name, "screen-direct") == 0 || strcmp(monitor_name, "screen-direct-force") == 0)
+ monitor_name = NULL;
+
+ if(!monitor_name) {
+ ++self->all_monitors_tracked_refcount;
return true;
+ }
- if(self->damage) {
- XDamageDestroy(self->display, self->damage);
- self->damage = None;
+ const int damage_monitor_index = gsr_damage_get_tracked_monitor_index(self, monitor_name);
+ if(damage_monitor_index != -1) {
+ ++self->monitors_tracked[damage_monitor_index].refcount;
+ return true;
}
- memset(&self->monitor, 0, sizeof(self->monitor));
- if(strcmp(monitor_name, "screen-direct") != 0 && strcmp(monitor_name, "screen-direct-force") != 0) {
- if(!get_monitor_by_name(self->egl, GSR_CONNECTION_X11, monitor_name, &self->monitor))
- fprintf(stderr, "gsr warning: gsr_damage_set_target_monitor: failed to find monitor: %s\n", monitor_name);
+ if(self->num_monitors_tracked + 1 > GSR_DAMAGE_MAX_TRACKED_TARGETS) {
+ fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: max monitor targets reached\n");
+ return false;
}
- if(self->window)
- XSelectInput(self->display, self->window, 0);
+ char *monitor_name_copy = strdup(monitor_name);
+ if(!monitor_name_copy) {
+ fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: strdup failed for monitor: %s\n", monitor_name);
+ return false;
+ }
- self->window = DefaultRootWindow(self->display);
- self->damage = XDamageCreate(self->display, self->window, XDamageReportNonEmpty);
- if(self->damage) {
- XDamageSubtract(self->display, self->damage, None, None);
- self->damaged = true;
- snprintf(self->monitor_name, sizeof(self->monitor_name), "%s", monitor_name);
- self->track_type = GSR_DAMAGE_TRACK_MONITOR;
- return true;
- } else {
- fprintf(stderr, "gsr warning: gsr_damage_set_target_monitor: XDamageCreate failed\n");
- self->track_type = GSR_DAMAGE_TRACK_NONE;
+ gsr_monitor *monitor = gsr_damage_get_monitor_by_name(self, monitor_name);
+ if(!monitor) {
+ fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: failed to find monitor: %s\n", monitor_name);
+ free(monitor_name_copy);
return false;
}
+
+ gsr_damage_monitor *damage_monitor = &self->monitors_tracked[self->num_monitors_tracked];
+ ++self->num_monitors_tracked;
+
+ damage_monitor->monitor_name = monitor_name_copy;
+ damage_monitor->monitor = monitor;
+ damage_monitor->refcount = 1;
+ return true;
+}
+
+void gsr_damage_stop_tracking_monitor(gsr_damage *self, const char *monitor_name) {
+ if(strcmp(monitor_name, "screen-direct") == 0 || strcmp(monitor_name, "screen-direct-force") == 0)
+ monitor_name = NULL;
+
+ if(!monitor_name) {
+ --self->all_monitors_tracked_refcount;
+ if(self->all_monitors_tracked_refcount < 0)
+ self->all_monitors_tracked_refcount = 0;
+ return;
+ }
+
+ const int damage_monitor_index = gsr_damage_get_tracked_monitor_index(self, monitor_name);
+ if(damage_monitor_index == -1)
+ return;
+
+ gsr_damage_monitor *damage_monitor = &self->monitors_tracked[damage_monitor_index];
+ --damage_monitor->refcount;
+ if(damage_monitor->refcount <= 0) {
+ free(damage_monitor->monitor_name);
+ self->monitors_tracked[damage_monitor_index] = self->monitors_tracked[self->num_monitors_tracked - 1];
+ --self->num_monitors_tracked;
+ }
}
static void gsr_damage_on_crtc_change(gsr_damage *self, XEvent *xev) {
const XRRCrtcChangeNotifyEvent *rr_crtc_change_event = (XRRCrtcChangeNotifyEvent*)xev;
- if(rr_crtc_change_event->crtc == 0 || self->monitor.monitor_identifier == 0)
+ if(rr_crtc_change_event->crtc == 0)
return;
- if(rr_crtc_change_event->crtc != self->monitor.monitor_identifier)
+ if(rr_crtc_change_event->width == 0 || rr_crtc_change_event->height == 0)
return;
- if(rr_crtc_change_event->width == 0 || rr_crtc_change_event->height == 0)
+ gsr_monitor *monitor = gsr_damage_get_monitor_by_id(self, rr_crtc_change_event->crtc);
+ if(!monitor)
return;
- if(rr_crtc_change_event->x != self->monitor.pos.x || rr_crtc_change_event->y != self->monitor.pos.y ||
- (int)rr_crtc_change_event->width != self->monitor.size.x || (int)rr_crtc_change_event->height != self->monitor.size.y) {
- self->monitor.pos.x = rr_crtc_change_event->x;
- self->monitor.pos.y = rr_crtc_change_event->y;
+ if(rr_crtc_change_event->x != monitor->pos.x || rr_crtc_change_event->y != monitor->pos.y ||
+ (int)rr_crtc_change_event->width != monitor->size.x || (int)rr_crtc_change_event->height != monitor->size.y) {
+ monitor->pos.x = rr_crtc_change_event->x;
+ monitor->pos.y = rr_crtc_change_event->y;
- self->monitor.size.x = rr_crtc_change_event->width;
- self->monitor.size.y = rr_crtc_change_event->height;
+ monitor->size.x = rr_crtc_change_event->width;
+ monitor->size.y = rr_crtc_change_event->height;
}
}
static void gsr_damage_on_output_change(gsr_damage *self, XEvent *xev) {
const XRROutputChangeNotifyEvent *rr_output_change_event = (XRROutputChangeNotifyEvent*)xev;
- if(!rr_output_change_event->output || self->monitor.monitor_identifier == 0)
+ if(!rr_output_change_event->output)
return;
- XRRScreenResources *screen_res = XRRGetScreenResources(self->display, DefaultRootWindow(self->display));
- if(!screen_res)
- return;
-
- // TODO: What about scaled output? look at for_each_active_monitor_output_x11_not_cached
- XRROutputInfo *out_info = XRRGetOutputInfo(self->display, screen_res, rr_output_change_event->output);
- if(out_info && out_info->crtc && out_info->crtc == self->monitor.monitor_identifier) {
- XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(self->display, screen_res, out_info->crtc);
- if(crtc_info && (crtc_info->x != self->monitor.pos.x || crtc_info->y != self->monitor.pos.y ||
- (int)crtc_info->width != self->monitor.size.x || (int)crtc_info->height != self->monitor.size.y))
- {
- self->monitor.pos.x = crtc_info->x;
- self->monitor.pos.y = crtc_info->y;
-
- self->monitor.size.x = crtc_info->width;
- self->monitor.size.y = crtc_info->height;
- }
-
- if(crtc_info)
- XRRFreeCrtcInfo(crtc_info);
- }
-
- if(out_info)
- XRRFreeOutputInfo(out_info);
-
- XRRFreeScreenResources(screen_res);
+ gsr_damage_deinit_monitors(self);
+ for_each_active_monitor_output_x11_not_cached(self->display, add_monitor_callback, self);
}
static void gsr_damage_on_randr_event(gsr_damage *self, XEvent *xev) {
@@ -232,19 +349,38 @@ static void gsr_damage_on_damage_event(gsr_damage *self, XEvent *xev) {
/* Subtract all the damage, repairing the window */
XDamageSubtract(self->display, de->damage, None, region);
- if(self->track_type == GSR_DAMAGE_TRACK_WINDOW || (self->track_type == GSR_DAMAGE_TRACK_MONITOR && self->monitor.connector_id == 0)) {
+ if(self->all_monitors_tracked_refcount > 0)
self->damaged = true;
- } else {
+
+ if(!self->damaged) {
+ for(int i = 0; i < self->num_windows_tracked; ++i) {
+ const gsr_damage_window *damage_window = &self->windows_tracked[i];
+ if(damage_window->window_id == (int64_t)de->drawable) {
+ self->damaged = true;
+ break;
+ }
+ }
+ }
+
+ if(!self->damaged) {
int num_rectangles = 0;
XRectangle *rectangles = XFixesFetchRegion(self->display, region, &num_rectangles);
if(rectangles) {
- const gsr_rectangle monitor_region = { self->monitor.pos, self->monitor.size };
for(int i = 0; i < num_rectangles; ++i) {
const gsr_rectangle damage_region = { (vec2i){rectangles[i].x, rectangles[i].y}, (vec2i){rectangles[i].width, rectangles[i].height} };
- self->damaged = rectangles_intersect(monitor_region, damage_region);
- if(self->damaged)
- break;
+ for(int j = 0; j < self->num_monitors_tracked; ++j) {
+ const gsr_monitor *monitor = self->monitors_tracked[j].monitor;
+ if(!monitor)
+ continue;
+
+ const gsr_rectangle monitor_region = { monitor->pos, monitor->size };
+ self->damaged = rectangles_intersect(monitor_region, damage_region);
+ if(self->damaged)
+ goto intersection_found;
+ }
}
+
+ intersection_found:
XFree(rectangles);
}
}
@@ -254,23 +390,35 @@ static void gsr_damage_on_damage_event(gsr_damage *self, XEvent *xev) {
}
static void gsr_damage_on_tick_cursor(gsr_damage *self) {
- vec2i prev_cursor_pos = self->cursor.position;
- gsr_cursor_tick(&self->cursor, self->window);
- if(self->cursor.position.x != prev_cursor_pos.x || self->cursor.position.y != prev_cursor_pos.y) {
- const gsr_rectangle cursor_region = { self->cursor.position, self->cursor.size };
- switch(self->track_type) {
- case GSR_DAMAGE_TRACK_NONE: {
+ if(self->cursor->position.x == self->cursor_pos.x && self->cursor->position.y == self->cursor_pos.y)
+ return;
+
+ self->cursor_pos = self->cursor->position;
+ const gsr_rectangle cursor_region = { self->cursor->position, self->cursor->size };
+
+ if(self->all_monitors_tracked_refcount > 0)
+ self->damaged = true;
+
+ if(!self->damaged) {
+ for(int i = 0; i < self->num_windows_tracked; ++i) {
+ const gsr_damage_window *damage_window = &self->windows_tracked[i];
+ const gsr_rectangle window_region = { damage_window->window_pos, damage_window->window_size };
+ if(rectangles_intersect(window_region, cursor_region)) {
self->damaged = true;
break;
}
- case GSR_DAMAGE_TRACK_WINDOW: {
- const gsr_rectangle window_region = { (vec2i){0, 0}, self->window_size };
- self->damaged = self->window_size.x == 0 || rectangles_intersect(window_region, cursor_region);
- break;
- }
- case GSR_DAMAGE_TRACK_MONITOR: {
- const gsr_rectangle monitor_region = { self->monitor.pos, self->monitor.size };
- self->damaged = self->monitor.monitor_identifier == 0 || rectangles_intersect(monitor_region, cursor_region);
+ }
+ }
+
+ if(!self->damaged) {
+ for(int i = 0; i < self->num_monitors_tracked; ++i) {
+ const gsr_monitor *monitor = self->monitors_tracked[i].monitor;
+ if(!monitor)
+ continue;
+
+ const gsr_rectangle monitor_region = { monitor->pos, monitor->size };
+ if(rectangles_intersect(monitor_region, cursor_region)) {
+ self->damaged = true;
break;
}
}
@@ -278,21 +426,24 @@ static void gsr_damage_on_tick_cursor(gsr_damage *self) {
}
static void gsr_damage_on_window_configure_notify(gsr_damage *self, XEvent *xev) {
- if(xev->xconfigure.window != self->window)
- return;
-
- //self->window_pos.x = xev->xconfigure.x;
- //self->window_pos.y = xev->xconfigure.y;
-
- self->window_size.x = xev->xconfigure.width;
- self->window_size.y = xev->xconfigure.height;
+ for(int i = 0; i < self->num_windows_tracked; ++i) {
+ gsr_damage_window *damage_window = &self->windows_tracked[i];
+ if(damage_window->window_id == (int64_t)xev->xconfigure.window) {
+ damage_window->window_pos.x = xev->xconfigure.x;
+ damage_window->window_pos.y = xev->xconfigure.y;
+
+ damage_window->window_size.x = xev->xconfigure.width;
+ damage_window->window_size.y = xev->xconfigure.height;
+ break;
+ }
+ }
}
void gsr_damage_on_event(gsr_damage *self, XEvent *xev) {
- if(self->damage_event == 0 || self->track_type == GSR_DAMAGE_TRACK_NONE)
+ if(self->damage_event == 0)
return;
- if(self->track_type == GSR_DAMAGE_TRACK_WINDOW && xev->type == ConfigureNotify)
+ if(xev->type == ConfigureNotify)
gsr_damage_on_window_configure_notify(self, xev);
if(self->randr_event) {
@@ -305,21 +456,18 @@ void gsr_damage_on_event(gsr_damage *self, XEvent *xev) {
if(self->damage_event && xev->type == self->damage_event + XDamageNotify)
gsr_damage_on_damage_event(self, xev);
-
- if(self->track_cursor)
- gsr_cursor_on_event(&self->cursor, xev);
}
void gsr_damage_tick(gsr_damage *self) {
- if(self->damage_event == 0 || self->track_type == GSR_DAMAGE_TRACK_NONE)
+ if(self->damage_event == 0)
return;
- if(self->track_cursor && self->cursor.visible && !self->damaged)
+ if(self->track_cursor && self->cursor->visible && !self->damaged)
gsr_damage_on_tick_cursor(self);
}
bool gsr_damage_is_damaged(gsr_damage *self) {
- return self->damage_event == 0 || !self->damage || self->damaged || self->track_type == GSR_DAMAGE_TRACK_NONE;
+ return self->damage_event == 0 || self->damaged;
}
void gsr_damage_clear(gsr_damage *self) {
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");
diff --git a/src/sound.cpp b/src/sound.cpp
index 1be3aed..af83b15 100644
--- a/src/sound.cpp
+++ b/src/sound.cpp
@@ -259,12 +259,19 @@ fail:
static bool pa_sound_device_should_reconnect(pa_handle *p, double now, char *device_name, size_t device_name_size) {
std::lock_guard<std::mutex> lock(p->reconnect_mutex);
+
+ if(!p->reconnect && (!p->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state(p->stream)))) {
+ p->reconnect = true;
+ p->reconnect_last_tried_seconds = now;
+ }
+
if(p->reconnect && now - p->reconnect_last_tried_seconds >= RECONNECT_TRY_TIMEOUT_SECONDS) {
p->reconnect_last_tried_seconds = now;
// TODO: Size check
snprintf(device_name, device_name_size, "%s", p->device_name);
return true;
}
+
return false;
}
@@ -292,6 +299,8 @@ static bool pa_sound_device_handle_reconnect(pa_handle *p, char *device_name, si
return false;
}
+ pa_mainloop_iterate(p->mainloop, 0, NULL);
+
std::lock_guard<std::mutex> lock(p->reconnect_mutex);
p->reconnect = false;
return true;
@@ -309,10 +318,11 @@ static int pa_sound_device_read(pa_handle *p, double timeout_seconds) {
pa_usec_t latency = 0;
int negative = 0;
- if(!pa_sound_device_handle_reconnect(p, device_name, sizeof(device_name), start_time))
+ pa_mainloop_iterate(p->mainloop, 0, NULL);
+
+ if(!pa_sound_device_handle_reconnect(p, device_name, sizeof(device_name), start_time) || !p->stream)
goto fail;
- pa_mainloop_iterate(p->mainloop, 0, NULL);
if(pa_stream_get_state(p->stream) != PA_STREAM_READY)
goto fail;
diff --git a/src/utils.c b/src/utils.c
index 3814894..708643d 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -1,5 +1,6 @@
#include "../include/utils.h"
#include "../include/window/window.h"
+#include "../include/capture/capture.h"
#include <time.h>
#include <string.h>
@@ -595,12 +596,44 @@ vec2i scale_keep_aspect_ratio(vec2i from, vec2i to) {
return from;
}
+vec2i gsr_capture_get_target_position(vec2i output_size, gsr_capture_metadata *capture_metadata) {
+ vec2i target_pos = {0, 0};
+
+ switch(capture_metadata->halign) {
+ case GSR_CAPTURE_ALIGN_START:
+ break;
+ case GSR_CAPTURE_ALIGN_CENTER:
+ target_pos.x = capture_metadata->video_size.x/2 - output_size.x/2;
+ break;
+ case GSR_CAPTURE_ALIGN_END:
+ target_pos.x = capture_metadata->video_size.x - output_size.x;
+ break;
+ }
+
+ switch(capture_metadata->valign) {
+ case GSR_CAPTURE_ALIGN_START:
+ break;
+ case GSR_CAPTURE_ALIGN_CENTER:
+ target_pos.y = capture_metadata->video_size.y/2 - output_size.y/2;
+ break;
+ case GSR_CAPTURE_ALIGN_END:
+ target_pos.y = capture_metadata->video_size.y - output_size.y;
+ break;
+ }
+
+ target_pos.x += capture_metadata->position.x;
+ target_pos.y += capture_metadata->position.y;
+ return target_pos;
+}
+
+
unsigned int gl_create_texture(gsr_egl *egl, int width, int height, int internal_format, unsigned int format, int filter) {
unsigned int texture_id = 0;
egl->glGenTextures(1, &texture_id);
egl->glBindTexture(GL_TEXTURE_2D, texture_id);
- //egl->glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, GL_UNSIGNED_BYTE, NULL);
- egl->glTexStorage2D(GL_TEXTURE_2D, 1, internal_format, width, height);
+ // TODO:
+ egl->glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, GL_UNSIGNED_BYTE, NULL);
+ //egl->glTexStorage2D(GL_TEXTURE_2D, 1, internal_format, width, height);
egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);