aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMaria Lisina <sekoohaka.sarisan@gmail.com>2026-07-04 22:13:32 +0300
committerMaria Lisina <sekoohaka.sarisan@gmail.com>2026-07-04 22:13:32 +0300
commit25f932f7ae30614a2612098655b2371bb11a7218 (patch)
tree8cf6f8597a5f96906407f2638fa37c3cec4c55c0
parent16dcac4a9d61f87c7185f2ed1ea08235275358e0 (diff)
parent9706252fcc46fef7c15522f07e4e61ee58ca6d6d (diff)
Merge tag '5.14.1' of https://repo.dec05eba.com/gpu-screen-recorder
-rw-r--r--README.md23
-rw-r--r--TODO5
-rw-r--r--gpu-screen-recorder.114
-rw-r--r--gsr-kms-server.14
-rw-r--r--include/pipewire_video.h7
-rw-r--r--include/sound.hpp7
-rw-r--r--kms/kms_shared.h21
-rw-r--r--kms/server/kms_server.c121
-rw-r--r--meson.build2
-rw-r--r--project.conf2
-rw-r--r--src/capture/kms.c126
-rw-r--r--src/capture/portal.c12
-rw-r--r--src/capture/v4l2.c19
-rw-r--r--src/dbus.c9
-rw-r--r--src/encoder/video/vaapi.c2
-rw-r--r--src/encoder/video/vulkan.c17
-rw-r--r--src/main.cpp122
-rw-r--r--src/pipewire_video.c187
-rw-r--r--src/sound.cpp20
19 files changed, 545 insertions, 175 deletions
diff --git a/README.md b/README.md
index 4ce7221..a39f011 100644
--- a/README.md
+++ b/README.md
@@ -33,22 +33,25 @@ If you are running an Arch Linux based distro then you can find gpu screen recor
If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\
You can also install gpu screen recorder ([the ui version](https://git.dec05eba.com/gpu-screen-recorder-gtk/)) from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder), which is the easiest method
to install GPU Screen Recorder on non-arch based distros.\
-If you install GPU Screen Recorder flatpak, which is the gtk gui version then you can still run GPU Screen Recorder command line by using the flatpak command option, for example `flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder -w screen -f 60 -o video.mp4`. Note that if you want to record your monitor on AMD/Intel then you need to install the flatpak system-wide (like so: `flatpak install --system com.dec05eba.gpu_screen_recorder`).
+You can alternatively install GPU Screen Recorder from one of the unofficial sources mentioned below.\
+If you install GPU Screen Recorder flatpak, which is the ui version then you can still run GPU Screen Recorder command line by using the flatpak command option, for example `flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder -w screen -f 60 -o video.mp4`. Note that if you want to record your monitor on AMD/Intel then you need to install the flatpak system-wide (like so: `flatpak install --system com.dec05eba.gpu_screen_recorder`).
## Unofficial install methods
The only official ways to install GPU Screen Recorder is either from source, arch linux extra repository or flathub. Other sources may be out of date and missing features or may not work correctly.\
If you install GPU Screen Recorder from somewhere else and have an issue then try installing it from one of the official sources before reporting it as an issue.\
If you still prefer to install GPU Screen Recorder with a package manager instead of from source or as a flatpak then you may be able to find a package for your distro.\
Here are some known unofficial packages:
-* Ubuntu: [gpu-screen-recorder-cli](https://launchpad.net/ubuntu/+source/gpu-screen-recorder)
-* Debian: [gpu-screen-recorder-cli](https://tracker.debian.org/pkg/gpu-screen-recorder)
+* Alpine Linux: [gpu-screen-recorder](https://pkgs.alpinelinux.org/package/edge/testing/x86/gpu-screen-recorder)
+* AppImage [AppImage GitHub releases](https://github.com/pkgforge-dev/gpu-screen-recorder-AppImage/releases)
+* CentOS: [Copr](https://copr.fedorainfracloud.org/coprs/brycensranch/gpu-screen-recorder-git/)
+* Debian: [gpu-screen-recorder-cli](https://tracker.debian.org/pkg/gpu-screen-recorder) or [gpu-screen-recorder on github](https://github.com/Sarisan/gpu-screen-recorder)
+* Fedora: [Copr](https://copr.fedorainfracloud.org/coprs/brycensranch/gpu-screen-recorder-git/)
* Nix: [NixOS wiki](https://wiki.nixos.org/wiki/Gpu-screen-recorder)
-* openSUSE: [openSUSE software repository](https://software.opensuse.org/package/gpu-screen-recorder)
-* Fedora, CentOS: [Copr](https://copr.fedorainfracloud.org/coprs/brycensranch/gpu-screen-recorder-git/)
+* Nobara: [Nobara wiki](https://wiki.nobaraproject.org/en/general-usage/additional-software/GPU-Screen-Recorder)
* OpenMandriva: [gpu-screen-recorder](https://github.com/OpenMandrivaAssociation/gpu-screen-recorder)
+* openSUSE: [openSUSE software repository](https://software.opensuse.org/package/gpu-screen-recorder)
* Solus: [gpu-screen-recorder](https://github.com/getsolus/packages/tree/main/packages/g/gpu-screen-recorder)
-* Nobara: [Nobara wiki](https://wiki.nobaraproject.org/en/general-usage/additional-software/GPU-Screen-Recorder)
-* AppImage [AppImage GitHub releases](https://github.com/pkgforge-dev/gpu-screen-recorder-AppImage/releases)
+* Ubuntu: [gpu-screen-recorder-cli](https://launchpad.net/ubuntu/+source/gpu-screen-recorder)
* Void Linux: [gpu-screen-recorder](https://github.com/cherrybtw/gsr4xbps)
# Dependencies
@@ -286,3 +289,9 @@ This is an nvidia power management driver bug which can happen when the system i
## Recording fails when using webcam on nvidia X11
This is a known issue. The issue is that nvfbc which is used for screen capture only supports glx on older systems and glx is not compatible with the webcam capture method used in GPU Screen Recorder.
This will be fixed in the future.
+## Desktop portal capture doesn't work on my AMD GPU on Hyprland
+This is a hyprland bug. Capture the monitor directly with `-w monitor` or use another Wayland compositor.
+## The captured video is laggy when capturing with desktop portal on COSMIC/Wlroots (sway)
+This is a bug in COSMIC and Wlroots. Their desktop portals send frame updates at low framerates (such as 25 fps) instead of the selected framerate (such as 60 fps). Capture a monitor directly instead (`-w DP-1` for example) to workaround that problem.
+## How to adjust the volume of the recorded audio?
+GPU Screen Recorder doesn't have the option to do that yet, but you can change the volume by starting recording and then open `pavucontrol`, go to the `Recording` tab and set the volume there. The system will remember the settings even after reboot, as long as the audio capture options remain the same.
diff --git a/TODO b/TODO
index 933df3a..df7907c 100644
--- a/TODO
+++ b/TODO
@@ -187,7 +187,7 @@ Always disable prime run/dri prime and list all monitors to record from from all
automatically use the associated gpu card.
Allow flv av1 if recent ffmpeg version and streaming to youtube (and twitch?) and for custom services.
-Use explicit sync in pipewire video code: https://docs.pipewire.org/page_dma_buf.html.
+Use explicit sync in pipewire video code: https://docs.pipewire.org/page_dma_buf.html. This should fix cross gpu pipewire capture.
Replay (and recording?) fails to save properly sometimes (especially for long videos). This is noticable with mp4 files since they get corrupt and become unplayable.
The entire video does seem to get saved (it's a large video file) and it seems to have the correct headers but it's not playable.
@@ -292,9 +292,6 @@ We can use dri2connect/dri3open to get the /dev/dri/card device. Note that this
Add support for QVBR (QP with target bitrate). Maybe use VBR instead, since nvidia doesn't support QVBR and neither does vulkan.
-KDE Plasma Wayland seems to use overlay planes now in non-fullscreen mode(limited to 1 overlay plane per gpu). Check if this is the case in the latest kde on arch linux.
- If it is, then support it in kms capture.
-
Check if pipewire audio link-factory is available before attempting to use app audio or merging audio with pipewire.
Also do the same in supports_app_audio check in gpu-screen-recorder --info output.
diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1
index 40a3b3e..564d35b 100644
--- a/gpu-screen-recorder.1
+++ b/gpu-screen-recorder.1
@@ -602,19 +602,13 @@ Report bugs to:
dec05eba@protonmail.com
.UE .
.br
-See more information about reporting bugs at the gpu-screen-recorder website:
-.UR https://git.dec05eba.com/?p=about
-https://git.dec05eba.com/?p=about
-.UE .
+See more information about reporting bugs at the gpu-screen-recorder website: \fIhttps://git.dec05eba.com/?p=about\fR.
.br
-Please take a look at the FAQ part of the README before reporting a bug:
-.UR https://git.dec05eba.com/gpu-screen-recorder/about/
-https://git.dec05eba.com/gpu-screen-recorder/about/
-.UE .
+Please take a look at the FAQ part of the README before reporting a bug: \fIhttps://git.dec05eba.com/gpu-screen-recorder/about\fR.
.br
The bug may have been previously reported or may not be related to gpu-screen-recorder (such as a driver issue).
.SH COPYRIGHT
-Copyright © dec05eba. Licensed under GPL3-only.
+Copyright © dec05eba. Licensed under GPL-3.0-only.
.SH SEE ALSO
.UR https://git.dec05eba.com/gpu-screen-recorder
Project homepage
@@ -634,4 +628,4 @@ ShadowPlay-style UI
.br
.BR xdotool (1),
.BR slop (1),
-.BR slurp (1), \ No newline at end of file
+.BR slurp (1),
diff --git a/gsr-kms-server.1 b/gsr-kms-server.1
index 330a893..720843f 100644
--- a/gsr-kms-server.1
+++ b/gsr-kms-server.1
@@ -62,9 +62,9 @@ Report bugs to:
dec05eba@protonmail.com
.UE .
.SH COPYRIGHT
-Copyright © dec05eba. Licensed under GPL3-only.
+Copyright © dec05eba. Licensed under GPL-3.0-only.
.SH SEE ALSO
.BR gpu\-screen\-recorder (1),
.BR drm (4)
.PP
-Project homepage: <https://git.dec05eba.com/gpu-screen-recorder/about/> \ No newline at end of file
+Project homepage: <https://git.dec05eba.com/gpu-screen-recorder/about/>
diff --git a/include/pipewire_video.h b/include/pipewire_video.h
index 38ec3d6..00228c5 100644
--- a/include/pipewire_video.h
+++ b/include/pipewire_video.h
@@ -85,6 +85,7 @@ typedef struct {
struct {
bool visible;
bool valid;
+ bool updated;
uint8_t *data;
int x, y;
int hotspot_x, hotspot_y;
@@ -126,6 +127,12 @@ bool gsr_pipewire_video_init(gsr_pipewire_video *self, int pipewire_fd, uint32_t
void gsr_pipewire_video_deinit(gsr_pipewire_video *self);
bool gsr_pipewire_video_map_texture(gsr_pipewire_video *self, gsr_texture_map texture_map, gsr_map_texture_output *output);
+/*
+ Returns true if the cursor has changed (moved, changed image or changed visibility) since the last call to
+ gsr_pipewire_video_map_texture or gsr_pipewire_video_update_cursor. This only happens when the wayland compositor
+ supports cursor metadata mode. |cursor_region| and the cursor texture in |texture_map| are updated with the latest cursor state.
+*/
+bool gsr_pipewire_video_update_cursor(gsr_pipewire_video *self, gsr_texture_map texture_map, gsr_pipewire_video_region *cursor_region);
bool gsr_pipewire_video_is_damaged(gsr_pipewire_video *self);
void gsr_pipewire_video_clear_damage(gsr_pipewire_video *self);
bool gsr_pipewire_video_should_restart(gsr_pipewire_video *self);
diff --git a/include/sound.hpp b/include/sound.hpp
index c7164f8..6d903d8 100644
--- a/include/sound.hpp
+++ b/include/sound.hpp
@@ -71,6 +71,13 @@ int sound_device_get_by_name(SoundDevice *device, const char *node_name, const c
void sound_device_close(SoundDevice *device);
/*
+ Discards the audio that has been captured so far.
+ Call this before the first call to sound_device_read_next_chunk to not get audio that was captured before that point,
+ since the sound device can be created a while before audio capture starts.
+*/
+void sound_device_flush(SoundDevice *device);
+
+/*
Returns the next chunk of audio into @buffer.
Returns the number of frames read, or a negative value on failure.
*/
diff --git a/kms/kms_shared.h b/kms/kms_shared.h
index c982723..81e0d77 100644
--- a/kms/kms_shared.h
+++ b/kms/kms_shared.h
@@ -5,7 +5,7 @@
#include <stdbool.h>
#include <drm_mode.h>
-#define GSR_KMS_PROTOCOL_VERSION 5
+#define GSR_KMS_PROTOCOL_VERSION 6
#define GSR_KMS_MAX_ITEMS 8
#define GSR_KMS_MAX_DMA_BUFS 4
@@ -46,6 +46,12 @@ typedef enum {
KMS_ROT_270
} gsr_kms_rotation;
+typedef enum {
+ KMS_PLANE_TYPE_PRIMARY,
+ KMS_PLANE_TYPE_CURSOR,
+ KMS_PLANE_TYPE_OVERLAY
+} gsr_kms_plane_type;
+
struct gsr_kms_response_item {
gsr_kms_response_dma_buf dma_buf[GSR_KMS_MAX_DMA_BUFS];
int num_dma_bufs;
@@ -54,13 +60,20 @@ struct gsr_kms_response_item {
uint32_t pixel_format;
uint64_t modifier;
uint32_t connector_id; /* 0 if unknown */
- bool is_cursor;
+ gsr_kms_plane_type plane_type;
bool has_hdr_metadata;
gsr_kms_rotation rotation;
- int x;
- int y;
+ /* The region in the framebuffer that is displayed */
+ int src_x;
+ int src_y;
int src_w;
int src_h;
+ /* The region on the monitor (crtc) where the framebuffer region is displayed */
+ int dst_x;
+ int dst_y;
+ int dst_w;
+ int dst_h;
+ int zpos;
struct hdr_output_metadata hdr_metadata;
};
diff --git a/kms/server/kms_server.c b/kms/server/kms_server.c
index 6714486..c41f8bd 100644
--- a/kms/server/kms_server.c
+++ b/kms/server/kms_server.c
@@ -145,17 +145,23 @@ typedef enum {
PLANE_PROPERTY_IS_CURSOR = 1 << 6,
PLANE_PROPERTY_IS_PRIMARY = 1 << 7,
PLANE_PROPERTY_ROTATION = 1 << 8,
+ PLANE_PROPERTY_W = 1 << 9,
+ PLANE_PROPERTY_H = 1 << 10,
+ PLANE_PROPERTY_IS_OVERLAY = 1 << 11,
+ PLANE_PROPERTY_ZPOS = 1 << 12,
} plane_property_mask;
+typedef struct {
+ int x, y, w, h; /* Region on the crtc (monitor) where the plane is displayed (CRTC_* properties) */
+ int src_x, src_y, src_w, src_h; /* Region in the framebuffer that is displayed (SRC_* properties) */
+ int zpos;
+ gsr_kms_rotation rotation;
+} plane_properties;
+
/* Returns plane_property_mask */
-static uint32_t plane_get_properties(int drmfd, uint32_t plane_id, int *x, int *y, int *src_x, int *src_y, int *src_w, int *src_h, gsr_kms_rotation *rotation) {
- *x = 0;
- *y = 0;
- *src_x = 0;
- *src_y = 0;
- *src_w = 0;
- *src_h = 0;
- *rotation = KMS_ROT_0;
+static uint32_t plane_get_properties(int drmfd, uint32_t plane_id, plane_properties *properties) {
+ memset(properties, 0, sizeof(*properties));
+ properties->rotation = KMS_ROT_0;
plane_property_mask property_mask = 0;
@@ -172,23 +178,35 @@ static uint32_t plane_get_properties(int drmfd, uint32_t plane_id, int *x, int *
// SRC_* values are fixed 16.16 points
const uint32_t type = prop->flags & (DRM_MODE_PROP_LEGACY_TYPE | DRM_MODE_PROP_EXTENDED_TYPE);
if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_X") == 0) {
- *x = (int)props->prop_values[i];
+ properties->x = (int)props->prop_values[i];
property_mask |= PLANE_PROPERTY_X;
} else if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_Y") == 0) {
- *y = (int)props->prop_values[i];
+ properties->y = (int)props->prop_values[i];
property_mask |= PLANE_PROPERTY_Y;
+ } else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "CRTC_W") == 0) {
+ properties->w = (int)props->prop_values[i];
+ property_mask |= PLANE_PROPERTY_W;
+ } else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "CRTC_H") == 0) {
+ properties->h = (int)props->prop_values[i];
+ property_mask |= PLANE_PROPERTY_H;
} else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "SRC_X") == 0) {
- *src_x = (int)(props->prop_values[i] >> 16);
+ properties->src_x = (int)(props->prop_values[i] >> 16);
property_mask |= PLANE_PROPERTY_SRC_X;
} else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "SRC_Y") == 0) {
- *src_y = (int)(props->prop_values[i] >> 16);
+ properties->src_y = (int)(props->prop_values[i] >> 16);
property_mask |= PLANE_PROPERTY_SRC_Y;
} else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "SRC_W") == 0) {
- *src_w = (int)(props->prop_values[i] >> 16);
+ properties->src_w = (int)(props->prop_values[i] >> 16);
property_mask |= PLANE_PROPERTY_SRC_W;
} else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "SRC_H") == 0) {
- *src_h = (int)(props->prop_values[i] >> 16);
+ properties->src_h = (int)(props->prop_values[i] >> 16);
property_mask |= PLANE_PROPERTY_SRC_H;
+ } else if(((type & DRM_MODE_PROP_RANGE) || (type & DRM_MODE_PROP_SIGNED_RANGE)) && (strcmp(prop->name, "zpos") == 0 || strcmp(prop->name, "ZPOS") == 0)) {
+ if(type & DRM_MODE_PROP_SIGNED_RANGE)
+ properties->zpos = (int)(int64_t)props->prop_values[i];
+ else
+ properties->zpos = (int)props->prop_values[i];
+ property_mask |= PLANE_PROPERTY_ZPOS;
} else if((type & DRM_MODE_PROP_ENUM) && strcmp(prop->name, "type") == 0) {
const uint64_t current_enum_value = props->prop_values[i];
for(int j = 0; j < prop->count_enums; ++j) {
@@ -198,17 +216,20 @@ static uint32_t plane_get_properties(int drmfd, uint32_t plane_id, int *x, int *
} else if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Cursor") == 0) {
property_mask |= PLANE_PROPERTY_IS_CURSOR;
break;
+ } else if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Overlay") == 0) {
+ property_mask |= PLANE_PROPERTY_IS_OVERLAY;
+ break;
}
}
} else if((type & DRM_MODE_PROP_BITMASK) && strcmp(prop->name, "rotation") == 0) {
const uint64_t rotation_bitmask = props->prop_values[i];
- *rotation = KMS_ROT_0;
+ properties->rotation = KMS_ROT_0;
if(rotation_bitmask & 2)
- *rotation = (*rotation + KMS_ROT_90) % 4;
+ properties->rotation = (properties->rotation + KMS_ROT_90) % 4;
if(rotation_bitmask & 4)
- *rotation = (*rotation + KMS_ROT_180) % 4;
+ properties->rotation = (properties->rotation + KMS_ROT_180) % 4;
if(rotation_bitmask & 8)
- *rotation = (*rotation + KMS_ROT_270) % 4;
+ properties->rotation = (properties->rotation + KMS_ROT_270) % 4;
}
drmModeFreeProperty(prop);
@@ -350,11 +371,41 @@ static int kms_get_fb(gsr_drm *drm, gsr_kms_response *response) {
// TODO: Check if dimensions have changed by comparing width and height to previous time this was called.
// TODO: Support other plane formats than rgb (with multiple planes, such as direct YUV420 on wayland).
- int x = 0, y = 0, src_x = 0, src_y = 0, src_w = 0, src_h = 0;
- gsr_kms_rotation rotation = KMS_ROT_0;
- const uint32_t property_mask = plane_get_properties(drm->drmfd, plane->plane_id, &x, &y, &src_x, &src_y, &src_w, &src_h, &rotation);
- if(!(property_mask & PLANE_PROPERTY_IS_PRIMARY) && !(property_mask & PLANE_PROPERTY_IS_CURSOR))
- continue;
+ plane_properties properties;
+ const uint32_t property_mask = plane_get_properties(drm->drmfd, plane->plane_id, &properties);
+ if(!(property_mask & (PLANE_PROPERTY_IS_PRIMARY | PLANE_PROPERTY_IS_CURSOR | PLANE_PROPERTY_IS_OVERLAY)))
+ goto cleanup_handles;
+
+ gsr_kms_plane_type plane_type = KMS_PLANE_TYPE_OVERLAY;
+ if(property_mask & PLANE_PROPERTY_IS_PRIMARY)
+ plane_type = KMS_PLANE_TYPE_PRIMARY;
+ else if(property_mask & PLANE_PROPERTY_IS_CURSOR)
+ plane_type = KMS_PLANE_TYPE_CURSOR;
+
+ /* These properties are not available when the drm driver doesn't support atomic modesetting */
+ if(!(property_mask & PLANE_PROPERTY_SRC_W))
+ properties.src_w = drmfb->width;
+ if(!(property_mask & PLANE_PROPERTY_SRC_H))
+ properties.src_h = drmfb->height;
+ if(!(property_mask & PLANE_PROPERTY_W))
+ properties.w = properties.src_w;
+ if(!(property_mask & PLANE_PROPERTY_H))
+ properties.h = properties.src_h;
+
+ /* Not all drm drivers support zpos. In that case assume that the planes are stacked in the order primary, overlay, cursor (bottom to top) */
+ if(!(property_mask & PLANE_PROPERTY_ZPOS)) {
+ switch(plane_type) {
+ case KMS_PLANE_TYPE_PRIMARY:
+ properties.zpos = 0;
+ break;
+ case KMS_PLANE_TYPE_OVERLAY:
+ properties.zpos = 1;
+ break;
+ case KMS_PLANE_TYPE_CURSOR:
+ properties.zpos = 2;
+ break;
+ }
+ }
int fb_fds[GSR_KMS_MAX_DMA_BUFS];
const int num_fb_fds = drm_prime_handles_to_fds(drm, drmfb, fb_fds);
@@ -386,19 +437,17 @@ static int kms_get_fb(gsr_drm *drm, gsr_kms_response *response) {
response->items[item_index].pixel_format = drmfb->pixel_format;
response->items[item_index].modifier = drmfb->flags & DRM_MODE_FB_MODIFIERS ? drmfb->modifier : DRM_FORMAT_MOD_INVALID;
response->items[item_index].connector_id = crtc_pair ? crtc_pair->connector_id : 0;
- response->items[item_index].rotation = rotation;
- response->items[item_index].is_cursor = property_mask & PLANE_PROPERTY_IS_CURSOR;
- if(property_mask & PLANE_PROPERTY_IS_CURSOR) {
- response->items[item_index].x = x;
- response->items[item_index].y = y;
- response->items[item_index].src_w = 0;
- response->items[item_index].src_h = 0;
- } else {
- response->items[item_index].x = src_x;
- response->items[item_index].y = src_y;
- response->items[item_index].src_w = src_w;
- response->items[item_index].src_h = src_h;
- }
+ response->items[item_index].rotation = properties.rotation;
+ response->items[item_index].plane_type = plane_type;
+ response->items[item_index].src_x = properties.src_x;
+ response->items[item_index].src_y = properties.src_y;
+ response->items[item_index].src_w = properties.src_w;
+ response->items[item_index].src_h = properties.src_h;
+ response->items[item_index].dst_x = properties.x;
+ response->items[item_index].dst_y = properties.y;
+ response->items[item_index].dst_w = properties.w;
+ response->items[item_index].dst_h = properties.h;
+ response->items[item_index].zpos = properties.zpos;
++response->num_items;
cleanup_handles:
diff --git a/meson.build b/meson.build
index 70b02e5..6865bbd 100644
--- a/meson.build
+++ b/meson.build
@@ -1,4 +1,4 @@
-project('gpu-screen-recorder', ['c', 'cpp'], version : '5.13.9', default_options : ['warning_level=2'])
+project('gpu-screen-recorder', ['c', 'cpp'], version : '5.14.1', default_options : ['warning_level=2'])
add_project_arguments('-Wshadow', language : ['c', 'cpp'])
if get_option('buildtype') == 'debug'
diff --git a/project.conf b/project.conf
index e6e43eb..40ae00e 100644
--- a/project.conf
+++ b/project.conf
@@ -1,7 +1,7 @@
[package]
name = "gpu-screen-recorder"
type = "executable"
-version = "5.13.9"
+version = "5.14.1"
platforms = ["posix"]
[config]
diff --git a/src/capture/kms.c b/src/capture/kms.c
index fe4eb40..1544e2c 100644
--- a/src/capture/kms.c
+++ b/src/capture/kms.c
@@ -218,7 +218,7 @@ static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture
static gsr_kms_response_item* find_drm_by_connector_id(gsr_kms_response *kms_response, uint32_t connector_id) {
for(int i = 0; i < kms_response->num_items; ++i) {
- if(kms_response->items[i].connector_id == connector_id && !kms_response->items[i].is_cursor)
+ if(kms_response->items[i].connector_id == connector_id && kms_response->items[i].plane_type == KMS_PLANE_TYPE_PRIMARY)
return &kms_response->items[i];
}
return NULL;
@@ -232,7 +232,7 @@ static gsr_kms_response_item* find_largest_drm(gsr_kms_response *kms_response) {
gsr_kms_response_item *largest_drm = &kms_response->items[0];
for(int i = 0; i < kms_response->num_items; ++i) {
const int64_t size = (int64_t)kms_response->items[i].width * (int64_t)kms_response->items[i].height;
- if(size > largest_size && !kms_response->items[i].is_cursor) {
+ if(size > largest_size && kms_response->items[i].plane_type == KMS_PLANE_TYPE_PRIMARY) {
largest_size = size;
largest_drm = &kms_response->items[i];
}
@@ -243,7 +243,7 @@ static gsr_kms_response_item* find_largest_drm(gsr_kms_response *kms_response) {
static gsr_kms_response_item* find_cursor_drm(gsr_kms_response *kms_response, uint32_t connector_id) {
gsr_kms_response_item *cursor_drm = NULL;
for(int i = 0; i < kms_response->num_items; ++i) {
- if(kms_response->items[i].is_cursor) {
+ if(kms_response->items[i].plane_type == KMS_PLANE_TYPE_CURSOR) {
cursor_drm = &kms_response->items[i];
if(kms_response->items[i].connector_id == connector_id)
break;
@@ -406,7 +406,7 @@ static void render_drm_cursor(gsr_capture_kms *self, gsr_color_conversion *color
const gsr_monitor_rotation cursor_plane_rotation = kms_rotation_to_gsr_monitor_rotation(cursor_drm_fd->rotation);
const gsr_monitor_rotation rotation = sub_rotations(self->display_server_monitor_rotation, cursor_plane_rotation);
- vec2i cursor_pos = {cursor_drm_fd->x, cursor_drm_fd->y};
+ vec2i cursor_pos = {cursor_drm_fd->dst_x, cursor_drm_fd->dst_y};
switch(rotation) {
case GSR_MONITOR_ROT_0:
break;
@@ -584,23 +584,14 @@ static void gsr_capture_kms_pre_capture(gsr_capture *cap, gsr_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->drm_fd || self->params.kms_response->num_items == 0)
- return -1;
-
+static void render_monitor_plane(gsr_capture_kms *self, gsr_color_conversion *color_conversion, gsr_capture_metadata *capture_metadata) {
vec2i capture_pos = self->capture_pos;
if(!self->capture_is_combined_plane)
- capture_pos = (vec2i){self->drm_fd->x, self->drm_fd->y};
+ capture_pos = (vec2i){self->drm_fd->src_x, self->drm_fd->src_y};
capture_pos.x += self->params.region_position.x;
capture_pos.y += self->params.region_position.y;
- //self->params.egl->glFlush();
- //self->params.egl->glFinish();
-
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);
@@ -611,6 +602,110 @@ static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *captu
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(self->final_monitor_rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, self->external_texture_fallback);
+}
+
+/* Renders an overlay plane on top of (or below, depending on render order) the monitor plane, scaled to the output */
+static void render_drm_plane(gsr_capture_kms *self, gsr_color_conversion *color_conversion, gsr_capture_metadata *capture_metadata, const gsr_kms_response_item *plane_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
+ };
+
+ const gsr_monitor_rotation plane_rotation = kms_rotation_to_gsr_monitor_rotation(plane_drm_fd->rotation);
+ const gsr_monitor_rotation rotation = sub_rotations(self->display_server_monitor_rotation, plane_rotation);
+
+ const vec2i plane_size = {plane_drm_fd->dst_w, plane_drm_fd->dst_h};
+ vec2i plane_pos = {plane_drm_fd->dst_x, plane_drm_fd->dst_y};
+ switch(rotation) {
+ case GSR_MONITOR_ROT_0:
+ break;
+ case GSR_MONITOR_ROT_90:
+ plane_pos = swap_vec2i(plane_pos);
+ plane_pos.x = framebuffer_size.x - plane_pos.x;
+ // TODO: Remove this horrible hack
+ plane_pos.x -= plane_size.x;
+ break;
+ case GSR_MONITOR_ROT_180:
+ plane_pos.x = framebuffer_size.x - plane_pos.x;
+ plane_pos.y = framebuffer_size.y - plane_pos.y;
+ // TODO: Remove this horrible hack
+ plane_pos.x -= plane_size.x;
+ plane_pos.y -= plane_size.y;
+ break;
+ case GSR_MONITOR_ROT_270:
+ plane_pos = swap_vec2i(plane_pos);
+ plane_pos.y = framebuffer_size.y - plane_pos.y;
+ // TODO: Remove this horrible hack
+ plane_pos.y -= plane_size.y;
+ break;
+ }
+
+ plane_pos.x -= self->params.region_position.x;
+ plane_pos.y -= self->params.region_position.y;
+
+ plane_pos.x *= scale.x;
+ plane_pos.y *= scale.y;
+
+ plane_pos.x += target_pos.x;
+ plane_pos.y += target_pos.y;
+
+ EGLImage image = gsr_capture_kms_create_egl_image_with_fallback(self, plane_drm_fd);
+ if(!image)
+ return;
+
+ gsr_capture_kms_bind_image_to_input_texture_with_fallback(self, image);
+ self->params.egl->eglDestroyImage(self->params.egl->egl_display, image);
+
+ 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->external_texture_fallback ? self->external_input_texture_id : self->input_texture_id,
+ plane_pos, (vec2i){plane_size.x * scale.x, plane_size.y * scale.y},
+ (vec2i){plane_drm_fd->src_x, plane_drm_fd->src_y}, (vec2i){plane_drm_fd->src_w, plane_drm_fd->src_h}, (vec2i){plane_drm_fd->width, plane_drm_fd->height},
+ gsr_monitor_rotation_to_rotation(rotation), capture_metadata->flip, GSR_SOURCE_COLOR_RGB, self->external_texture_fallback);
+
+ self->params.egl->glDisable(GL_SCISSOR_TEST);
+}
+
+static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *capture_metadata, gsr_color_conversion *color_conversion) {
+ gsr_capture_kms *self = cap->priv;
+
+ if(!self->drm_fd || self->params.kms_response->num_items == 0)
+ return -1;
+
+ const vec2i framebuffer_size = rotate_capture_size_if_rotated(self, (vec2i){ self->drm_fd->src_w, self->drm_fd->src_h }, self->final_monitor_rotation);
+
+ //self->params.egl->glFlush();
+ //self->params.egl->glFinish();
+
+ /* Gather all planes that are displayed on the captured monitor. Overlay planes are not used on x11 (combined plane) */
+ const gsr_kms_response_item *planes[GSR_KMS_MAX_ITEMS];
+ int num_planes = 0;
+ planes[num_planes++] = self->drm_fd;
+ if(!self->capture_is_combined_plane) {
+ for(int i = 0; i < self->params.kms_response->num_items && num_planes < GSR_KMS_MAX_ITEMS; ++i) {
+ const gsr_kms_response_item *item = &self->params.kms_response->items[i];
+ if(item->plane_type == KMS_PLANE_TYPE_OVERLAY && item->connector_id == self->drm_fd->connector_id)
+ planes[num_planes++] = item;
+ }
+ }
+
+ /* Sort the planes by zpos, from bottom to top. Insertion sort to keep planes with the same zpos in the order the drm driver returned them (stable) */
+ for(int i = 1; i < num_planes; ++i) {
+ const gsr_kms_response_item *plane = planes[i];
+ int j = i - 1;
+ for(; j >= 0 && planes[j]->zpos > plane->zpos; --j) {
+ planes[j + 1] = planes[j];
+ }
+ planes[j + 1] = plane;
+ }
+
+ for(int i = 0; i < num_planes; ++i) {
+ if(planes[i] == self->drm_fd)
+ render_monitor_plane(self, color_conversion, capture_metadata);
+ else
+ render_drm_plane(self, color_conversion, capture_metadata, planes[i], self->target_pos, self->output_size, framebuffer_size);
+ }
if(self->params.record_cursor) {
gsr_kms_response_item *cursor_drm_fd = find_cursor_drm_if_on_monitor(self, self->drm_fd->connector_id, self->capture_is_combined_plane);
@@ -624,7 +719,6 @@ static int gsr_capture_kms_capture(gsr_capture *cap, gsr_capture_metadata *captu
cursor_monitor_offset.y += self->params.region_position.y;
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){ self->drm_fd->src_w, self->drm_fd->src_h }, self->final_monitor_rotation);
render_drm_cursor(self, color_conversion, capture_metadata, cursor_drm_fd, self->target_pos, self->output_size, framebuffer_size);
}
}
diff --git a/src/capture/portal.c b/src/capture/portal.c
index c6f058d..fd1caec 100644
--- a/src/capture/portal.c
+++ b/src/capture/portal.c
@@ -201,7 +201,7 @@ static int gsr_capture_portal_setup_dbus(gsr_capture_portal *self, int *pipewire
}
fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: SelectSources\n");
- response_status = gsr_dbus_screencast_select_sources(&self->dbus, self->session_handle, GSR_PORTAL_CAPTURE_TYPE_ALL, self->params.record_cursor ? GSR_PORTAL_CURSOR_MODE_EMBEDDED : GSR_PORTAL_CURSOR_MODE_HIDDEN);
+ response_status = gsr_dbus_screencast_select_sources(&self->dbus, self->session_handle, GSR_PORTAL_CAPTURE_TYPE_ALL, self->params.record_cursor ? GSR_PORTAL_CURSOR_MODE_METADATA : GSR_PORTAL_CURSOR_MODE_HIDDEN);
if(response_status != 0) {
fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: SelectSources failed\n");
return response_status;
@@ -347,7 +347,10 @@ static void gsr_capture_portal_pre_capture(gsr_capture *cap, gsr_capture_metadat
self->capture_size.y = self->pipewire_data.region.height;
color_conversion->schedule_clear = true;
}
- } else {
+ } else if(self->pipewire_data.texture_width == 0 || !gsr_pipewire_video_update_cursor(&self->pipewire, self->texture_map, &self->pipewire_data.cursor_region)) {
+ /* Theres no new video frame to capture and the cursor hasn't changed (which only happens in cursor metadata mode).
+ If the cursor has changed then the latest video frame is redrawn with the new cursor state, otherwise nothing is captured.
+ This is needed on gnome which doesn't send new video frames when capturing a window and only the cursor moves. */
return;
}
}
@@ -387,9 +390,10 @@ static int gsr_capture_portal_capture(gsr_capture *cap, gsr_capture_metadata *ca
self->capture_size.y == 0 ? 0 : (double)output_size.y / (double)self->capture_size.y
};
+ /* The cursor position is relative to the video buffer, remove the crop offset to make it relative to the visible video content */
const vec2i cursor_pos = {
- target_pos.x + (self->pipewire_data.cursor_region.x * scale.x),
- target_pos.y + (self->pipewire_data.cursor_region.y * scale.y)
+ target_pos.x + ((self->pipewire_data.cursor_region.x - self->pipewire_data.region.x) * scale.x),
+ target_pos.y + ((self->pipewire_data.cursor_region.y - self->pipewire_data.region.y) * scale.y)
};
self->params.egl->glEnable(GL_SCISSOR_TEST);
diff --git a/src/capture/v4l2.c b/src/capture/v4l2.c
index cd72293..a4e81e4 100644
--- a/src/capture/v4l2.c
+++ b/src/capture/v4l2.c
@@ -883,6 +883,23 @@ gsr_capture* gsr_capture_v4l2_create(const gsr_capture_v4l2_params *params) {
return cap;
}
+static int gsr_capture_v4l2_supported_setup_compare(const void *a, const void *b) {
+ const gsr_capture_v4l2_supported_setup *setup_a = a;
+ const gsr_capture_v4l2_supported_setup *setup_b = b;
+
+ const uint64_t resolution_a = (uint64_t)setup_a->resolution.width * (uint64_t)setup_a->resolution.height;
+ const uint64_t resolution_b = (uint64_t)setup_b->resolution.width * (uint64_t)setup_b->resolution.height;
+ if(resolution_a != resolution_b)
+ return resolution_a < resolution_b ? 1 : -1;
+
+ const uint32_t framerate_a = gsr_capture_v4l2_framerate_to_number(setup_a->framerate);
+ const uint32_t framerate_b = gsr_capture_v4l2_framerate_to_number(setup_b->framerate);
+ if(framerate_a != framerate_b)
+ return framerate_a < framerate_b ? 1 : -1;
+
+ return 0;
+}
+
void gsr_capture_v4l2_list_devices(v4l2_devices_query_callback callback, void *userdata) {
const bool has_libturbojpeg_lib = is_libturbojpeg_library_available();
char v4l2_device_path[128];
@@ -915,6 +932,8 @@ void gsr_capture_v4l2_list_devices(v4l2_devices_query_callback callback, void *u
if(num_supported_setups == 0)
continue;
+ qsort(supported_setups, num_supported_setups, sizeof(gsr_capture_v4l2_supported_setup), gsr_capture_v4l2_supported_setup_compare);
+
for(size_t j = 0; j < num_supported_setups; ++j) {
const gsr_capture_v4l2_supported_setup *setup = &supported_setups[j];
callback(v4l2_device_path, setup, userdata);
diff --git a/src/dbus.c b/src/dbus.c
index 8ba58bc..715a021 100644
--- a/src/dbus.c
+++ b/src/dbus.c
@@ -650,8 +650,15 @@ int gsr_dbus_screencast_select_sources(gsr_dbus *self, const char *session_handl
uint32_t available_cursor_modes = 0;
gsr_dbus_desktop_portal_get_property(self, "org.freedesktop.portal.ScreenCast", "AvailableCursorModes", &available_cursor_modes);
if(available_cursor_modes == 0)
- fprintf(stderr, "gsr error: gsr_dbus_screencast_select_sources: no cursors modes are available\n");
+ fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: no cursors modes are available\n");
+
+ if(cursor_mode == GSR_PORTAL_CURSOR_MODE_METADATA && !(available_cursor_modes & GSR_PORTAL_CURSOR_MODE_METADATA)) {
+ fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: cursor mode metadata is not available, using cursor mode embedded instead\n");
+ cursor_mode = GSR_PORTAL_CURSOR_MODE_EMBEDDED;
+ }
cursor_mode = unset_unsupported_cursor_modes(cursor_mode, available_cursor_modes);
+ if(cursor_mode == 0)
+ cursor_mode = GSR_PORTAL_CURSOR_MODE_HIDDEN;
char handle_token[64];
gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token));
diff --git a/src/encoder/video/vaapi.c b/src/encoder/video/vaapi.c
index 81c03c8..449fb1d 100644
--- a/src/encoder/video/vaapi.c
+++ b/src/encoder/video/vaapi.c
@@ -264,7 +264,7 @@ static bool gsr_video_encoder_vaapi_start(gsr_video_encoder *encoder, AVCodecCon
}
if(FFALIGN(video_codec_context->width, 2) != FFALIGN(frame->width, 2) || FFALIGN(video_codec_context->height, 2) != FFALIGN(frame->height, 2)) {
- fprintf(stderr, "gsr warning: gsr_video_encoder_vaapi_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264 codec instead (-k h264) to get around this issue\n");
+ fprintf(stderr, "gsr warning: gsr_video_encoder_vaapi_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc codec instead (-k h264) to get around this issue\n");
}
if(video_codec_context->width < 128)
diff --git a/src/encoder/video/vulkan.c b/src/encoder/video/vulkan.c
index 89836df..3b7c567 100644
--- a/src/encoder/video/vulkan.c
+++ b/src/encoder/video/vulkan.c
@@ -471,8 +471,21 @@ static void gsr_video_encoder_vulkan_stop(gsr_video_encoder_vulkan *self, AVCode
static bool gsr_video_encoder_vulkan_start(gsr_video_encoder *encoder, AVCodecContext *video_codec_context, AVFrame *frame) {
gsr_video_encoder_vulkan *self = encoder->priv;
- video_codec_context->width = FFALIGN(video_codec_context->width, 2);
- video_codec_context->height = FFALIGN(video_codec_context->height, 2);
+ if(self->params.egl->gpu_info.vendor == GSR_GPU_VENDOR_AMD && video_codec_context->codec_id == AV_CODEC_ID_HEVC) {
+ video_codec_context->width = FFALIGN(video_codec_context->width, 2);
+ video_codec_context->height = FFALIGN(video_codec_context->height, 2);
+ } else if(self->params.egl->gpu_info.vendor == GSR_GPU_VENDOR_AMD && video_codec_context->codec_id == AV_CODEC_ID_AV1) {
+ // TODO: Dont do this for VCN 5 and forward which should fix this hardware bug
+ video_codec_context->width = FFALIGN(video_codec_context->width, 64);
+ video_codec_context->height = FFALIGN(video_codec_context->height, 16);
+ } else {
+ video_codec_context->width = FFALIGN(video_codec_context->width, 2);
+ video_codec_context->height = FFALIGN(video_codec_context->height, 2);
+ }
+
+ if(FFALIGN(video_codec_context->width, 2) != FFALIGN(frame->width, 2) || FFALIGN(video_codec_context->height, 2) != FFALIGN(frame->height, 2)) {
+ fprintf(stderr, "gsr warning: gsr_video_encoder_vulkan_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc vulkan codec instead (-k h264_vulkan) to get around this issue\n");
+ }
if(video_codec_context->width < 128)
video_codec_context->width = 128;
diff --git a/src/main.cpp b/src/main.cpp
index e4d1cbe..d64d7f4 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1124,7 +1124,7 @@ static RecordingStartResult start_recording_create_streams(const char *filename,
const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE);
if(open_ret < 0) {
- fprintf(stderr, "gsr error: start: could not open '%s': %s\n", filename, av_error_to_string(open_ret));
+ fprintf(stderr, "gsr error: start_recording_create_streams: could not open '%s': %s\n", filename, av_error_to_string(open_ret));
return result;
}
@@ -1137,7 +1137,7 @@ static RecordingStartResult start_recording_create_streams(const char *filename,
const int header_write_ret = avformat_write_header(av_format_context, &options);
av_dict_free(&options);
if(header_write_ret < 0) {
- fprintf(stderr, "gsr error: start: error occurred when writing header to output file: %s\n", av_error_to_string(header_write_ret));
+ fprintf(stderr, "gsr error: start_recording_create_streams: error occurred when writing header to output file: %s\n", av_error_to_string(header_write_ret));
avio_close(av_format_context->pb);
avformat_free_context(av_format_context);
return result;
@@ -1165,7 +1165,7 @@ static bool stop_recording_close_streams(AVFormatContext *av_format_context) {
return trailer_written && closed;
}
-static std::future<void> save_replay_thread;
+static std::future<bool> save_replay_thread;
static std::string save_replay_output_filepath;
static std::string create_new_recording_filepath_from_timestamp(std::string directory, const char *filename_prefix, const std::string &file_extension, bool date_folders) {
@@ -1196,9 +1196,9 @@ 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_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, std::vector<VideoSource> &video_sources, int current_save_replay_seconds) {
+static bool save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const std::vector<AudioTrack> &audio_tracks, gsr_encoder *encoder, const args_parser &arg_parser, 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;
+ return true;
pthread_mutex_lock(&encoder->replay_mutex);
gsr_replay_buffer *cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer);
@@ -1206,7 +1206,7 @@ static void save_replay_async(AVCodecContext *video_codec_context, int video_str
if(!cloned_replay_buffer) {
// TODO: Return this error to mark the replay as failed
fprintf(stderr, "gsr error: failed to save replay: failed to clone replay buffer\n");
- return;
+ return false;
}
const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == save_replay_seconds_full ? gsr_replay_buffer_iterator{0, 0} : gsr_replay_buffer_find_packet_index_by_time_passed(cloned_replay_buffer, current_save_replay_seconds);
@@ -1216,7 +1216,7 @@ static void save_replay_async(AVCodecContext *video_codec_context, int video_str
pthread_mutex_lock(&encoder->replay_mutex);
gsr_replay_buffer_destroy(cloned_replay_buffer);
pthread_mutex_unlock(&encoder->replay_mutex);
- return;
+ return true;
}
const int64_t video_pts_offset = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, video_start_iterator)->pts;
@@ -1235,13 +1235,15 @@ static void save_replay_async(AVCodecContext *video_codec_context, int video_str
pthread_mutex_lock(&encoder->replay_mutex);
gsr_replay_buffer_destroy(cloned_replay_buffer);
pthread_mutex_unlock(&encoder->replay_mutex);
- return;
+ return false;
}
save_replay_output_filepath = std::move(output_filepath);
save_replay_thread = std::async(std::launch::async, [video_stream_index, recording_start_result, video_start_iterator, video_pts_offset, audio_pts_offsets{std::move(audio_pts_offsets)}, video_codec_context, cloned_replay_buffer, encoder]() mutable {
+ bool success = true;
gsr_replay_buffer_iterator replay_iterator = video_start_iterator;
+
for(;;) {
AVPacket *replay_packet = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, replay_iterator);
uint8_t *replay_packet_data = NULL;
@@ -1253,11 +1255,13 @@ static void save_replay_async(AVCodecContext *video_codec_context, int video_str
if(!replay_packet) {
fprintf(stderr, "gsr error: save_replay_async: no replay packet\n");
+ success = false;
break;
}
if(!replay_packet->data && !replay_packet_data) {
fprintf(stderr, "gsr error: save_replay_async: no replay packet data\n");
+ success = false;
break;
}
@@ -1316,7 +1320,11 @@ static void save_replay_async(AVCodecContext *video_codec_context, int video_str
pthread_mutex_lock(&encoder->replay_mutex);
gsr_replay_buffer_destroy(cloned_replay_buffer);
pthread_mutex_unlock(&encoder->replay_mutex);
+
+ return success;
});
+
+ return true;
}
static void split_string(const std::string &str, char delimiter, std::function<bool(const char*,size_t)> callback) {
@@ -1513,6 +1521,10 @@ static int init_filter_graph(AVCodecContext* audio_codec_context, AVFilterGraph*
goto fail;
}
+ /* Make sure the sink always outputs frames with the exact amount of samples the audio encoder wants,
+ otherwise the audio encoder rejects the frame and that piece of audio is lost */
+ av_buffersink_set_frame_size(abuffersink_ctx, audio_codec_context->frame_size);
+
*graph = filter_graph;
*sink = abuffersink_ctx;
@@ -2622,6 +2634,41 @@ static void match_app_audio_input_to_available_apps(const std::vector<AudioInput
}
}
+struct AudioTrackDescription {
+ std::vector<std::string> devices;
+ std::vector<std::string> applications;
+ bool app_inverse = false;
+
+ std::string to_title() const {
+ std::string title;
+ if(!devices.empty()) {
+ title += "Devices: ";
+ for(size_t i = 0; i < devices.size(); ++i) {
+ if(i > 0)
+ title += ", ";
+ title += devices[i];
+ }
+ }
+
+ if(!applications.empty()) {
+ if(!title.empty())
+ title += ". ";
+
+ if(app_inverse)
+ title += "All applications except: ";
+ else
+ title += "Applications: ";
+
+ for(size_t i = 0; i < applications.size(); ++i) {
+ if(i > 0)
+ title += ", ";
+ title += applications[i];
+ }
+ }
+ return title;
+ }
+};
+
// Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio.
// Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED.
// OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU
@@ -2634,9 +2681,14 @@ static std::vector<MergedAudioInputs> parse_audio_inputs(const AudioDevices &aud
continue;
requested_audio_inputs.push_back(parse_audio_input_arg(audio_input));
+ AudioTrackDescription audio_track_description;
+
for(AudioInput &request_audio_input : requested_audio_inputs.back().audio_inputs) {
- if(request_audio_input.type != AudioInputType::DEVICE)
+ if(request_audio_input.type == AudioInputType::APPLICATION) {
+ audio_track_description.applications.push_back(request_audio_input.name);
+ audio_track_description.app_inverse = request_audio_input.inverted;
continue;
+ }
bool match = false;
@@ -2646,16 +2698,20 @@ static std::vector<MergedAudioInputs> parse_audio_inputs(const AudioDevices &aud
_exit(2);
}
match = true;
+ audio_track_description.devices.push_back("Default output");
} else if(request_audio_input.name == "default_input") {
if(audio_devices.default_input.empty()) {
fprintf(stderr, "gsr error: -a default_input was specified but no default audio input is specified in the audio server\n");
_exit(2);
}
match = true;
+ audio_track_description.devices.push_back("Default input");
} else {
- const bool name_is_existing_audio_device = get_audio_device_by_name(audio_devices.audio_inputs, request_audio_input.name.c_str()) != nullptr;
- if(name_is_existing_audio_device)
+ const AudioDevice *audio_device = get_audio_device_by_name(audio_devices.audio_inputs, request_audio_input.name.c_str());
+ if(audio_device) {
match = true;
+ audio_track_description.devices.push_back(audio_device->description);
+ }
}
if(!match) {
@@ -2670,6 +2726,8 @@ static std::vector<MergedAudioInputs> parse_audio_inputs(const AudioDevices &aud
_exit(50);
}
}
+
+ requested_audio_inputs.back().track_name = audio_track_description.to_title();
}
return requested_audio_inputs;
@@ -4142,9 +4200,13 @@ int main(int argc, char **argv) {
const double audio_fps = (double)audio_track.codec_context->sample_rate / (double)audio_track.codec_context->frame_size;
const int64_t timeout_ms = std::round(1000.0 / audio_fps);
const double timeout_sec = 1000.0 / audio_fps / 1000.0;
- bool first_frame = true;
int64_t num_received_frames = 0;
+ // The sound device is opened before the recording starts, so it can contain old audio from before the recording started.
+ // Discard it so the recording doesn't start with old audio.
+ if(audio_device.sound_device.handle)
+ sound_device_flush(&audio_device.sound_device);
+
while(running) {
void *sound_buffer;
int sound_buffer_size = -1;
@@ -4192,15 +4254,13 @@ int main(int argc, char **argv) {
// despite nvidia shadowplay and xbox game bar producing variable frame rate videos.
// So we have to make sure we produce frames at the same relative rate as the video.
if((num_missing_frames >= 1 && got_audio_data) || num_missing_frames >= 5 || !audio_device.sound_device.handle) {
- // TODO:
- //audio_track.frame->data[0] = empty_audio;
- if(first_frame || num_missing_frames >= 5) {
- if(needs_audio_conversion)
- swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&empty_audio, audio_track.codec_context->frame_size);
- else
- audio_device.frame->data[0] = empty_audio;
- }
- first_frame = false;
+ // Fill the missing frames with silence. Duplicating the previous audio frame to fill the gap instead
+ // sounds like a stutter and it's especially noticeable at the start of the recording when the audio device
+ // hasn't started to deliver audio at a stable rate yet, which repeats the first audio frame multiple times.
+ if(needs_audio_conversion)
+ swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&empty_audio, audio_track.codec_context->frame_size);
+ else
+ audio_device.frame->data[0] = empty_audio;
// TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again
std::lock_guard<std::mutex> lock(audio_filter_mutex);
@@ -4229,12 +4289,20 @@ int main(int argc, char **argv) {
if(!audio_device.sound_device.handle) {
av_usleep(timeout_ms * 1000);
} else if(got_audio_data) {
+ // The frame has to be made writable again if the frame was already sent to the audio filter above (when filling missing frames)
+ // because the audio filter only references the frame data instead of copying it. Without this the sent frames data would be
+ // overwritten with the audio data below, causing the audio to repeat instead of the missing frames being silent.
+ ret = av_frame_make_writable(audio_device.frame);
+ if (ret < 0) {
+ fprintf(stderr, "Failed to make audio frame writable\n");
+ break;
+ }
+
// TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format?
if(needs_audio_conversion)
swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&sound_buffer, audio_track.codec_context->frame_size);
else
audio_device.frame->data[0] = (uint8_t*)sound_buffer;
- first_frame = false;
std::lock_guard<std::mutex> lock(audio_filter_mutex);
@@ -4589,8 +4657,8 @@ int main(int argc, char **argv) {
}
if(save_replay_thread.valid() && save_replay_thread.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
- save_replay_thread.get();
- if(save_replay_output_filepath.empty()) {
+ const bool replay_save_result = save_replay_thread.get();
+ if(save_replay_output_filepath.empty() || !replay_save_result) {
printf("gsr error: Failed to save replay\n");
fflush(stdout);
} else {
@@ -4608,7 +4676,11 @@ 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, arg_parser, file_extension, arg_parser.date_folders, hdr, video_sources, current_save_replay_seconds);
+ const bool replay_start_result = save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, &encoder, arg_parser, file_extension, arg_parser.date_folders, hdr, video_sources, current_save_replay_seconds);
+ if(!replay_start_result) {
+ printf("gsr error: Failed to save replay\n");
+ fflush(stdout);
+ }
if(arg_parser.restart_replay_on_save && current_save_replay_seconds == save_replay_seconds_full) {
pthread_mutex_lock(&encoder.replay_mutex);
diff --git a/src/pipewire_video.c b/src/pipewire_video.c
index 30e00a8..360c8b5 100644
--- a/src/pipewire_video.c
+++ b/src/pipewire_video.c
@@ -114,32 +114,111 @@ static const struct pw_core_events core_events = {
.error = on_core_error_cb,
};
+static void gsr_pipewire_video_read_cursor_metadata(gsr_pipewire_video *self, struct spa_buffer *buffer) {
+ const struct spa_meta_cursor *cursor = spa_buffer_find_meta_data(buffer, SPA_META_Cursor, sizeof(*cursor));
+ if(!cursor)
+ return;
+
+ /* The cursor can become invalid when it moves off the captured window/monitor, in which case it should be hidden */
+ const bool cursor_valid = spa_meta_cursor_is_valid(cursor);
+ if(cursor_valid != self->cursor.valid) {
+ self->cursor.updated = true;
+ self->damaged = true;
+ }
+ self->cursor.valid = cursor_valid;
+
+ if(!self->cursor.visible || !self->cursor.valid)
+ return;
+
+ struct spa_meta_bitmap *bitmap = NULL;
+ if(cursor->bitmap_offset)
+ bitmap = SPA_MEMBER(cursor, cursor->bitmap_offset, struct spa_meta_bitmap);
+
+ // TODO: Maybe check if the cursor is actually visible by checking if there are visible pixels
+ if(bitmap && bitmap->size.width > 0 && bitmap->size.height > 0 && is_cursor_format_supported(bitmap->format)) {
+ /* Animated cursors update the bitmap for every animation frame, only log when the size changes */
+ if((int)bitmap->size.width != self->cursor.width || (int)bitmap->size.height != self->cursor.height) {
+ fprintf(stderr, "gsr info: pipewire: cursor bitmap update, size: %dx%d, format: %s\n",
+ (int)bitmap->size.width, (int)bitmap->size.height, spa_debug_type_find_name(spa_type_video_format, bitmap->format));
+ }
+
+ const uint8_t *bitmap_data = SPA_MEMBER(bitmap, bitmap->offset, uint8_t);
+ const size_t bitmap_size = bitmap->size.width * bitmap->size.height * 4;
+ uint8_t *new_bitmap_data = realloc(self->cursor.data, bitmap_size);
+ if(new_bitmap_data) {
+ self->cursor.data = new_bitmap_data;
+ /* TODO: Convert bgr and other image formats to rgb here */
+ memcpy(self->cursor.data, bitmap_data, bitmap_size);
+
+ self->cursor.hotspot_x = cursor->hotspot.x;
+ self->cursor.hotspot_y = cursor->hotspot.y;
+ self->cursor.width = bitmap->size.width;
+ self->cursor.height = bitmap->size.height;
+ self->cursor.updated = true;
+ self->damaged = true;
+ }
+ }
+
+ /* Position changes for a cursor that cant be drawn (no bitmap received yet) shouldn't trigger a redraw */
+ if(self->cursor.width > 0 && (cursor->position.x != self->cursor.x || cursor->position.y != self->cursor.y)) {
+ self->cursor.updated = true;
+ self->damaged = true;
+ }
+
+ self->cursor.x = cursor->position.x;
+ self->cursor.y = cursor->position.y;
+
+ //fprintf(stderr, "gsr info: pipewire: cursor: %d %d %d %d\n", cursor->hotspot.x, cursor->hotspot.y, cursor->position.x, cursor->position.y);
+}
+
+static bool buffer_has_video_content(const struct spa_buffer *buffer) {
+ /* Cursor-only updates in cursor metadata mode are sent as buffers without valid video content
+ (chunk size 0 on gnome, chunk marked as corrupted on kde plasma) */
+ return buffer->n_datas > 0 && buffer->datas[0].chunk->size != 0 && !(buffer->datas[0].chunk->flags & SPA_CHUNK_FLAG_CORRUPTED);
+}
+
static void on_process_cb(void *user_data) {
gsr_pipewire_video *self = user_data;
- /* Find the most recent buffer */
+ /* Find the most recent buffer with video content. The cursor metadata is read from all buffers in the order they arrive
+ because a batch of buffers can contain both video buffers and cursor-only buffers (in cursor metadata mode)
+ and a video buffer shouldn't be discarded just because a cursor-only buffer arrived after it. */
+ bool got_buffer = false;
struct pw_buffer *pw_buf = NULL;
for(;;) {
struct pw_buffer *aux = pw_stream_dequeue_buffer(self->stream);
if(!aux)
break;
- if(pw_buf)
- pw_stream_queue_buffer(self->stream, pw_buf);
- pw_buf = aux;
+
+ got_buffer = true;
+ pthread_mutex_lock(&self->mutex);
+ gsr_pipewire_video_read_cursor_metadata(self, aux->buffer);
+ pthread_mutex_unlock(&self->mutex);
+
+ if(buffer_has_video_content(aux->buffer)) {
+ if(pw_buf)
+ pw_stream_queue_buffer(self->stream, pw_buf);
+ pw_buf = aux;
+ } else {
+ pw_stream_queue_buffer(self->stream, aux);
+ }
}
- if(!pw_buf) {
+ if(!got_buffer) {
fprintf(stderr, "gsr info: pipewire: out of buffers!\n");
return;
}
+ /* Only cursor-only buffers arrived, theres no new video frame to process */
+ if(!pw_buf)
+ return;
+
struct spa_buffer *buffer = pw_buf->buffer;
- const bool has_buffer = buffer->n_datas > 0 && buffer->datas[0].chunk->size != 0;
pthread_mutex_lock(&self->mutex);
bool buffer_updated = false;
- if(has_buffer && buffer->datas[0].type == SPA_DATA_DmaBuf) {
+ if(buffer->datas[0].type == SPA_DATA_DmaBuf) {
for(size_t i = 0; i < self->dmabuf_num_planes; ++i) {
if(self->dmabuf_data[i].fd > 0) {
close(self->dmabuf_data[i].fd);
@@ -210,44 +289,6 @@ static void on_process_cb(void *user_data) {
self->damaged = true;
}
- const struct spa_meta_cursor *cursor = spa_buffer_find_meta_data(buffer, SPA_META_Cursor, sizeof(*cursor));
- self->cursor.valid = cursor && spa_meta_cursor_is_valid(cursor);
-
- if (self->cursor.visible && self->cursor.valid) {
- struct spa_meta_bitmap *bitmap = NULL;
- if (cursor->bitmap_offset)
- bitmap = SPA_MEMBER(cursor, cursor->bitmap_offset, struct spa_meta_bitmap);
-
- // TODO: Maybe check if the cursor is actually visible by checking if there are visible pixels
- if (bitmap && bitmap->size.width > 0 && bitmap->size.height > 0 && is_cursor_format_supported(bitmap->format)) {
- const uint8_t *bitmap_data = SPA_MEMBER(bitmap, bitmap->offset, uint8_t);
- fprintf(stderr, "gsr info: pipewire: cursor bitmap update, size: %dx%d, format: %s\n",
- (int)bitmap->size.width, (int)bitmap->size.height, spa_debug_type_find_name(spa_type_video_format, bitmap->format));
-
- const size_t bitmap_size = bitmap->size.width * bitmap->size.height * 4;
- uint8_t *new_bitmap_data = realloc(self->cursor.data, bitmap_size);
- if(new_bitmap_data) {
- self->cursor.data = new_bitmap_data;
- /* TODO: Convert bgr and other image formats to rgb here */
- memcpy(self->cursor.data, bitmap_data, bitmap_size);
- }
-
- self->cursor.hotspot_x = cursor->hotspot.x;
- self->cursor.hotspot_y = cursor->hotspot.y;
- self->cursor.width = bitmap->size.width;
- self->cursor.height = bitmap->size.height;
- self->damaged = true;
- }
-
- if(cursor->position.x != self->cursor.x || cursor->position.y != self->cursor.y)
- self->damaged = true;
-
- self->cursor.x = cursor->position.x;
- self->cursor.y = cursor->position.y;
-
- //fprintf(stderr, "gsr info: pipewire: cursor: %d %d %d %d\n", cursor->hotspot.x, cursor->hotspot.y, cursor->position.x, cursor->position.y);
- }
-
pthread_mutex_unlock(&self->mutex);
pw_stream_queue_buffer(self->stream, pw_buf);
}
@@ -840,15 +881,26 @@ static void gsr_pipewire_video_update_cursor_texture(gsr_pipewire_video *self, g
self->cursor.data = NULL;
}
-bool gsr_pipewire_video_map_texture(gsr_pipewire_video *self, gsr_texture_map texture_map, gsr_map_texture_output *output) {
- for(int i = 0; i < GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES; ++i) {
- memset(&output->dmabuf_data[i], 0, sizeof(gsr_pipewire_video_dmabuf_data));
+static void gsr_pipewire_video_update_cursor_data(gsr_pipewire_video *self, gsr_texture_map texture_map, gsr_pipewire_video_region *cursor_region) {
+ self->cursor.updated = false;
+ gsr_pipewire_video_update_cursor_texture(self, texture_map);
+
+ if(self->cursor.valid) {
+ cursor_region->x = self->cursor.x - self->cursor.hotspot_x;
+ cursor_region->y = self->cursor.y - self->cursor.hotspot_y;
+
+ cursor_region->width = self->cursor.width;
+ cursor_region->height = self->cursor.height;
+ } else {
+ cursor_region->x = 0;
+ cursor_region->y = 0;
+
+ cursor_region->width = 0;
+ cursor_region->height = 0;
}
- output->num_dmabuf_data = 0;
- output->using_external_image = self->external_texture_fallback;
- output->fourcc = 0;
- output->modifiers = 0;
- output->rotation = GSR_MONITOR_ROT_0;
+}
+
+bool gsr_pipewire_video_map_texture(gsr_pipewire_video *self, gsr_texture_map texture_map, gsr_map_texture_output *output) {
pthread_mutex_lock(&self->mutex);
if(!self->negotiated || !self->streaming || self->dmabuf_data[0].fd <= 0) {
@@ -862,12 +914,16 @@ bool gsr_pipewire_video_map_texture(gsr_pipewire_video *self, gsr_texture_map te
return false;
}
+ /* |output| is only written to from this point on. When this function returns false the caller
+ can keep using the data from the previous successful call to redraw the previous video frame. */
+ for(int i = 0; i < GSR_PIPEWIRE_VIDEO_DMABUF_MAX_PLANES; ++i) {
+ memset(&output->dmabuf_data[i], 0, sizeof(gsr_pipewire_video_dmabuf_data));
+ }
+
gsr_pipewire_video_bind_image_to_texture_with_fallback(self, texture_map, image);
output->using_external_image = self->external_texture_fallback;
self->egl->eglDestroyImage(self->egl->egl_display, image);
- gsr_pipewire_video_update_cursor_texture(self, texture_map);
-
output->texture_width = self->format.info.raw.size.width;
output->texture_height = self->format.info.raw.size.height;
@@ -892,12 +948,7 @@ bool gsr_pipewire_video_map_texture(gsr_pipewire_video *self, gsr_texture_map te
output->region.height = temp;
}
- /* TODO: Test if cursor hotspot is correct */
- output->cursor_region.x = self->cursor.x - self->cursor.hotspot_x;
- output->cursor_region.y = self->cursor.y - self->cursor.hotspot_y;
-
- output->cursor_region.width = self->cursor.width;
- output->cursor_region.height = self->cursor.height;
+ gsr_pipewire_video_update_cursor_data(self, texture_map, &output->cursor_region);
for(size_t i = 0; i < self->dmabuf_num_planes; ++i) {
output->dmabuf_data[i] = self->dmabuf_data[i];
@@ -913,6 +964,20 @@ bool gsr_pipewire_video_map_texture(gsr_pipewire_video *self, gsr_texture_map te
return true;
}
+bool gsr_pipewire_video_update_cursor(gsr_pipewire_video *self, gsr_texture_map texture_map, gsr_pipewire_video_region *cursor_region) {
+ pthread_mutex_lock(&self->mutex);
+
+ if(!self->negotiated || !self->streaming || !self->cursor.updated) {
+ pthread_mutex_unlock(&self->mutex);
+ return false;
+ }
+
+ gsr_pipewire_video_update_cursor_data(self, texture_map, cursor_region);
+
+ pthread_mutex_unlock(&self->mutex);
+ return true;
+}
+
bool gsr_pipewire_video_is_damaged(gsr_pipewire_video *self) {
if(!self->mutex_initialized)
return false;
diff --git a/src/sound.cpp b/src/sound.cpp
index eedfbf9..4e04d8f 100644
--- a/src/sound.cpp
+++ b/src/sound.cpp
@@ -520,6 +520,26 @@ void sound_device_close(SoundDevice *device) {
device->handle = NULL;
}
+void sound_device_flush(SoundDevice *device) {
+ pa_handle *p = (pa_handle*)device->handle;
+ if(!p || !p->stream)
+ return;
+
+ if(pa_stream_get_state(p->stream) != PA_STREAM_READY)
+ return;
+
+ pa_operation *op = pa_stream_flush(p->stream, NULL, NULL);
+ if(!op)
+ return;
+
+ /* The flush operation should finish immediately, the timeout is to not freeze if pulseaudio is in a bad state */
+ const double start_time = clock_get_monotonic_seconds();
+ while(pa_operation_get_state(op) == PA_OPERATION_RUNNING && clock_get_monotonic_seconds() - start_time < 0.1) {
+ pa_mainloop_iterate(p->mainloop, 1, NULL);
+ }
+ pa_operation_unref(op);
+}
+
int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds) {
pa_handle *pa = (pa_handle*)device->handle;
if(pa_sound_device_read(pa, timeout_sec) < 0) {