diff options
-rw-r--r-- | README.md | 77 | ||||
-rw-r--r-- | TODO | 30 | ||||
-rw-r--r-- | com.dec05eba.gpu_screen_recorder.appdata.xml | 522 | ||||
-rw-r--r-- | com.dec05eba.gpu_screen_recorder.desktop (renamed from gpu-screen-recorder-gtk.desktop) | 0 | ||||
-rwxr-xr-x | install.sh | 3 | ||||
-rw-r--r-- | meson.build | 32 | ||||
-rw-r--r-- | project.conf | 9 | ||||
-rw-r--r-- | src/config.hpp | 66 | ||||
-rw-r--r-- | src/egl.c | 380 | ||||
-rw-r--r-- | src/egl.h | 114 | ||||
-rw-r--r-- | src/global_shortcuts.c | 336 | ||||
-rw-r--r-- | src/global_shortcuts.h | 41 | ||||
-rw-r--r-- | src/library_loader.c | 34 | ||||
-rw-r--r-- | src/library_loader.h | 17 | ||||
-rw-r--r-- | src/main.cpp | 3641 |
15 files changed, 2748 insertions, 2554 deletions
@@ -1,44 +1,39 @@  -gtk frontend for [gpu-screen-recorder](https://git.dec05eba.com/gpu-screen-recorder/about/). - -This is a screen recorder that has minimal impact on system performance by recording your monitor using the GPU only, -similar to shadowplay on windows. This is the fastest screen recording tool for Linux. - -This screen recorder can be used for recording your desktop offline, for live streaming and for nvidia shadowplay-like instant replay, -where only the last few minutes are saved. - -More info at [gpu-screen-recorder](https://git.dec05eba.com/gpu-screen-recorder/about/). - -## Note -This software works with x11 and wayland, but when using wayland only monitors can be recorded. Hotkeys are also not supported on wayland (wayland doesn't really support this properly yet). Use X11 if you want a proper desktop experience in general. -### TEMPORARY ISSUES -1) Videos are in variable framerate format. Use MPV to play such videos, otherwise you might experience stuttering in the video if you are using a buggy video player. You can try saving the video into a .mkv file instead as some software may have better support for .mkv files (such as kdenlive). You can use the "-fm cfr" option to to use constant framerate mode. -### AMD/Intel/Wayland root permission -When recording a window under AMD/Intel no special user permission is required, however when recording a monitor (or when using wayland) the program needs root permission (to access KMS).\ -To make this safer, the part that needs root access has been moved to its own executable (to make it as small as possible).\ -For you as a user this only means that if you installed GPU Screen Recorder as a flatpak then a prompt asking for root password will show up when you start recording. -# Performance -On a system with a i5 4690k CPU and a GTX 1080 GPU:\ -When recording Legend of Zelda Breath of the Wild at 4k, fps drops from 30 to 7 when using OBS Studio + nvenc, however when using this screen recorder the fps remains at 30.\ -When recording GTA V at 4k on highest settings, fps drops from 60 to 23 when using obs-nvfbc + nvenc, however when using this screen recorder the fps only drops to 58. The quality is also much better when using gpu-screen-recorder.\ -It is recommended to save the video to a SSD because of the large file size, which a slow HDD might not be fast enough to handle. -## Note about optimal performance on NVIDIA -NVIDIA driver has a "feature" (read: bug) where it will downclock memory transfer rate when a program uses cuda (or nvenc, which uses cuda), such as GPU Screen Recorder. See https://git.dec05eba.com/gpu-screen-recorder/about/ for more information and how to overcome this. -## Hotkey -Hotkeys are currently only supported on X11. Most Wayland compositors are missing a way to bind hotkeys programatically. If you want to have hotkeys then you can bind hotkeys in your Wayland compositors settings. -Bind a key to `killall -SIGINT gpu-screen-recorder` to stop recording (also saves the video when recording a regular video). Bind another key to `killall -SIGUSR1 gpu-screen-recorder` to save a replay and another key to `killall -SIGUSR2 gpu-screen-recorder` to pause/unpause the recording (when recording a regular video). - -## Installation -This program depends on [gpu-screen-recorder](https://git.dec05eba.com/gpu-screen-recorder/) which needs to be installed first.\ -Run `sudo ./install.sh` or if you are running Arch Linux, then you can find gpu screen recorder gtk on aur under the name gpu-screen-recorder-gtk-git (`yay -S gpu-screen-recorder-gtk-git`).\ -Dependencies needed when building using `build.sh` or `install.sh`: `gtk3 libx11 libxrandr libpulse libdrm wayland-client ayatana-appindicator3-0.1`.\ -You can also install gpu screen recorder (the gtk gui version) from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes gpu-screen-recorder so no need to install that first. - -## Screenshots - +# GPU Screen Recorder GTK +GTK frontend for [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/about/).\ +There is a new alternative UI for GPU Screen Recorder in the style of ShadowPlay available here: [GPU Screen Recorder UI](https://git.dec05eba.com/gpu-screen-recorder-ui/). + +## Notes +The program has to be launched from your application launcher or hotkeys may not work properly in your Wayland compositor (this is the case with GNOME). + +## Deprecation +This project is no longer being developed as it has been superseded by [GPU Screen Recorder UI](https://git.dec05eba.com/gpu-screen-recorder-ui/) which has more features. This project will remain available until GPU Screen Recorder UI can run as a regular window, just like GPU Screen Recorder GTK does.\ +The `com.dec05eba.gpu_screen_recorder.appdata.xml` file has been moved to the [AppData](https://git.dec05eba.com/gpu-screen-recorder-appdata/) repository. + +# Installation +If you are using an Arch Linux based distro then you can find gpu screen recorder gtk on aur under the name gpu-screen-recorder-gtk (`yay -S gpu-screen-recorder-gtk`).\ +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 from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI. + +# Dependencies +GPU Screen Recorder GTK uses meson build system so you need to install `meson` to build GPU Screen Recorder GTK. + +## Build dependencies +These are the dependencies needed to build GPU Screen Recorder GTK: -# Donations -If you want to donate you can donate via bitcoin or monero. -* Bitcoin: bc1qqvuqnwrdyppf707ge27fqz2n9y9gu7lf5ypyuf -* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet +* gtk3 +* libx11 +* ayatana-appindicator3-0.1 +* desktop-file-utils + +## Runtime dependencies +There are also additional dependencies needed at runtime: + +* [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/) + +# Reporting bugs, contributing patches, questions or donation +See [https://git.dec05eba.com/?p=about](https://git.dec05eba.com/?p=about). + +# Screenshots + @@ -3,23 +3,33 @@ Make sure the resolution is allowed for streaming. Add list of windows to select from. This makes it easier to select another window that is not in the view to be clickable. Disable overclocking and show some kind of sign that overclocking is not possible (if coolbits not set). Button (in the field) to remove hotkey. -Implement global hotkeys on wayland. -Add pipewire capture option. Error if polkit agent is not running (pkexec --help may work but the agent might not be running anyways). -Fix gui crash on stop recording on sway. Re-enable wlroots based capture when it's fixed. Add translation support (using gettext, which uses .mo files and _ macro). It should be easy for non-devs to contribute. But how? maybe have a page that links to the english translation files (or translation directory) in git and give instructions, telling them to copy the file content and then send me the file by email with the language name (then I can add it to the correct) file with correct locale. Or if they know how to use git they can use that directly. -Add option to start replay on system startup (use the gpu screen recorder systemd file?) and use the settings from the replay page. - Dont add this option if not running systemd, or disable the button then. - Fix screen capture breaking after suspend/monitor change on nvidia x11. - Add note about replaying can get bugged on suspend unless nvidia reg for suspend is added (because cuda breaks, nvidia driver bug). - Detect suspend before it happens and unload cuda/nvenc then and reload after suspend. This is a workaround to nvidia driver bug that breaks cuda on suspend. - Disable the gpu screen recorders aur/source package systemd service if enabled in the gui since these are two different services. Or make that service and gui service the same and work with the same (gui) config file. Remove the need to install gpu screen recorder flatpak as system. This can now be done because of kms-server-proxy. Implement profiles to quickly switch between settings. Use https://hosted.weblate.org/ for translation. -Detect game name by using x11 window class or title. Fallback to finding pressure vessel, find the binary is runs and get the directory name directly under the proton game list directory. Fallback to pure wine. +Have separate options for each record option (stream, record and replay) or have option to use profiles. Remake the gui and have a proper overlay! on wlroots and kde use https://wayland.app/protocols/wlr-layer-shell-unstable-v1. + +A single flatpak can only be installed either system-wide or user, so there can be a check if it's installed system-wide or user and it will only match one. With this information we can guaranteed know the flatpak directory of the running gpu screen recorder instance. The command `flatpak info -l com.dec05eba.gpu_screen_recorder` can also be used and is available for all flatpak users. + +Re-renable hotkeys on hyprland after it's fixed in the hyprland desktop portal. Or use hyprland specific protocol to do it ourselves, and it also works better. + +Notifications are not shown on kde plasma while using desktop portal capture (system-wide). This is a design choice by kde, see https://invent.kde.org/plasma/xdg-desktop-portal-kde/-/blob/master/src/screencast.cpp?ref_type=heads#L41 . The DoNotDisturb.WhenScreenSharing config controls this. This can be bypassed by making the notification critical. Maybe notifications should be set as critical? but only on kde. + Maybe we should create our own notification system with gtk layer shell (which is supported by every platform except gnome wayland). + On gnome wayland maybe we can fallback to x11? we need to add back --socket=x11 and remove --socket=fallback-x11 from flatpak manifest. + Maybe use this: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/264 when that gets supported. But will gnome support that? + Maybe map the drm framebuffer to opengl with write permission and draw to that opengl texture with a framebuffer. That would allow us to draw on the screen anywhere on any wayland compositor (and x11 amd/intel). We can also do this after the video frame has been captured to not include it in the video. + Maybe use drm overlay plane, if possible. + +Start recording after showing start recording notification has disappeared, not at the same time. + +Add profile option. Convert view to profile, add an option at the bottom that says "Edit profiles..." which should show a popup where you can create/remove profiles. New profiles should always be in advanced view. + +Move x11 hotkey code to its own file. + +Detect gpu screen recorder flatpak update and restart the gsr-ui systemd service to apply the update. Or show a notification when it has been updated and can be restarted to apply update.
\ No newline at end of file diff --git a/com.dec05eba.gpu_screen_recorder.appdata.xml b/com.dec05eba.gpu_screen_recorder.appdata.xml deleted file mode 100644 index 7d05e52..0000000 --- a/com.dec05eba.gpu_screen_recorder.appdata.xml +++ /dev/null @@ -1,522 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<component type="desktop-application"> - <id>com.dec05eba.gpu_screen_recorder</id> - <name>GPU Screen Recorder</name> - <summary>A shadowplay-like screen recorder for Linux. The fastest screen recorder for Linux</summary> - <developer_name>dec05eba</developer_name> - <metadata_license>CC0-1.0</metadata_license> - <project_license>GPL-3.0</project_license> - <url type="homepage">https://git.dec05eba.com/gpu-screen-recorder/about/</url> - <url type="bugtracker">https://github.com/dec05eba/gpu-screen-recorder-issues/issues</url> - <url type="contribute">https://git.dec05eba.com/?p=about</url> - <url type="donation">https://git.dec05eba.com/?p=about</url> - - <supports> - <control>pointing</control> - <control>keyboard</control> - </supports> - - <description> - <p> - This is a screen recorder that has minimal impact on system performance by recording a monitor using the GPU only, similar to shadowplay on windows. This is the fastest screen recording tool for Linux. This screen recorder works with both X11 and Wayland. - </p> - <p> - This screen recorder can be used for recording your desktop offline, for live streaming and for nvidia-like instant replay, where only the last few minutes are saved. - </p> - <p>Supported video codecs:</p> - <ul> - <li>H264 (default)</li> - <li>HEVC</li> - <li>AV1 (not currently supported on NVIDIA in the flatpak)</li> - </ul> - <p>Supported audio codecs:</p> - <ul> - <li>Opus (default)</li> - <li>AAC</li> - </ul> - <p> - Recording a monitor requires (restricted) root access which means that you have to install GPU Screen Recorder system-wide: flatpak install flathub --system com.dec05eba.gpu_screen_recorder - and pkexec needs to be installed on the system and a polkit agent needs to be running. - </p> - <p>Recording a single window is only possible on X11. Hotkeys are not supported on wayland either (wayland doesn't really support this). Use X11 if you want a proper desktop experience in general.</p> - <p>AV1 is currently not supported in the flatpak for Nvidia since GPU Screen Recorder uses an older ffmpeg version to support older Nvidia cards. Install GPU Screen Recorder from source or from AUR if you want to use AV1 on Nvidia.</p> - <p>On some Intel integrated GPUs the video can appear glitched when recording on Wayland. The only known workaround at the moment is to record on X11.</p> - <p> - Videos are in variable framerate format. Very out of date video players might have an issue playing such videos. It's recommend to use MPV to play such videos, otherwise you might experience stuttering in the video. - You can select constant frame rate mode in advanced view if you need it. - </p> - <p> - If the video doesn't play or you get green/yellow overlay then your video player is missing H264/HEVC video codec. Either install the video codecs or use mpv. - </p> - <p> - If the video is glitched with checkerboard pattern and you are using and Intel integrated GPU on wayland then this is a known issue and right now the only solution is to record on X11 instead. - </p> - <p>AMD has a driver/hardware fault that causes black bars/distorted colors on the right side/bottom of the video for certain video resolutions. This happens for both av1 and hevc, so if you have this issue then switch to h264 video codec option in advanced settings. - </p> - <p> - If H264/HEVC video encoding option is not available on your AMD/Intel system but you know that your supports those codecs then you may need to install mesa-extra freedesktop runtime by running this command: "flatpak install --system org.freedesktop.Platform.GL.default//23.08-extra". - </p> - <p> - On NVIDIA systems the flatpak NVIDIA driver needs to match the systems NVIDIA driver version or the NVIDIA driver will fail to properly load in flatpaks. - Make sure you update flatpak on your system with: "flatpak update", or if that doesn't fix it you can try to install the specific flatpak NVIDIA driver version version that matches your systems NVIDIA driver version by running: - "flatpak install org.freedesktop.Platform.GL.nvidia-version", for example "flatpak install org.freedesktop.Platform.GL.nvidia-550-54-14". - You can find which NVIDIA driver version is running on your system by running "cat /proc/driver/nvidia/version". - </p> - <p>GPU Screen Recorder flatpak can install files in $HOME/.local/share/gpu-screen-recorder. If you want to uninstall GPU Screen Recorder then you will have to remove this directory manually.</p> - </description> - - <launchable type="desktop-id">com.dec05eba.gpu_screen_recorder.desktop</launchable> - <screenshots> - <screenshot type="default"> - <caption>Simple view</caption> - <image>https://raw.githubusercontent.com/dec05eba/com.dec05eba.gpu_screen_recorder/master/resources/screenshot1.png</image> - </screenshot> - <screenshot> - <caption>Advanced view</caption> - <image>https://raw.githubusercontent.com/dec05eba/com.dec05eba.gpu_screen_recorder/master/resources/screenshot2.png</image> - </screenshot> - <screenshot> - <caption>Recording page</caption> - <image>https://raw.githubusercontent.com/dec05eba/com.dec05eba.gpu_screen_recorder/master/resources/screenshot3.png</image> - </screenshot> - </screenshots> - - <releases> - <release version="3.8.2" date="2024-06-22"> - <description> - <ul> - <li>Default to h264 video codec because of amd driver issues (black bar) and for better compatibility with software</li> - </ul> - </description> - </release> - <release version="3.8.1" date="2024-06-16"> - <description> - <ul> - <li>Properly cutout cursor outside video area when dealing with AMD HEVC padding driver bug</li> - </ul> - </description> - </release> - <release version="3.8.0" date="2024-06-10"> - <description> - <ul> - <li>Add system tray icon</li> - <li>Fix screen recording on Intel ARC GPU</li> - <li>Workaround AMD driver bug with HEVC video codec that causes glitched graphics on the right/bottom side with certain video resolutions. It's now replaced with black color</li> - <li>Fix possible incorrect monitor rotation when using multiple monitors with one monitor rotated on GNOME Wayland</li> - <li>Add hls (m3u8) container option</li> - </ul> - </description> - </release> - <release version="3.7.7" date="2024-05-29"> - <description> - <ul> - <li>Fix recording freeze on nvidia when __GL_SYNC_TO_VBLANK environment variable is set to 1</li> - </ul> - </description> - </release> - <release version="3.7.6" date="2024-05-20"> - <description> - <ul> - <li>Do not force constant framerate for live streams</li> - </ul> - </description> - </release> - <release version="3.7.5" date="2024-05-19"> - <description> - <ul> - <li>Fix twitch/youtube streaming not working</li> - <li>Fix possible freeze on recording stop if stopping quickly after starting recording</li> - </ul> - </description> - </release> - <release version="3.7.4" date="2024-05-13"> - <description> - <p>Make audio sync even better. Audio sync is broken in all applications anyways so what can be done really</p> - </description> - </release> - <release version="3.7.3" date="2024-05-12"> - <description> - <ul> - <li>Re-enable opus audio codec</li> - <li>Remove flac audio codec option until it's fixed</li> - <li>Improve video quality when recording HDR</li> - <li>Fix flv issues</li> - <li>Add mpegts container and option to select codec for custom streaming service</li> - </ul> - </description> - </release> - <release version="3.7.2" date="2024-04-20"> - <description> - <ul> - <li>Improve nvidia video encoding performance a lot on certain GPUs</li> - <li>Improve audio/video sync</li> - <li>Increase audio bitrate</li> - <li>Hide notifications after a few seconds</li> - </ul> - </description> - </release> - <release version="3.7.1" date="2024-04-12"> - <description> - <p>Fix audio sync regression when using mixed audio</p> - </description> - </release> - <release version="3.7.0" date="2024-04-11"> - <description> - <p>Fix possible audio desync after some time for some users</p> - </description> - </release> - <release version="3.6.5" date="2024-04-10"> - <description> - <p>Fix nvidia x11 monitor capture breaking after suspend/monitor reconfiguration (even after applying nvidia preserve video memory on suspend)</p> - </description> - </release> - <release version="3.6.4" date="2024-04-06"> - <description> - <p>Fix recording breaking when updating flatpak on some systems (where $HOME is symlinked to another location), requires password prompt update</p> - </description> - </release> - <release version="3.6.3" date="2024-03-21"> - <description> - <ul> - <li>Fix nvidia window capture/wayland capture if the user has set __GL_THREADED_OPTIMIZATIONS to 1</li> - <li>Fix full color range capture in nvidia window capture</li> - <li>Properly clear cursor in window capture</li> - </ul> - </description> - </release> - <release version="3.6.2" date="2024-03-20"> - <description> - <ul> - <li>Fix incorrect overclocking offset on some nvidia gpus when enabling overclocking option</li> - <li>Ignore color standards for more accurate colors</li> - </ul> - </description> - </release> - <release version="3.6.1" date="2024-03-11"> - <description> - <p>Add the option to not record cursor</p> - </description> - </release> - <release version="3.6.0" date="2024-03-10"> - <description> - <ul> - <li>Support HDR capture and full color range on nvidia</li> - <li>Support cursor capture when recording a single window</li> - <li>Show the correct monitors when using prime-run</li> - </ul> - </description> - </release> - <release version="3.5.3" date="2024-02-16"> - <description> - <p>Fix minor permissions issue</p> - </description> - </release> - <release version="3.5.2" date="2024-02-14"> - <description> - <p>Better error message for missing h264/hevc, ignore user nvidia vaapi setting</p> - </description> - </release> - <release version="3.5.1" date="2024-02-11"> - <description> - <ul> - <li>Fix crash on x11 nvidia when recording monitor</li> - <li>Fix color issue when recording window on amd/intel</li> - </ul> - </description> - </release> - <release version="3.5.0" date="2024-02-11"> - <description> - <ul> - <li>Support monitor rotation</li> - <li>Support hdr capture on amd/intel wayland (currently missing hdr metadata because of driver bugs)</li> - <li>Default to variable framerate videos on nvidia x11 as well</li> - <li>Center capture window/follow focused and clear background</li> - <li>Remove 'remove password prompts' button. Only ask for password once and never again</li> - </ul> - </description> - </release> - <release version="3.4.2" date="2024-01-27"> - <description> - <ul> - <li>Support hardware cursor plane capture on nvidia wayland</li> - <li>Add record timer</li> - </ul> - </description> - </release> - <release version="3.4.1" date="2024-01-17"> - <description> - <ul> - <li>Improve audio drifting in some cases</li> - <li>Fix audio stuttering with pulseaudio + opus/flac + multiple audio sources merged</li> - <li>Add option to remove hotkey with backspace</li> - <li>Decide which audio device is default output/input when starting to record</li> - </ul> - </description> - </release> - <release version="3.4.0" date="2024-01-07"> - <description> - <p>Add option to pause recording</p> - </description> - </release> - <release version="3.3.2" date="2023-12-31"> - <description> - <p>Fix monitor capture on AMD/Intel or NVIDIA Wayland on some distros, such as OpenSUSE</p> - </description> - </release> - <release version="3.3.1" date="2023-12-02"> - <description> - <ul> - <li>Fix opus and flac</li> - <li>Fix crash when live streaming without an audio source</li> - <li>Fix missing streaming show/hide key symbol</li> - </ul> - </description> - </release> - <release version="3.3.0" date="2023-12-01"> - <description> - <ul> - <li>Add option to remove password prompts (if possible on the system)</li> - <li>Add experimental av1 support (if supported by the hardware). Currently only enabled for AMD/Intel in the flatpak.</li> - <li>Configure quality settings to reduce file size on amd/intel</li> - <li>Workaround amd/intel driver issue when using h264 (or fps > 60) and mkv (forcefully set video codec to hevc)</li> - </ul> - </description> - </release> - <release version="3.2.6" date="2023-11-27"> - <description> - <p>Fix possible crash when recording multiple audio sources (merged)</p> - </description> - </release> - <release version="3.2.5" date="2023-11-18"> - <description> - <p>Fix incorrect color on some nvidia wayland systems running kde plasma (support 10-bit color depth)</p> - </description> - </release> - <release version="3.2.4" date="2023-11-05"> - <description> - <p>Nvidia x11: disable screen direct record option, as there is an nvidia driver bug that causes some games to freeze/crash with this option</p> - </description> - </release> - <release version="3.2.3" date="2023-11-01"> - <description> - <p>Use opengl context instead of egl2, might fix program startup not working for some users</p> - </description> - </release> - <release version="3.2.2" date="2023-10-31"> - <description> - <p>Intel: attempt to fix glitched graphics on some intel gpus. I dont know, ask intel why they are weird</p> - </description> - </release> - <release version="3.2.1" date="2023-10-22"> - <description> - <p>AMD/Intel wayland: fix broken colors when system uses 10-bit</p> - </description> - </release> - <release version="3.2.0" date="2023-10-21"> - <description> - <ul> - <li>AMD/Intel: workaround driver bug that causes vram leak</li> - <li>Add frame rate mode option in advanced menu</li> - <li>Redesign audio input to make it clear that you have to add audio to use it</li> - </ul> - </description> - </release> - <release version="3.1.7" date="2023-08-15"> - <description> - <p>NVIDIA: Workaround new nvidia driver bug that causes recording of a small window to freeze recording.</p> - </description> - </release> - <release version="3.1.6" date="2023-08-13"> - <description> - <p>AMD/Intel: fix capture issue on some systems</p> - </description> - </release> - <release version="3.1.5" date="2023-07-25"> - <description> - <p>AMD/Intel: Fix too dark video in the flatpak version.</p> - </description> - </release> - <release version="3.1.4" date="2023-07-23"> - <description> - <p>AMD/Intel: Better color accuracy.</p> - </description> - </release> - <release version="3.1.3" date="2023-07-22"> - <description> - <p>Fix capture on wlroots based wayland compositors. Fix cursor offset in amd/intel capture.</p> - </description> - </release> - <release version="3.1.2" date="2023-07-22"> - <description> - <p>Support cursor capture on AMD/Intel on Wayland. Attempt to fix multi monitor offset capture in some cases.</p> - </description> - </release> - <release version="3.1.1" date="2023-07-21"> - <description> - <p>Support cursor capture on AMD/Intel on Wayland.</p> - </description> - </release> - <release version="3.1.0" date="2023-07-21"> - <description> - <ul> - <li>Support wlroots capture when possible (no kms root access required)</li> - <li>Make window capture a bit more robust</li> - <li>Fix possible lag on recording start on AMD/Intel</li> - <li>Make capture on AMD/Intel possible without Xwayland</li> - </ul> - </description> - </release> - <release version="3.0.0" date="2023-07-18"> - <description> - <p>Experimental wayland support on AMD/Intel/NVIDIA. Hotkeys not supported.</p> - </description> - </release> - <release version="2.2.0" date="2023-07-10"> - <description> - <p>Attempt to fix screen recording when multiple graphics cards are connected</p> - </description> - </release> - <release version="2.1.7" date="2023-07-07"> - <description> - <p>Show proper error when running on wayland. Preparing for wayland support.</p> - </description> - </release> - <release version="2.1.6" date="2023-06-10"> - <description> - <p>AMD/Intel: fix multi monitor capture coordinate being incorrect for some GPU driver versions.</p> - </description> - </release> - <release version="2.1.5" date="2023-05-22"> - <description> - <p>Attempt to fix a recent nvidia (cuda) driver bug on RTX cards that causes a freeze when stopping recording.</p> - </description> - </release> - <release version="2.1.4" date="2023-05-17"> - <description> - <p>Fix issue where the application freezes and keyboard freezes when selecting a new hotkey and another widget is clicked.</p> - </description> - </release> - <release version="2.1.3" date="2023-05-12"> - <description> - <p>Attempt to fix possible issue on some systems with amd/intel where capture region is incorrect when multiple monitors are connected.</p> - </description> - </release> - <release version="2.1.2" date="2023-04-27"> - <description> - <p>Attempt to fix possible audio/video sync on NVIDIA after recording for a long time. Temporary disable opus/flac because it breaks when recording multiple audio inputs.</p> - </description> - </release> - <release version="2.1.1" date="2023-04-22"> - <description> - <p>AMD/Intel: Add support for capturing cursor when recording a monitor. Fix some cases of capture being glitched when using multiple monitors.</p> - </description> - </release> - <release version="2.1.0" date="2023-04-18"> - <description> - <p>Enable AMD/Intel monitor capture. Requires the flatpak to be installed system-wide and it requires restricted root access. Record a single window if you dont like these restrictions. Videos created on AMD/Intel should be played with MPV otherwise it might have issues such as stuttering.</p> - </description> - </release> - <release version="2.0.0" date="2023-04-11"> - <description> - <p>Add experimental support for AMD/Intel. Quality might not be well tuned yet and the video is in variable framerate mode, which might cause issues with some out of date video editing software or video players. Recording on AMD/intel is currently limited to a window (monitor capture is not possible yet).</p> - </description> - </release> - <release version="1.3.5" date="2023-03-22"> - <description> - <p>Show error when using wayland (because wayland is not supported)</p> - </description> - </release> - <release version="1.3.4" date="2023-03-17"> - <description> - <p>Add option to workaround a NVIDIA driver "bug" that causes framerate to drop a bit when recording (overclock memory transfer rate back to normal)</p> - </description> - </release> - <release version="1.3.3" date="2023-03-11"> - <description> - <p>Make it clear when graphics card is not supported</p> - </description> - </release> - <release version="1.3.2" date="2023-03-04"> - <description> - <ul> - <li>Add VRR record option, only use with VRR as it might have driver issues!</li> - <li>Add opus/flac audio options. opus/flac is only supported by .mp4/.mkv. Automatically changes audio codec if not supported by the container</li> - </ul> - </description> - </release> - <release version="1.3.1" date="2023-02-22"> - <description> - <p>Fix broken replay when recording with audio (possibly broken in normal recording as well)</p> - </description> - </release> - <release version="1.3.0" date="2023-02-20"> - <description> - <ul> - <li>Switch to EGL (fixes possible window capture issues when using a compositor)</li> - <li>Add option to change hotkeys</li> - <li>Add option to merge audio tracks into one audio track</li> - <li>Add option to follow the focused window</li> - <li>Add option to force set h264/hevc (services such as discord can't play hevc videos directly in the application)</li> - <li>Show proper error when NVIDIA GPU is not in use</li> - </ul> - </description> - </release> - <release version="1.2.1" date="2022-11-24"> - <description> - <p>Allow choosing between mp4, flv and mkv for record/replay. mkv survives system crashes</p> - </description> - </release> - <release version="1.2.0" date="2022-10-27"> - <description> - <p>Re-enable screen-direct, disable h264 forced fallback and use p6 again</p> - </description> - </release> - <release version="1.1.8" date="2022-10-18"> - <description> - <p>Attempt to fix stuttering in video that can happen sometimes (especially when vsync is enabled)</p> - </description> - </release> - <release version="1.1.7" date="2022-10-16"> - <description> - <p>Properly fallback to h264 if hevc is not supported by the gpu</p> - </description> - </release> - <release version="1.1.6" date="2022-10-15"> - <description> - <p>Fix livestreaming: flv does not properly fallback to h264</p> - </description> - </release> - <release version="1.1.5" date="2022-10-11"> - <description> - <p>Use lower preset on older gpus (kepler) and switch to h264 if h265 is not supported</p> - </description> - </release> - <release version="1.1.4" date="2022-10-11"> - <description> - <p>Tune options to try and reduce file size</p> - </description> - </release> - <release version="1.1.3" date="2022-10-08"> - <description> - <p>Better tune quality options for different resolutions</p> - </description> - </release> - <release version="1.1.2" date="2022-10-07"> - <description> - <p>Fix crash caused by invalid memory write when recording audio</p> - </description> - </release> - <release version="1.1.1" date="2022-10-05"> - <description> - <p>Fix possible pulseaudio crash</p> - </description> - </release> - <release version="1.1.0" date="2022-10-05"> - <description> - <p>Fixes some streaming issues (mostly metadata missing)</p> - </description> - </release> - <release version="1.0.0" date="2022-09-30"/> - </releases> - <content_rating type="oars-1.1"/> -</component> diff --git a/gpu-screen-recorder-gtk.desktop b/com.dec05eba.gpu_screen_recorder.desktop index 0a83558..0a83558 100644 --- a/gpu-screen-recorder-gtk.desktop +++ b/com.dec05eba.gpu_screen_recorder.desktop @@ -7,7 +7,8 @@ cd "$script_dir" echo "Warning: this install.sh script is deprecated. Use meson directly instead if possible" -test -d build || meson setup build +rm -rf build +meson setup build meson configure --prefix=/usr --buildtype=release -Dstrip=true build ninja -C build install diff --git a/meson.build b/meson.build index cdfc095..ee35e06 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('gpu-screen-recorder-gtk', ['c', 'cpp'], version : '3.8.0', default_options : ['warning_level=2']) +project('gpu-screen-recorder-gtk', ['c', 'cpp'], version : '5.7.6', default_options : ['warning_level=2']) add_project_arguments('-Wshadow', language : ['c', 'cpp']) if get_option('buildtype') == 'debug' @@ -8,28 +8,32 @@ elif get_option('buildtype') == 'release' endif src = [ - 'src/egl.c', - 'src/library_loader.c', + 'src/global_shortcuts.c', 'src/main.cpp', ] + dep = [ dependency('gtk+-3.0'), dependency('x11'), - dependency('xrandr'), - dependency('libpulse'), - dependency('libdrm'), - dependency('wayland-egl'), - dependency('wayland-client'), dependency('ayatana-appindicator3-0.1'), ] -executable('gpu-screen-recorder-gtk', src, dependencies : dep, install : true) - prefix = get_option('prefix') datadir = get_option('datadir') -install_data(files('gpu-screen-recorder-gtk.desktop'), install_dir : join_paths(prefix, datadir, 'applications')) -install_data(files('com.dec05eba.gpu_screen_recorder.appdata.xml'), install_dir : join_paths(prefix, datadir, 'metainfo')) -install_subdir('icons/hicolor', install_dir : join_paths(prefix, datadir, 'icons')) +icons_path = join_paths(prefix, datadir, 'icons') + +executable('gpu-screen-recorder-gtk', + src, + dependencies : dep, + install : true, + cpp_args : [ + '-DGSR_ICONS_PATH="' + icons_path + '"', + '-DGSR_VERSION="' + meson.project_version() + '"' + ] +) + +install_data(files('com.dec05eba.gpu_screen_recorder.desktop'), install_dir : join_paths(prefix, datadir, 'applications')) +install_subdir('icons/hicolor', install_dir : icons_path) gnome = import('gnome') -gnome.post_install(gtk_update_icon_cache : true, update_desktop_database : true)
\ No newline at end of file +gnome.post_install(gtk_update_icon_cache : true, update_desktop_database : true) diff --git a/project.conf b/project.conf index 164cf6b..ddf4c22 100644 --- a/project.conf +++ b/project.conf @@ -1,19 +1,14 @@ [package] name = "gpu-screen-recorder-gtk" type = "executable" -version = "3.8.0" +version = "5.7.6" platforms = ["posix"] [config] ignore_dirs = ["build"] -error_on_warning = "true" +error_on_warning = "false" [dependencies] gtk+-3.0 = "3" x11 = "1" -xrandr = "1" -libpulse = ">=13" -libdrm = ">=2" -wayland-egl = ">=15" -wayland-client = ">=1" ayatana-appindicator3-0.1 = ">=0" diff --git a/src/config.hpp b/src/config.hpp index be58704..4f28994 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -23,8 +23,13 @@ struct MainConfig { std::string record_area_option; int32_t record_area_width = 0; int32_t record_area_height = 0; + int32_t video_width = 0; + int32_t video_height = 0; int32_t fps = 60; + int32_t video_bitrate = 15000; bool merge_audio_tracks = true; + bool record_app_audio_inverted = false; + bool change_video_resolution = false; std::vector<std::string> audio_input; std::string color_range; std::string quality; @@ -33,9 +38,18 @@ struct MainConfig { std::string framerate_mode; bool advanced_view = false; bool overclock = false; - bool show_notifications = true; + bool show_recording_started_notifications = false; + bool show_recording_stopped_notifications = false; + bool show_recording_saved_notifications = true; bool record_cursor = true; bool hide_window_when_recording = false; + bool software_encoding_warning_shown = false; + bool steam_deck_warning_shown = false; + bool hevc_amd_bug_warning_shown = false; + bool av1_amd_bug_warning_shown = false; + bool restore_portal_session = true; + bool use_new_ui = false; + int32_t installed_gsr_global_hotkeys_version = 0; }; struct YoutubeStreamConfig { @@ -56,21 +70,21 @@ struct StreamingConfig { YoutubeStreamConfig youtube; TwitchStreamConfig twitch; CustomStreamConfig custom; - ConfigHotkey start_recording_hotkey; + ConfigHotkey start_stop_recording_hotkey; }; struct RecordConfig { std::string save_directory; std::string container; - ConfigHotkey start_recording_hotkey; - ConfigHotkey pause_recording_hotkey; + ConfigHotkey start_stop_recording_hotkey; + ConfigHotkey pause_unpause_recording_hotkey; }; struct ReplayConfig { std::string save_directory; std::string container; int32_t replay_time = 30; - ConfigHotkey start_recording_hotkey; + ConfigHotkey start_stop_recording_hotkey; ConfigHotkey save_recording_hotkey; }; @@ -288,7 +302,7 @@ static bool config_split_key_value(const StringView str, StringView &key, String value.str = str.str + index + 1; value.size = str.size - (index + 1); - + return true; } @@ -297,8 +311,13 @@ static std::map<std::string, ConfigValue> get_config_options(Config &config) { {"main.record_area_option", {CONFIG_TYPE_STRING, &config.main_config.record_area_option}}, {"main.record_area_width", {CONFIG_TYPE_I32, &config.main_config.record_area_width}}, {"main.record_area_height", {CONFIG_TYPE_I32, &config.main_config.record_area_height}}, + {"main.video_width", {CONFIG_TYPE_I32, &config.main_config.video_width}}, + {"main.video_height", {CONFIG_TYPE_I32, &config.main_config.video_height}}, {"main.fps", {CONFIG_TYPE_I32, &config.main_config.fps}}, + {"main.video_bitrate", {CONFIG_TYPE_I32, &config.main_config.video_bitrate}}, {"main.merge_audio_tracks", {CONFIG_TYPE_BOOL, &config.main_config.merge_audio_tracks}}, + {"main.record_app_audio_inverted", {CONFIG_TYPE_BOOL, &config.main_config.record_app_audio_inverted}}, + {"main.change_video_resolution", {CONFIG_TYPE_BOOL, &config.main_config.change_video_resolution}}, {"main.audio_input", {CONFIG_TYPE_STRING_ARRAY, &config.main_config.audio_input}}, {"main.color_range", {CONFIG_TYPE_STRING, &config.main_config.color_range}}, {"main.quality", {CONFIG_TYPE_STRING, &config.main_config.quality}}, @@ -307,26 +326,35 @@ static std::map<std::string, ConfigValue> get_config_options(Config &config) { {"main.framerate_mode", {CONFIG_TYPE_STRING, &config.main_config.framerate_mode}}, {"main.advanced_view", {CONFIG_TYPE_BOOL, &config.main_config.advanced_view}}, {"main.overclock", {CONFIG_TYPE_BOOL, &config.main_config.overclock}}, - {"main.show_notifications", {CONFIG_TYPE_BOOL, &config.main_config.show_notifications}}, + {"main.show_recording_started_notifications", {CONFIG_TYPE_BOOL, &config.main_config.show_recording_started_notifications}}, + {"main.show_recording_stopped_notifications", {CONFIG_TYPE_BOOL, &config.main_config.show_recording_stopped_notifications}}, + {"main.show_recording_saved_notifications", {CONFIG_TYPE_BOOL, &config.main_config.show_recording_saved_notifications}}, {"main.record_cursor", {CONFIG_TYPE_BOOL, &config.main_config.record_cursor}}, {"main.hide_window_when_recording", {CONFIG_TYPE_BOOL, &config.main_config.hide_window_when_recording}}, + {"main.software_encoding_warning_shown", {CONFIG_TYPE_BOOL, &config.main_config.software_encoding_warning_shown}}, + {"main.steam_deck_warning_shown", {CONFIG_TYPE_BOOL, &config.main_config.steam_deck_warning_shown}}, + {"main.hevc_amd_bug_warning_shown", {CONFIG_TYPE_BOOL, &config.main_config.hevc_amd_bug_warning_shown}}, + {"main.av1_amd_bug_warning_shown", {CONFIG_TYPE_BOOL, &config.main_config.av1_amd_bug_warning_shown}}, + {"main.restore_portal_session", {CONFIG_TYPE_BOOL, &config.main_config.restore_portal_session}}, + {"main.use_new_ui", {CONFIG_TYPE_BOOL, &config.main_config.use_new_ui}}, + {"main.installed_gsr_global_hotkeys_version", {CONFIG_TYPE_I32, &config.main_config.installed_gsr_global_hotkeys_version}}, {"streaming.service", {CONFIG_TYPE_STRING, &config.streaming_config.streaming_service}}, {"streaming.youtube.key", {CONFIG_TYPE_STRING, &config.streaming_config.youtube.stream_key}}, {"streaming.twitch.key", {CONFIG_TYPE_STRING, &config.streaming_config.twitch.stream_key}}, {"streaming.custom.url", {CONFIG_TYPE_STRING, &config.streaming_config.custom.url}}, {"streaming.custom.container", {CONFIG_TYPE_STRING, &config.streaming_config.custom.container}}, - {"streaming.start_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.streaming_config.start_recording_hotkey}}, + {"streaming.start_stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.streaming_config.start_stop_recording_hotkey}}, {"record.save_directory", {CONFIG_TYPE_STRING, &config.record_config.save_directory}}, {"record.container", {CONFIG_TYPE_STRING, &config.record_config.container}}, - {"record.start_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.start_recording_hotkey}}, - {"record.pause_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.pause_recording_hotkey}}, + {"record.start_stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.start_stop_recording_hotkey}}, + {"record.pause_unpause_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.pause_unpause_recording_hotkey}}, {"replay.save_directory", {CONFIG_TYPE_STRING, &config.replay_config.save_directory}}, {"replay.container", {CONFIG_TYPE_STRING, &config.replay_config.container}}, {"replay.time", {CONFIG_TYPE_I32, &config.replay_config.replay_time}}, - {"replay.start_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.replay_config.start_recording_hotkey}}, + {"replay.start_stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.replay_config.start_stop_recording_hotkey}}, {"replay.save_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.replay_config.save_recording_hotkey}} }; } @@ -349,13 +377,13 @@ static Config read_config(bool &config_empty) { auto config_options = get_config_options(config); string_split_char(file_content, '\n', [&](StringView line) { - StringView key, value; - if(!config_split_key_value(line, key, value)) { + StringView key, sv_val; + if(!config_split_key_value(line, key, sv_val)) { fprintf(stderr, "Warning: Invalid config option format: %.*s\n", (int)line.size, line.str); return true; } - if(key.size == 0 || value.size == 0) + if(key.size == 0 || sv_val.size == 0) return true; auto it = config_options.find(std::string(key.str, key.size)); @@ -364,15 +392,15 @@ static Config read_config(bool &config_empty) { switch(it->second.type) { case CONFIG_TYPE_BOOL: { - *(bool*)it->second.data = value == "true"; + *(bool*)it->second.data = sv_val == "true"; break; } case CONFIG_TYPE_STRING: { - ((std::string*)it->second.data)->assign(value.str, value.size); + ((std::string*)it->second.data)->assign(sv_val.str, sv_val.size); break; } case CONFIG_TYPE_I32: { - std::string value_str(value.str, value.size); + std::string value_str(sv_val.str, sv_val.size); int32_t *value = (int32_t*)it->second.data; if(sscanf(value_str.c_str(), FORMAT_I32, value) != 1) { fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key.size, key.str); @@ -381,7 +409,7 @@ static Config read_config(bool &config_empty) { break; } case CONFIG_TYPE_HOTKEY: { - std::string value_str(value.str, value.size); + std::string value_str(sv_val.str, sv_val.size); ConfigHotkey *config_hotkey = (ConfigHotkey*)it->second.data; if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->keysym, &config_hotkey->modifiers) != 2) { fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key.size, key.str); @@ -391,7 +419,7 @@ static Config read_config(bool &config_empty) { break; } case CONFIG_TYPE_STRING_ARRAY: { - std::string array_value(value.str, value.size); + std::string array_value(sv_val.str, sv_val.size); ((std::vector<std::string>*)it->second.data)->push_back(std::move(array_value)); break; } diff --git a/src/egl.c b/src/egl.c deleted file mode 100644 index 79aab1f..0000000 --- a/src/egl.c +++ /dev/null @@ -1,380 +0,0 @@ -#include "egl.h" -#include "library_loader.h" -#include <string.h> -#include <stdio.h> -#include <stdlib.h> -#include <dlfcn.h> -#include <assert.h> - -#include <wayland-client.h> -#include <wayland-egl.h> -#include <unistd.h> - -static void output_handle_geometry(void *data, struct wl_output *wl_output, - int32_t x, int32_t y, int32_t phys_width, int32_t phys_height, - int32_t subpixel, const char *make, const char *model, - int32_t transform) { - (void)wl_output; - (void)phys_width; - (void)phys_height; - (void)subpixel; - (void)make; - (void)model; - (void)transform; - gsr_wayland_output *gsr_output = data; - gsr_output->pos.x = x; - gsr_output->pos.y = y; -} - -static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { - (void)wl_output; - (void)flags; - (void)refresh; - gsr_wayland_output *gsr_output = data; - gsr_output->size.x = width; - gsr_output->size.y = height; -} - -static void output_handle_done(void *data, struct wl_output *wl_output) { - (void)data; - (void)wl_output; -} - -static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) { - (void)data; - (void)wl_output; - (void)factor; -} - -static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) { - (void)wl_output; - gsr_wayland_output *gsr_output = data; - if(gsr_output->name) { - free(gsr_output->name); - gsr_output->name = NULL; - } - gsr_output->name = strdup(name); -} - -static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) { - (void)data; - (void)wl_output; - (void)description; -} - -static const struct wl_output_listener output_listener = { - .geometry = output_handle_geometry, - .mode = output_handle_mode, - .done = output_handle_done, - .scale = output_handle_scale, - .name = output_handle_name, - .description = output_handle_description, -}; - -static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { - (void)version; - gsr_egl *egl = data; - if (strcmp(interface, "wl_compositor") == 0) { - if(egl->wayland.compositor) { - wl_compositor_destroy(egl->wayland.compositor); - egl->wayland.compositor = NULL; - } - egl->wayland.compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 1); - } else if(strcmp(interface, wl_output_interface.name) == 0) { - if(version < 4) { - fprintf(stderr, "gsr warning: wl output interface version is < 4, expected >= 4 to capture a monitor. Using KMS capture instead\n"); - return; - } - - if(egl->wayland.num_outputs == GSR_MAX_OUTPUTS) { - fprintf(stderr, "gsr warning: reached maximum outputs (32), ignoring output %u\n", name); - return; - } - - gsr_wayland_output *gsr_output = &egl->wayland.outputs[egl->wayland.num_outputs]; - egl->wayland.num_outputs++; - *gsr_output = (gsr_wayland_output) { - .wl_name = name, - .output = wl_registry_bind(registry, name, &wl_output_interface, 4), - .pos = { .x = 0, .y = 0 }, - .size = { .x = 0, .y = 0 }, - .name = NULL, - }; - wl_output_add_listener(gsr_output->output, &output_listener, gsr_output); - } -} - -static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) { - (void)data; - (void)registry; - (void)name; -} - -static struct wl_registry_listener registry_listener = { - .global = registry_add_object, - .global_remove = registry_remove_object, -}; - -// TODO: Create egl context without surface (in other words, x11/wayland agnostic, doesn't require x11/wayland dependency) -static bool gsr_egl_create_window(gsr_egl *self, bool wayland) { - EGLConfig ecfg; - int32_t num_config = 0; - - const int32_t attr[] = { - EGL_BUFFER_SIZE, 24, - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, - EGL_NONE - }; - - const int32_t ctxattr[] = { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE - }; - - if(wayland) { - self->wayland.dpy = wl_display_connect(NULL); - if(!self->wayland.dpy) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: wl_display_connect failed\n"); - goto fail; - } - - self->wayland.registry = wl_display_get_registry(self->wayland.dpy); // TODO: Error checking - wl_registry_add_listener(self->wayland.registry, ®istry_listener, self); // TODO: Error checking - - // Fetch globals - wl_display_roundtrip(self->wayland.dpy); - - // fetch wl_output - wl_display_roundtrip(self->wayland.dpy); - - if(!self->wayland.compositor) { - fprintf(stderr, "gsr error: gsr_gl_create_window failed: failed to find compositor\n"); - goto fail; - } - } else { - self->x11.window = XCreateWindow(self->x11.dpy, DefaultRootWindow(self->x11.dpy), 0, 0, 16, 16, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); - - if(!self->x11.window) { - fprintf(stderr, "gsr error: gsr_gl_create_window failed: failed to create gl window\n"); - goto fail; - } - } - - self->eglBindAPI(EGL_OPENGL_API); - - self->egl_display = self->eglGetDisplay(self->wayland.dpy ? (EGLNativeDisplayType)self->wayland.dpy : (EGLNativeDisplayType)self->x11.dpy); - if(!self->egl_display) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglGetDisplay failed\n"); - goto fail; - } - - if(!self->eglInitialize(self->egl_display, NULL, NULL)) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglInitialize failed\n"); - goto fail; - } - - if(!self->eglChooseConfig(self->egl_display, attr, &ecfg, 1, &num_config) || num_config != 1) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a matching config\n"); - goto fail; - } - - self->egl_context = self->eglCreateContext(self->egl_display, ecfg, NULL, ctxattr); - if(!self->egl_context) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create egl context\n"); - goto fail; - } - - if(wayland) { - self->wayland.surface = wl_compositor_create_surface(self->wayland.compositor); - self->wayland.window = wl_egl_window_create(self->wayland.surface, 16, 16); - self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)self->wayland.window, NULL); - } else { - self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)self->x11.window, NULL); - } - - if(!self->egl_surface) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create window surface\n"); - goto fail; - } - - if(!self->eglMakeCurrent(self->egl_display, self->egl_surface, self->egl_surface, self->egl_context)) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make context current\n"); - goto fail; - } - - return true; - - fail: - gsr_egl_unload(self); - return false; -} - -static bool gsr_egl_load_egl(gsr_egl *self, void *library) { - const dlsym_assign required_dlsym[] = { - { (void**)&self->eglGetDisplay, "eglGetDisplay" }, - { (void**)&self->eglInitialize, "eglInitialize" }, - { (void**)&self->eglTerminate, "eglTerminate" }, - { (void**)&self->eglChooseConfig, "eglChooseConfig" }, - { (void**)&self->eglCreateWindowSurface, "eglCreateWindowSurface" }, - { (void**)&self->eglCreateContext, "eglCreateContext" }, - { (void**)&self->eglMakeCurrent, "eglMakeCurrent" }, - { (void**)&self->eglDestroyContext, "eglDestroyContext" }, - { (void**)&self->eglDestroySurface, "eglDestroySurface" }, - { (void**)&self->eglBindAPI, "eglBindAPI" }, - { (void**)&self->eglGetProcAddress, "eglGetProcAddress" }, - - { NULL, NULL } - }; - - if(!dlsym_load_list(library, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libEGL.so.1\n"); - return false; - } - - return true; -} - -static bool gsr_egl_load_gl(gsr_egl *self, void *library) { - const dlsym_assign required_dlsym[] = { - { (void**)&self->glGetString, "glGetString" }, - - { NULL, NULL } - }; - - if(!dlsym_load_list(library, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGL.so.1\n"); - return false; - } - - return true; -} - -static bool gsr_egl_proc_load_egl(gsr_egl *self) { - self->eglQueryDisplayAttribEXT = (FUNC_eglQueryDisplayAttribEXT)self->eglGetProcAddress("eglQueryDisplayAttribEXT"); - self->eglQueryDeviceStringEXT = (FUNC_eglQueryDeviceStringEXT)self->eglGetProcAddress("eglQueryDeviceStringEXT"); - - return true; -} - -bool gsr_egl_load(gsr_egl *self, Display *dpy, bool wayland) { - memset(self, 0, sizeof(gsr_egl)); - self->x11.dpy = dpy; - - void *egl_lib = NULL; - void *gl_lib = NULL; - - dlerror(); /* clear */ - egl_lib = dlopen("libEGL.so.1", RTLD_LAZY); - if(!egl_lib) { - fprintf(stderr, "gsr error: gsr_egl_load: failed to load libEGL.so.1, error: %s\n", dlerror()); - goto fail; - } - - gl_lib = dlopen("libGL.so.1", RTLD_LAZY); - if(!egl_lib) { - fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGL.so.1, error: %s\n", dlerror()); - goto fail; - } - - if(!gsr_egl_load_egl(self, egl_lib)) - goto fail; - - if(!gsr_egl_load_gl(self, gl_lib)) - goto fail; - - if(!gsr_egl_proc_load_egl(self)) - goto fail; - - if(!gsr_egl_create_window(self, wayland)) - goto fail; - - if(self->eglQueryDisplayAttribEXT && self->eglQueryDeviceStringEXT) { - intptr_t device = 0; - if(self->eglQueryDisplayAttribEXT(self->egl_display, EGL_DEVICE_EXT, &device) && device) - self->dri_card_path = self->eglQueryDeviceStringEXT((void*)device, EGL_DRM_DEVICE_FILE_EXT); - } - - self->egl_library = egl_lib; - self->gl_library = gl_lib; - return true; - - fail: - if(egl_lib) - dlclose(egl_lib); - if(gl_lib) - dlclose(gl_lib); - memset(self, 0, sizeof(gsr_egl)); - return false; -} - -void gsr_egl_unload(gsr_egl *self) { - if(self->egl_context) { - self->eglDestroyContext(self->egl_display, self->egl_context); - self->egl_context = NULL; - } - - if(self->egl_surface) { - self->eglDestroySurface(self->egl_display, self->egl_surface); - self->egl_surface = NULL; - } - - if(self->egl_display) { - self->eglTerminate(self->egl_display); - self->egl_display = NULL; - } - - if(self->x11.window) { - XDestroyWindow(self->x11.dpy, self->x11.window); - self->x11.window = None; - } - - if(self->wayland.window) { - wl_egl_window_destroy(self->wayland.window); - self->wayland.window = NULL; - } - - if(self->wayland.surface) { - wl_surface_destroy(self->wayland.surface); - self->wayland.surface = NULL; - } - - for(int i = 0; i < self->wayland.num_outputs; ++i) { - if(self->wayland.outputs[i].output) { - wl_output_destroy(self->wayland.outputs[i].output); - self->wayland.outputs[i].output = NULL; - } - - if(self->wayland.outputs[i].name) { - free(self->wayland.outputs[i].name); - self->wayland.outputs[i].name = NULL; - } - } - self->wayland.num_outputs = 0; - - if(self->wayland.compositor) { - wl_compositor_destroy(self->wayland.compositor); - self->wayland.compositor = NULL; - } - - if(self->wayland.registry) { - wl_registry_destroy(self->wayland.registry); - self->wayland.registry = NULL; - } - - if(self->wayland.dpy) { - wl_display_disconnect(self->wayland.dpy); - self->wayland.dpy = NULL; - } - - if(self->egl_library) { - dlclose(self->egl_library); - self->egl_library = NULL; - } - - if(self->gl_library) { - dlclose(self->gl_library); - self->gl_library = NULL; - } - - memset(self, 0, sizeof(gsr_egl)); -} diff --git a/src/egl.h b/src/egl.h deleted file mode 100644 index e46a6ab..0000000 --- a/src/egl.h +++ /dev/null @@ -1,114 +0,0 @@ -#ifndef GSR_EGL_H -#define GSR_EGL_H - -/* OpenGL EGL library with a hidden window context (to allow using the opengl functions) */ - -#include <X11/X.h> -#include <X11/Xutil.h> -#include <stdbool.h> -#include <stdint.h> - -typedef struct { - int x, y; -} vec2i; - -#ifdef _WIN64 -typedef signed long long int khronos_intptr_t; -typedef unsigned long long int khronos_uintptr_t; -typedef signed long long int khronos_ssize_t; -typedef unsigned long long int khronos_usize_t; -#else -typedef signed long int khronos_intptr_t; -typedef unsigned long int khronos_uintptr_t; -typedef signed long int khronos_ssize_t; -typedef unsigned long int khronos_usize_t; -#endif - -typedef void* EGLDisplay; -typedef void* EGLNativeDisplayType; -typedef uintptr_t EGLNativeWindowType; -typedef uintptr_t EGLNativePixmapType; -typedef void* EGLConfig; -typedef void* EGLSurface; -typedef void* EGLContext; -typedef void* EGLClientBuffer; -typedef void* EGLImage; -typedef void* EGLImageKHR; -typedef void *GLeglImageOES; -typedef void (*__eglMustCastToProperFunctionPointerType)(void); - -typedef int (*FUNC_eglQueryDisplayAttribEXT)(EGLDisplay dpy, int32_t attribute, intptr_t *value); -typedef const char* (*FUNC_eglQueryDeviceStringEXT)(void *device, int32_t name); - -#define EGL_BUFFER_SIZE 0x3020 -#define EGL_RENDERABLE_TYPE 0x3040 -#define EGL_OPENGL_BIT 0x0008 -#define EGL_OPENGL_API 0x30A2 -#define EGL_NONE 0x3038 -#define EGL_CONTEXT_CLIENT_VERSION 0x3098 -#define EGL_DEVICE_EXT 0x322C -#define EGL_DRM_DEVICE_FILE_EXT 0x3233 - -#define GL_VENDOR 0x1F00 -#define GL_RENDERER 0x1F01 - -#define GSR_MAX_OUTPUTS 32 - -typedef struct { - Display *dpy; - Window window; -} gsr_x11; - -typedef struct { - uint32_t wl_name; - void *output; - vec2i pos; - vec2i size; - char *name; -} gsr_wayland_output; - -typedef struct { - void *dpy; - void *window; - void *registry; - void *surface; - void *compositor; - gsr_wayland_output outputs[GSR_MAX_OUTPUTS]; - int num_outputs; -} gsr_wayland; - -typedef struct { - void *egl_library; - void *gl_library; - - EGLDisplay egl_display; - EGLSurface egl_surface; - EGLContext egl_context; - const char *dri_card_path; - - gsr_x11 x11; - gsr_wayland wayland; - char card_path[128]; - - EGLDisplay (*eglGetDisplay)(EGLNativeDisplayType display_id); - unsigned int (*eglInitialize)(EGLDisplay dpy, int32_t *major, int32_t *minor); - unsigned int (*eglTerminate)(EGLDisplay dpy); - unsigned int (*eglChooseConfig)(EGLDisplay dpy, const int32_t *attrib_list, EGLConfig *configs, int32_t config_size, int32_t *num_config); - EGLSurface (*eglCreateWindowSurface)(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const int32_t *attrib_list); - EGLContext (*eglCreateContext)(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const int32_t *attrib_list); - unsigned int (*eglMakeCurrent)(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx); - unsigned int (*eglDestroyContext)(EGLDisplay dpy, EGLContext ctx); - unsigned int (*eglDestroySurface)(EGLDisplay dpy, EGLSurface surface); - unsigned int (*eglBindAPI)(unsigned int api); - __eglMustCastToProperFunctionPointerType (*eglGetProcAddress)(const char *procname); - - FUNC_eglQueryDisplayAttribEXT eglQueryDisplayAttribEXT; - FUNC_eglQueryDeviceStringEXT eglQueryDeviceStringEXT; - - const unsigned char* (*glGetString)(unsigned int name); -} gsr_egl; - -bool gsr_egl_load(gsr_egl *self, Display *dpy, bool wayland); -void gsr_egl_unload(gsr_egl *self); - -#endif /* GSR_EGL_H */ diff --git a/src/global_shortcuts.c b/src/global_shortcuts.c new file mode 100644 index 0000000..9933891 --- /dev/null +++ b/src/global_shortcuts.c @@ -0,0 +1,336 @@ +#include "global_shortcuts.h" +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <sys/random.h> +#include <gio/gio.h> + +/* TODO: Remove G_DBUS_CALL_FLAGS_NO_AUTO_START and G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START? also in gpu screen recorder equivalent */ +/* TODO: More error handling and clean up resources after done */ +/* TODO: Use GArray instead of GVariant where possible */ + +static bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) { + /* TODO: Use other functions on other platforms than linux */ + if(getrandom(buffer, buffer_size, 0) < buffer_size) { + fprintf(stderr, "gsr error: generate_random_characters: failed to get random bytes, error: %s\n", strerror(errno)); + return false; + } + + for(int i = 0; i < buffer_size; ++i) { + unsigned char c = *(unsigned char*)&buffer[i]; + buffer[i] = alphabet[c % alphabet_size]; + } + + return true; +} + +static void gsr_dbus_portal_get_unique_handle_token(gsr_global_shortcuts *self, char *buffer, int size) { + snprintf(buffer, size, "gpu_screen_recorder_gtk_handle_%s_%u", self->random_str, self->handle_counter++); +} + +/* Assumes shortcuts is an array */ +static void handle_shortcuts_data(GVariant *shortcuts, gsr_shortcut_callback callback, void *userdata) { + for(guint i = 0; i < g_variant_n_children(shortcuts); ++i) { + gchar *shortcut_id = NULL; + GVariant *shortcut_values = NULL; + g_variant_get_child(shortcuts, i, "(s@a{sv})", &shortcut_id, &shortcut_values); + + if(!shortcut_id || !shortcut_values) + continue; + + // gchar *description = NULL; + // g_variant_lookup(shortcut_values, "description", "s", &description); + + gchar *trigger_description = NULL; + g_variant_lookup(shortcut_values, "trigger_description", "s", &trigger_description); + + gsr_shortcut shortcut; + shortcut.id = shortcut_id; + shortcut.trigger_description = trigger_description ? trigger_description : ""; + callback(shortcut, userdata); + } +} + +typedef struct { + gsr_global_shortcuts *self; + gsr_shortcut_callback callback; + void *userdata; +} signal_list_bind_userdata; + +static void dbus_signal_list_bind(GDBusProxy *proxy, gchar *sender_name, gchar *signal_name, GVariant *parameters, signal_list_bind_userdata *userdata) { + (void)proxy; + (void)sender_name; + if(g_strcmp0(signal_name, "Response") != 0) + goto done; + + guint32 response = 0; + GVariant *results = NULL; + g_variant_get(parameters, "(u@a{sv})", &response, &results); + + if(response != 0 || !results) + goto done; + + GVariant *shortcuts = g_variant_lookup_value(results, "shortcuts", G_VARIANT_TYPE("a(sa{sv})")); + if(!shortcuts) + goto done; + + handle_shortcuts_data(shortcuts, userdata->callback, userdata->userdata); + + done: + free(userdata); +} + +typedef struct { + gsr_global_shortcuts *self; + gsr_deactivated_callback deactivated_callback; + gsr_shortcut_callback shortcut_changed_callback; + void *userdata; +} signal_userdata; + +static void signal_callback(GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer userdata) +{ + (void)connection; + (void)sender_name; + (void)object_path; + (void)interface_name; + (void)signal_name; + (void)parameters; + signal_userdata *cu = userdata; + + /* Button released */ + if(strcmp(signal_name, "Deactivated") == 0) { + gchar *session_handle = NULL; + gchar *shortcut_id = NULL; + gchar *timestamp = NULL; + GVariant *options = NULL; + g_variant_get(parameters, "(ost@a{sv})", &session_handle, &shortcut_id, ×tamp, &options); + + if(session_handle && shortcut_id && strcmp(session_handle, cu->self->session_handle) == 0) + cu->deactivated_callback(shortcut_id, cu->userdata); + } else if(strcmp(signal_name, "ShortcutsChanged") == 0) { + gchar *session_handle = NULL; + GVariant *shortcuts = NULL; + g_variant_get(parameters, "(o@a(sa{sv}))", &session_handle, &shortcuts); + + if(session_handle && shortcuts && strcmp(session_handle, cu->self->session_handle) == 0) + handle_shortcuts_data(shortcuts, cu->shortcut_changed_callback, cu->userdata); + } +} + +typedef struct { + gsr_global_shortcuts *self; + gsr_init_callback callback; + void *userdata; +} signal_create_session_userdata; + +static void dbus_signal_create_session(GDBusProxy *proxy, gchar *sender_name, gchar *signal_name, GVariant *parameters, signal_create_session_userdata *cu) { + (void)proxy; + (void)sender_name; + if(g_strcmp0(signal_name, "Response") != 0) + goto done; + + guint32 response = 0; + GVariant *results = NULL; + g_variant_get(parameters, "(u@a{sv})", &response, &results); + + if(response != 0 || !results) { + cu->callback(false, cu->userdata); + goto done; + } + + gchar *session_handle = NULL; + if(g_variant_lookup(results, "session_handle", "s", &session_handle) && session_handle) { + cu->self->session_handle = strdup(session_handle); + cu->self->session_created = true; + cu->callback(true, cu->userdata); + } + + done: + free(cu); +} + +static bool gsr_global_shortcuts_create_session(gsr_global_shortcuts *self, gsr_init_callback callback, void *userdata) { + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + char session_handle_token[64]; + snprintf(session_handle_token, sizeof(session_handle_token), "gpu_screen_recorder_gtk"); + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(handle_token)); + g_variant_builder_add(&builder, "{sv}", "session_handle_token", g_variant_new_string(session_handle_token)); + GVariant *aa = g_variant_builder_end(&builder); + + GVariant *ret = g_dbus_connection_call_sync(self->gdbus_con, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "CreateSession", g_variant_new_tuple(&aa, 1), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, 1000, NULL, NULL); + + if(ret) { + const gchar *val = NULL; + g_variant_get(ret, "(&o)", &val); + if(!val) + return false; + //g_variant_unref(ret); + + GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, NULL, "org.freedesktop.portal.Desktop", val, "org.freedesktop.portal.Request", NULL, NULL); + if(!proxy) + return false; + //g_object_unref(proxy); + + signal_create_session_userdata *cu = malloc(sizeof(signal_create_session_userdata)); + cu->self = self; + cu->callback = callback; + cu->userdata = userdata; + g_signal_connect(proxy, "g-signal", G_CALLBACK(dbus_signal_create_session), cu); + return true; + } else { + return false; + } +} + +bool gsr_global_shortcuts_init(gsr_global_shortcuts *self, gsr_init_callback callback, void *userdata) { + memset(self, 0, sizeof(*self)); + + self->random_str[DBUS_RANDOM_STR_SIZE] = '\0'; + if(!generate_random_characters(self->random_str, DBUS_RANDOM_STR_SIZE, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 62)) { + fprintf(stderr, "gsr error: gsr_global_shortcuts_init: failed to generate random string\n"); + return false; + } + + self->gdbus_con = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); + if(!self->gdbus_con) { + fprintf(stderr, "gsr error: gsr_global_shortcuts_init: g_bus_get_sync failed\n"); + return false; + } + + if(!gsr_global_shortcuts_create_session(self, callback, userdata)) { + gsr_global_shortcuts_deinit(self); + return false; + } + + return true; +} + +void gsr_global_shortcuts_deinit(gsr_global_shortcuts *self) { + if(self->gdbus_con) { + /* TODO: Re-add this. Right now it causes errors as the connection is already closed, but checking if it's already closed here has no effect */ + //g_dbus_connection_close(self->gdbus_con, NULL, NULL, NULL); + self->gdbus_con = NULL; + } + + if(self->session_handle) { + free(self->session_handle); + self->session_handle = NULL; + } +} + +bool gsr_global_shortcuts_list_shortcuts(gsr_global_shortcuts *self, gsr_shortcut_callback callback, void *userdata) { + if(!self->session_created) + return false; + + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + GVariant *session_handle_obj = g_variant_new_object_path(self->session_handle); + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(handle_token)); + GVariant *aa = g_variant_builder_end(&builder); + + GVariant *args[2] = { session_handle_obj, aa }; + + GVariant *ret = g_dbus_connection_call_sync(self->gdbus_con, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "ListShortcuts", g_variant_new_tuple(args, 2), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, 1000, NULL, NULL); + if(ret) { + const gchar *val = NULL; + g_variant_get(ret, "(&o)", &val); + if(!val) + return false; + + GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, NULL, "org.freedesktop.portal.Desktop", val, "org.freedesktop.portal.Request", NULL, NULL); + if(!proxy) + return false; + //g_object_unref(proxy); + + signal_list_bind_userdata *cu = malloc(sizeof(signal_list_bind_userdata)); + cu->self = self; + cu->callback = callback; + cu->userdata = userdata; + g_signal_connect(proxy, "g-signal", G_CALLBACK(dbus_signal_list_bind), cu); + return true; + } else { + return false; + } +} + +bool gsr_global_shortcuts_bind_shortcuts(gsr_global_shortcuts *self, const gsr_bind_shortcut *shortcuts, int num_shortcuts, gsr_shortcut_callback callback, void *userdata) { + if(!self->session_created) + return false; + + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + GVariant *session_handle_obj = g_variant_new_object_path(self->session_handle); + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a(sa{sv})")); + + for(int i = 0; i < num_shortcuts; ++i) { + GVariantBuilder shortcuts_builder; + g_variant_builder_init(&shortcuts_builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&shortcuts_builder, "{sv}", "description", g_variant_new_string(shortcuts[i].description)); + g_variant_builder_add(&shortcuts_builder, "{sv}", "preferred_trigger", g_variant_new_string(shortcuts[i].shortcut.trigger_description)); + GVariant *shortcuts_data = g_variant_builder_end(&shortcuts_builder); + GVariant *ss_l[2] = { g_variant_new_string(shortcuts[i].shortcut.id), shortcuts_data }; + g_variant_builder_add_value(&builder, g_variant_new_tuple(ss_l, 2)); + } + GVariant *aa = g_variant_builder_end(&builder); + + GVariantBuilder builder_zzz; + g_variant_builder_init(&builder_zzz, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder_zzz, "{sv}", "handle_token", g_variant_new_string(handle_token)); + GVariant *bb = g_variant_builder_end(&builder_zzz); + + GVariant *parent_window = g_variant_new_string(""); + GVariant *args[4] = { session_handle_obj, aa, parent_window, bb }; + + GVariant *ret = g_dbus_connection_call_sync(self->gdbus_con, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "BindShortcuts", g_variant_new_tuple(args, 4), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, NULL); + if(ret) { + const gchar *val = NULL; + g_variant_get(ret, "(&o)", &val); + if(!val) + return false; + + GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, NULL, "org.freedesktop.portal.Desktop", val, "org.freedesktop.portal.Request", NULL, NULL); + if(!proxy) + return false; + //g_object_unref(proxy); + + signal_list_bind_userdata *cu = malloc(sizeof(signal_list_bind_userdata)); + cu->self = self; + cu->callback = callback; + cu->userdata = userdata; + g_signal_connect(proxy, "g-signal", G_CALLBACK(dbus_signal_list_bind), cu); + return true; + } else { + return false; + } +} + +bool gsr_global_shortcuts_subscribe_activated_signal(gsr_global_shortcuts *self, gsr_deactivated_callback deactivated_callback, gsr_shortcut_callback shortcut_changed_callback, void *userdata) { + if(!self->session_created) + return false; + + signal_userdata *cu = malloc(sizeof(signal_userdata)); + cu->self = self; + cu->deactivated_callback = deactivated_callback; + cu->shortcut_changed_callback = shortcut_changed_callback; + cu->userdata = userdata; + g_dbus_connection_signal_subscribe(self->gdbus_con, "org.freedesktop.portal.Desktop", "org.freedesktop.portal.GlobalShortcuts", NULL, "/org/freedesktop/portal/desktop", NULL, G_DBUS_SIGNAL_FLAGS_NONE, signal_callback, cu, free); + return true; +} diff --git a/src/global_shortcuts.h b/src/global_shortcuts.h new file mode 100644 index 0000000..087c177 --- /dev/null +++ b/src/global_shortcuts.h @@ -0,0 +1,41 @@ +#ifndef GLOBAL_SHORTCUTS_H +#define GLOBAL_SHORTCUTS_H + +/* Global shortcuts via desktop portal */ + +#include <stdbool.h> +#include <gio/gio.h> + +#define DBUS_RANDOM_STR_SIZE 16 + +typedef struct { + const char *id; + const char *trigger_description; +} gsr_shortcut; + +typedef struct { + const char *description; + gsr_shortcut shortcut; +} gsr_bind_shortcut; + +typedef void (*gsr_init_callback)(bool success, void *userdata); +typedef void (*gsr_shortcut_callback)(gsr_shortcut shortcut, void *userdata); +typedef void (*gsr_deactivated_callback)(const char *id, void *userdata); + +typedef struct { + GDBusConnection *gdbus_con; + char *session_handle; + bool session_created; + char random_str[DBUS_RANDOM_STR_SIZE + 1]; + unsigned int handle_counter; +} gsr_global_shortcuts; + +bool gsr_global_shortcuts_init(gsr_global_shortcuts *self, gsr_init_callback callback, void *userdata); +void gsr_global_shortcuts_deinit(gsr_global_shortcuts *self); + +bool gsr_global_shortcuts_list_shortcuts(gsr_global_shortcuts *self, gsr_shortcut_callback callback, void *userdata); +bool gsr_global_shortcuts_bind_shortcuts(gsr_global_shortcuts *self, const gsr_bind_shortcut *shortcuts, int num_shortcuts, gsr_shortcut_callback callback, void *userdata); + +bool gsr_global_shortcuts_subscribe_activated_signal(gsr_global_shortcuts *self, gsr_deactivated_callback deactivated_callback, gsr_shortcut_callback shortcut_changed_callback, void *userdata); + +#endif /* GLOBAL_SHORTCUTS_H */ diff --git a/src/library_loader.c b/src/library_loader.c deleted file mode 100644 index fed1fe5..0000000 --- a/src/library_loader.c +++ /dev/null @@ -1,34 +0,0 @@ -#include "library_loader.h" - -#include <dlfcn.h> -#include <stdbool.h> -#include <stdio.h> - -void* dlsym_print_fail(void *handle, const char *name, bool required) { - dlerror(); - void *sym = dlsym(handle, name); - char *err_str = dlerror(); - - if(!sym) - fprintf(stderr, "%s: dlsym(handle, \"%s\") failed, error: %s\n", required ? "error" : "warning", name, err_str ? err_str : "(null)"); - - return sym; -} - -/* |dlsyms| should be null terminated */ -bool dlsym_load_list(void *handle, const dlsym_assign *dlsyms) { - bool success = true; - for(int i = 0; dlsyms[i].func; ++i) { - *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, true); - if(!*dlsyms[i].func) - success = false; - } - return success; -} - -/* |dlsyms| should be null terminated */ -void dlsym_load_list_optional(void *handle, const dlsym_assign *dlsyms) { - for(int i = 0; dlsyms[i].func; ++i) { - *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, false); - } -} diff --git a/src/library_loader.h b/src/library_loader.h deleted file mode 100644 index 47bc9f0..0000000 --- a/src/library_loader.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef GSR_LIBRARY_LOADER_H -#define GSR_LIBRARY_LOADER_H - -#include <stdbool.h> - -typedef struct { - void **func; - const char *name; -} dlsym_assign; - -void* dlsym_print_fail(void *handle, const char *name, bool required); -/* |dlsyms| should be null terminated */ -bool dlsym_load_list(void *handle, const dlsym_assign *dlsyms); -/* |dlsyms| should be null terminated */ -void dlsym_load_list_optional(void *handle, const dlsym_assign *dlsyms); - -#endif /* GSR_LIBRARY_LOADER_H */ diff --git a/src/main.cpp b/src/main.cpp index f5fbdbd..91f7c3d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,13 +1,15 @@ #include "config.hpp" +extern "C" { +#include "global_shortcuts.h" +} #include <gtk/gtk.h> #include <gdk/gdkx.h> +#include <gdk/gdkwayland.h> #include <X11/Xlib.h> #include <X11/Xatom.h> #include <X11/cursorfont.h> -#include <X11/extensions/Xrandr.h> #include <assert.h> #include <string> -#include <pulse/pulseaudio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/prctl.h> @@ -16,13 +18,19 @@ #include <dlfcn.h> #include <functional> #include <vector> -extern "C" { -#include "egl.h" -} -#include <xf86drmMode.h> -#include <xf86drm.h> #include <libayatana-appindicator/app-indicator.h> +#define GSR_CURRENT_GLOBAL_HOTKEYS_CODE_VERSION 6 + +#ifndef GSR_VERSION +#define GSR_VERSION "unknown" +#endif + +// Start/stop recording also means start/stop streaming and start/stop replay +#define SHORTCUT_ID_START_STOP_RECORDING "gpu_screen_recorder_start_stop_recording" +#define SHORTCUT_ID_PAUSE_UNPAUSE_RECORDING "gpu_screen_recorder_pause_unpause_recording" +#define SHORTCUT_ID_SAVE_REPLAY "gpu_screen_recorder_save_replay" + typedef struct { Display *display; GtkApplication *app; @@ -45,14 +53,17 @@ static SelectWindowUserdata select_window_userdata; static PageNavigationUserdata page_navigation_userdata; static Cursor crosshair_cursor; static GtkSpinButton *fps_entry; -static GtkLabel *area_size_label; +static GtkSpinButton *video_bitrate_entry; static GtkGrid *area_size_grid; +static GtkWidget *change_video_resolution_button; +static GtkGrid *video_resolution_grid; static GtkSpinButton *area_width_entry; static GtkSpinButton *area_height_entry; +static GtkSpinButton *video_width_entry; +static GtkSpinButton *video_height_entry; static GtkComboBox *record_area_selection_menu; static GtkTreeModel *record_area_selection_model; static GtkComboBoxText *quality_input_menu; -static GtkComboBoxText *video_codec_input_menu; static GtkComboBoxText *audio_codec_input_menu; static GtkComboBoxText *color_range_input_menu; static GtkComboBoxText *framerate_mode_input_menu; @@ -60,6 +71,8 @@ static GtkComboBoxText *stream_service_input_menu; static GtkComboBoxText *record_container; static GtkComboBoxText *replay_container; static GtkComboBoxText *custom_stream_container; +static GtkComboBox *video_codec_selection_menu; +static GtkTreeModel *video_codec_selection_model; static GtkLabel *stream_key_label; static GtkButton *record_file_chooser_button; static GtkButton *replay_file_chooser_button; @@ -79,20 +92,28 @@ static GtkEntry *twitch_stream_id_entry; static GtkEntry *custom_stream_url_entry; static GtkSpinButton *replay_time_entry; static GtkButton *select_window_button; -static GtkWidget *audio_input_used_list; -static GtkWidget *add_audio_input_button; -static GtkWidget *record_hotkey_button; +static GtkBox *audio_devices_items_box; +static GtkWidget *record_start_stop_hotkey_button; static GtkWidget *pause_unpause_hotkey_button; static GtkWidget *replay_start_stop_hotkey_button; static GtkWidget *replay_save_hotkey_button; -static GtkWidget *streaming_hotkey_button; -static GtkWidget *merge_audio_tracks_button; -static GtkWidget *show_notification_button; +static GtkWidget *streaming_start_stop_hotkey_button; +static GtkWidget *record_app_audio_inverted_button; +static GtkWidget *split_audio_button; +static GtkFrame *notifications_frame; +static GtkWidget *show_recording_started_notification_button; +static GtkWidget *show_recording_stopped_notification_button; +static GtkWidget *show_recording_saved_notification_button; static GtkWidget *record_cursor_button; +static GtkWidget *restore_portal_session_button; +static GtkGrid *audio_devices_grid; +static GtkWidget *add_application_audio_button; +static GtkWidget *add_custom_application_audio_button; static GtkGrid *video_codec_grid; static GtkGrid *audio_codec_grid; static GtkGrid *color_range_grid; static GtkGrid *framerate_mode_grid; +static GtkGrid *video_bitrate_grid; static GtkComboBoxText *view_combo_box; static GtkGrid *overclock_grid; static GtkWidget *overclock_button; @@ -112,6 +133,12 @@ static GtkWidget *pause_recording_menu_item; static GtkWidget *start_stop_replay_menu_item; static GtkWidget *save_replay_menu_item; static GtkWidget *hide_window_when_recording_menu_item; +static GtkGrid *recording_hotkeys_grid; +static GtkGrid *replay_hotkeys_grid; +static GtkGrid *streaming_hotkeys_grid; +static GtkWidget *recording_hotkeys_not_supported_label; +static GtkWidget *replay_hotkeys_not_supported_label; +static GtkWidget *streaming_hotkeys_not_supported_label; static double record_start_time_sec = 0.0; static double pause_start_sec = 0.0; @@ -126,36 +153,31 @@ static bool paused = false; static bool streaming = false; static pid_t gpu_screen_recorder_process = -1; static int prev_exit_status = -1; +static bool config_empty = false; static Config config; static std::string record_file_current_filename; static bool nvfbc_installed = false; static Display *dpy = NULL; -static bool wayland = false; static bool flatpak = false; -static gsr_egl egl; static bool showing_notification = false; static double notification_timeout_seconds = 0.0; static double notification_start_seconds = 0.0; static AppIndicator *app_indicator; -static const char *tray_idle_icon_name = "com.dec05eba.gpu_screen_recorder.tray-idle"; -static const char *tray_recording_icon_name = "com.dec05eba.gpu_screen_recorder.tray-recording"; -static const char *tray_paused_icon_name = "com.dec05eba.gpu_screen_recorder.tray-paused"; + +static gsr_global_shortcuts global_shortcuts; +static bool global_shortcuts_initialized = false; +static bool global_shortcuts_received = false; struct AudioInput { std::string name; std::string description; }; -struct PulseAudioServerInfo { - std::string default_sink_name; - std::string default_source_name; -}; - static std::vector<AudioInput> audio_inputs; -static PulseAudioServerInfo pa_default_sources; +static std::vector<std::string> application_audio; enum class HotkeyMode { NoAction, @@ -165,30 +187,116 @@ enum class HotkeyMode { static HotkeyMode hotkey_mode = HotkeyMode::NoAction; +typedef gboolean (*hotkey_trigger_handler)(GtkButton *button, gpointer userdata); struct Hotkey { uint32_t modkey_mask = 0; KeySym keysym = None; GtkWidget *hotkey_entry = nullptr; GtkWidget *hotkey_active_label = nullptr; + ConfigHotkey *config = nullptr; + bool grab_success = false; + GtkWidget *page = nullptr; + hotkey_trigger_handler trigger_handler = nullptr; + GtkButton *associated_button = nullptr; + const char *shortcut_id = nullptr; }; static Hotkey *current_hotkey = nullptr; static Hotkey pressed_hotkey; static Hotkey latest_hotkey; -static Hotkey streaming_hotkey; -static Hotkey record_hotkey; +static Hotkey streaming_start_stop_hotkey; +static Hotkey record_start_stop_hotkey; static Hotkey pause_unpause_hotkey; static Hotkey replay_start_stop_hotkey; static Hotkey replay_save_hotkey; +static Hotkey *hotkeys[] = { + &streaming_start_stop_hotkey, + &record_start_stop_hotkey, + &pause_unpause_hotkey, + &replay_start_stop_hotkey, + &replay_save_hotkey +}; +static int num_hotkeys = 5; + struct SupportedVideoCodecs { - bool h264; - bool hevc; - bool av1; + bool h264 = false; + bool h264_software = false; + bool hevc = false; + bool hevc_hdr = false; + bool hevc_10bit = false; + bool av1 = false; + bool av1_hdr = false; + bool av1_10bit = false; + bool vp8 = false; + bool vp9 = false; }; -static SupportedVideoCodecs supported_video_codecs; -static int supported_video_codecs_exit_status = 0; +struct vec2i { + int x = 0; + int y = 0; +}; + +struct GsrMonitor { + std::string name; + vec2i size; +}; + +struct SupportedCaptureOptions { + bool window = false; + bool focused = false; + bool portal = false; + std::vector<GsrMonitor> monitors; +}; + +enum class DisplayServer { + UNKNOWN, + X11, + WAYLAND +}; + +struct SystemInfo { + DisplayServer display_server = DisplayServer::UNKNOWN; + bool supports_app_audio = false; + bool is_steam_deck = false; +}; + +enum class GpuVendor { + UNKNOWN, + AMD, + INTEL, + NVIDIA, + BROADCOM +}; + +struct GpuInfo { + GpuVendor vendor = GpuVendor::UNKNOWN; +}; + +struct GsrInfo { + SystemInfo system_info; + GpuInfo gpu_info; + SupportedVideoCodecs supported_video_codecs; + SupportedCaptureOptions supported_capture_options; +}; + +static GsrInfo gsr_info; + +enum class GsrInfoExitStatus { + OK, + FAILED_TO_RUN_COMMAND, + OPENGL_FAILED, + NO_DRM_CARD +}; + +static GsrInfoExitStatus gsr_info_exit_status; + +enum class WaylandCompositor { + UNKNOWN, + HYPRLAND, + KDE // kwin +}; +static WaylandCompositor wayland_compositor = WaylandCompositor::UNKNOWN; struct Container { const char *container_name; @@ -204,23 +312,27 @@ static const Container supported_containers[] = { { "hls", "m3u8" } }; -struct AudioRow { - GtkWidget *row; - GtkComboBoxText *input_list; -}; - -typedef enum { - GPU_VENDOR_AMD, - GPU_VENDOR_INTEL, - GPU_VENDOR_NVIDIA -} gpu_vendor; +// Dumb hacks below!! why dont these fking paths work outside flatpak.. except in kde. TODO: fix this! +static const char* get_tray_idle_icon_name() { + if(flatpak) + return "com.dec05eba.gpu_screen_recorder.tray-idle"; + else + return "/usr/share/icons/hicolor/32x32/status/com.dec05eba.gpu_screen_recorder.tray-idle.png"; +} -typedef struct { - gpu_vendor vendor; - int gpu_version; /* 0 if unknown */ -} gpu_info; +static const char* get_tray_recording_icon_name() { + if(flatpak) + return "com.dec05eba.gpu_screen_recorder.tray-recording"; + else + return "/usr/share/icons/hicolor/32x32/status/com.dec05eba.gpu_screen_recorder.tray-recording.png"; +} -static gpu_info gpu_inf; +static const char* get_tray_paused_icon_name() { + if(flatpak) + return "com.dec05eba.gpu_screen_recorder.tray-paused"; + else + return "/usr/share/icons/hicolor/32x32/status/com.dec05eba.gpu_screen_recorder.tray-paused.png"; +} static bool is_program_installed(const StringView program_name) { const char *path = getenv("PATH"); @@ -293,6 +405,19 @@ static void hide_window_when_recording_systray_callback(GtkMenuItem*, gpointer) config.main_config.hide_window_when_recording = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item)); } +static void set_label_selectable(gpointer data, gpointer) { + GtkWidget *widget = GTK_WIDGET(data); + if(GTK_IS_LABEL(widget)) + gtk_label_set_selectable(GTK_LABEL(widget), true); +} + +static void set_dialog_selectable(GtkWidget *dialog) { + GtkWidget *area = gtk_message_dialog_get_message_area(GTK_MESSAGE_DIALOG(dialog)); + GList *children = gtk_container_get_children(GTK_CONTAINER(area)); + g_list_foreach(children, set_label_selectable, nullptr); + g_list_free(children); +} + static void start_stop_streaming_menu_item_systray_callback(GtkMenuItem*, gpointer userdata); static void start_stop_recording_systray_callback(GtkMenuItem*, gpointer userdata); static void pause_recording_systray_callback(GtkMenuItem*, gpointer userdata); @@ -391,7 +516,7 @@ static GtkMenuShell* create_systray_menu(GtkApplication *app, SystrayPage systra static void setup_systray(GtkApplication *app) { app_indicator = app_indicator_new("com.dec05eba.gpu_screen_recorder", "", APP_INDICATOR_CATEGORY_APPLICATION_STATUS); - app_indicator_set_icon_full(app_indicator, tray_idle_icon_name, "Idle"); + app_indicator_set_icon_full(app_indicator, get_tray_idle_icon_name(), "Idle"); // This triggers Gdk assert: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed, // dont know why but it works anyways app_indicator_set_title(app_indicator, "GPU Screen Recorder"); @@ -399,189 +524,119 @@ static void setup_systray(GtkApplication *app) { app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(app, SystrayPage::FRONT))); } -static void pa_state_cb(pa_context *c, void *userdata) { - pa_context_state state = pa_context_get_state(c); - int *pa_ready = (int*)userdata; - switch(state) { - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - default: - break; - case PA_CONTEXT_FAILED: - case PA_CONTEXT_TERMINATED: - *pa_ready = 2; - break; - case PA_CONTEXT_READY: - *pa_ready = 1; - break; - } +static AudioInput parse_audio_device_line(const std::string &line) { + AudioInput audio_input; + const size_t space_index = line.find('|'); + if(space_index == std::string::npos) + return audio_input; + + const StringView audio_input_name = {line.c_str(), space_index}; + const StringView audio_input_description = {line.c_str() + space_index + 1, line.size() - (space_index + 1)}; + audio_input.name.assign(audio_input_name.str, audio_input_name.size); + audio_input.description.assign(audio_input_description.str, audio_input_description.size); + return audio_input; } -static void pa_sourcelist_cb(pa_context*, const pa_source_info *source_info, int eol, void *userdata) { - if(eol > 0) - return; - - std::vector<AudioInput> *inputs = (std::vector<AudioInput>*)userdata; - inputs->push_back({ source_info->name, source_info->description }); -} - -static std::vector<AudioInput> get_pulseaudio_inputs() { +static std::vector<AudioInput> get_audio_devices() { std::vector<AudioInput> inputs; - pa_mainloop *main_loop = pa_mainloop_new(); - - pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder-gtk"); - pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL); - int state = 0; - int pa_ready = 0; - pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); - pa_operation *pa_op = NULL; - - for(;;) { - // Not ready - if(pa_ready == 0) { - pa_mainloop_iterate(main_loop, 1, NULL); - continue; - } - - switch(state) { - case 0: { - pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, &inputs); - ++state; - break; - } - } - - // Couldn't get connection to the server - if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) { - if(pa_op) - pa_operation_unref(pa_op); - pa_context_disconnect(ctx); - pa_context_unref(ctx); - pa_mainloop_free(main_loop); - return inputs; - } + FILE *f = popen("gpu-screen-recorder --list-audio-devices", "r"); + if(!f) { + fprintf(stderr, "error: 'gpu-screen-recorder --list-audio-devices' failed\n"); + return inputs; + } - pa_mainloop_iterate(main_loop, 1, NULL); + char output[16384]; + ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f); + if(bytes_read < 0 || ferror(f)) { + fprintf(stderr, "error: failed to read 'gpu-screen-recorder --list-audio-devices' output\n"); + pclose(f); + return inputs; } + output[bytes_read] = '\0'; - pa_mainloop_free(main_loop); - return {}; -} + string_split_char(output, '\n', [&](StringView line) { + const std::string line_str(line.str, line.size); + inputs.push_back(parse_audio_device_line(line_str)); + return true; + }); -static void server_info_callback(pa_context*, const pa_server_info *server_info, void *userdata) { - PulseAudioServerInfo *u = (PulseAudioServerInfo*)userdata; - if(server_info->default_sink_name) - u->default_sink_name = std::string(server_info->default_sink_name) + ".monitor"; - if(server_info->default_source_name) - u->default_source_name = server_info->default_source_name; + return inputs; } -static PulseAudioServerInfo get_pulseaudio_default_inputs() { - PulseAudioServerInfo server_info; - pa_mainloop *main_loop = pa_mainloop_new(); +static std::vector<std::string> get_application_audio() { + std::vector<std::string> application_audio; - pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder-gtk"); - pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL); - int state = 0; - int pa_ready = 0; - pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); + FILE *f = popen("gpu-screen-recorder --list-application-audio", "r"); + if(!f) { + fprintf(stderr, "error: 'gpu-screen-recorder --list-application-audio' failed\n"); + return application_audio; + } - pa_operation *pa_op = NULL; + char output[16384]; + ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f); + if(bytes_read < 0 || ferror(f)) { + fprintf(stderr, "error: failed to read 'gpu-screen-recorder --list-application-audio' output\n"); + pclose(f); + return application_audio; + } + output[bytes_read] = '\0'; - for(;;) { - // Not ready - if(pa_ready == 0) { - pa_mainloop_iterate(main_loop, 1, NULL); - continue; - } + string_split_char(output, '\n', [&](StringView line) { + std::string line_str(line.str, line.size); + application_audio.emplace_back(std::move(line_str)); + return true; + }); - switch(state) { - case 0: { - pa_op = pa_context_get_server_info(ctx, server_info_callback, &server_info); - ++state; - break; - } - } + return application_audio; +} - // Couldn't get connection to the server - if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) { - if(pa_op) - pa_operation_unref(pa_op); - pa_context_disconnect(ctx); - pa_context_unref(ctx); - pa_mainloop_free(main_loop); - return server_info; - } +static bool is_video_capture_option_enabled(const char *str) { + bool enabled = true; - pa_mainloop_iterate(main_loop, 1, NULL); - } + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) + enabled = strcmp(str, "window") != 0 && strcmp(str, "focused") != 0; - pa_mainloop_free(main_loop); - return server_info; -} + if(strcmp(str, "portal") == 0 && !gsr_info.supported_capture_options.portal) + enabled = false; -static void used_audio_input_loop_callback(GtkWidget *row, gpointer userdata) { - const AudioRow *audio_row = (AudioRow*)g_object_get_data(G_OBJECT(row), "audio-row"); - std::function<void(const AudioRow*)> &callback = *(std::function<void(const AudioRow*)>*)userdata; - callback(audio_row); + return enabled; } -static void for_each_used_audio_input(GtkListBox *list_box, std::function<void(const AudioRow*)> callback) { - gtk_container_foreach(GTK_CONTAINER(list_box), used_audio_input_loop_callback, &callback); -} +static bool is_video_codec_enabled(const char *str) { + bool enabled = true; -static void drag_begin (GtkWidget *widget, GdkDragContext *context, gpointer) { - GtkAllocation alloc; - int x, y; - double sx, sy; + if(strcmp(str, "h264") == 0 && !gsr_info.supported_video_codecs.h264) + enabled = false; - GtkWidget *row = gtk_widget_get_ancestor(widget, GTK_TYPE_LIST_BOX_ROW); - gtk_widget_get_allocation(row, &alloc); - cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, alloc.width, alloc.height); - cairo_t *cr = cairo_create(surface); + if(strcmp(str, "h264_software") == 0 && !gsr_info.supported_video_codecs.h264_software) + enabled = false; - gtk_style_context_add_class(gtk_widget_get_style_context (row), "drag-icon"); - gtk_widget_draw(row, cr); - gtk_style_context_remove_class(gtk_widget_get_style_context (row), "drag-icon"); + if(strcmp(str, "hevc") == 0 && !gsr_info.supported_video_codecs.hevc) + enabled = false; - gtk_widget_translate_coordinates(widget, row, 0, 0, &x, &y); - cairo_surface_get_device_scale(surface, &sx, &sy); - cairo_surface_set_device_offset(surface, -x * sx, -y * sy); - gtk_drag_set_icon_surface(context, surface); + if(strcmp(str, "hevc_hdr") == 0 && !gsr_info.supported_video_codecs.hevc_hdr) + enabled = false; - cairo_destroy(cr); - cairo_surface_destroy(surface); -} + if(strcmp(str, "hevc_10bit") == 0 && !gsr_info.supported_video_codecs.hevc_10bit) + enabled = false; -static void drag_data_get(GtkWidget *widget, GdkDragContext*, GtkSelectionData *selection_data, guint, guint, gpointer) { - gtk_selection_data_set(selection_data, gdk_atom_intern_static_string("GTK_LIST_BOX_ROW"), - 32, - (const guchar *)&widget, - sizeof(gpointer)); -} + if(strcmp(str, "av1") == 0 && !gsr_info.supported_video_codecs.av1) + enabled = false; -static void drag_data_received(GtkWidget *widget, GdkDragContext*, - gint, gint, - GtkSelectionData *selection_data, - guint, guint32, gpointer) -{ - GtkWidget *target = widget; + if(strcmp(str, "av1_hdr") == 0 && !gsr_info.supported_video_codecs.av1_hdr) + enabled = false; - int pos = gtk_list_box_row_get_index(GTK_LIST_BOX_ROW (target)); - GtkWidget *row = *(GtkWidget**)gtk_selection_data_get_data(selection_data); - GtkWidget *source = gtk_widget_get_ancestor(row, GTK_TYPE_LIST_BOX_ROW); + if(strcmp(str, "av1_10bit") == 0 && !gsr_info.supported_video_codecs.av1_10bit) + enabled = false; - if (source == target) - return; + if(strcmp(str, "vp8") == 0 && !gsr_info.supported_video_codecs.vp8) + enabled = false; - GtkWidget *list_box = gtk_widget_get_parent(source); - g_object_ref(source); - gtk_container_remove(GTK_CONTAINER(list_box), source); - gtk_list_box_insert(GTK_LIST_BOX(list_box), source, pos); - g_object_unref(source); + if(strcmp(str, "vp9") == 0 && !gsr_info.supported_video_codecs.vp9) + enabled = false; + + return enabled; } static std::string record_area_selection_menu_get_active_id() { @@ -598,6 +653,9 @@ static std::string record_area_selection_menu_get_active_id() { } static void record_area_selection_menu_set_active_id(const gchar *id) { + if(!is_video_capture_option_enabled(id)) + return; + GtkTreeIter iter; if(!gtk_tree_model_get_iter_first(record_area_selection_model, &iter)) return; @@ -615,8 +673,42 @@ static void record_area_selection_menu_set_active_id(const gchar *id) { } while(gtk_tree_model_iter_next(record_area_selection_model, &iter)); } +static std::string video_codec_selection_menu_get_active_id() { + std::string id_str; + GtkTreeIter iter; + if(!gtk_combo_box_get_active_iter(video_codec_selection_menu, &iter)) + return id_str; + + gchar *id; + gtk_tree_model_get(video_codec_selection_model, &iter, 1, &id, -1); + id_str = id; + g_free(id); + return id_str; +} + +static void video_codec_selection_menu_set_active_id(const gchar *id) { + if(!is_video_codec_enabled(id)) + return; + + GtkTreeIter iter; + if(!gtk_tree_model_get_iter_first(video_codec_selection_model, &iter)) + return; + + do { + gchar *row_id = nullptr; + gtk_tree_model_get(video_codec_selection_model, &iter, 1, &row_id, -1); + + const bool found_row = strcmp(row_id, id) == 0; + g_free(row_id); + if(found_row) { + gtk_combo_box_set_active_iter(video_codec_selection_menu, &iter); + break; + } + } while(gtk_tree_model_iter_next(video_codec_selection_model, &iter)); +} + static void enable_stream_record_button_if_info_filled() { - if(!wayland) { + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { const std::string selected_window_area = record_area_selection_menu_get_active_id(); if(strcmp(selected_window_area.c_str(), "window") == 0 && select_window_userdata.selected_window == None) { gtk_widget_set_sensitive(GTK_WIDGET(replay_button), false); @@ -631,55 +723,8 @@ static void enable_stream_record_button_if_info_filled() { gtk_widget_set_sensitive(GTK_WIDGET(stream_button), true); } -static GtkWidget* create_used_audio_input_row(void) { - char entry_name[] = "GTK_LIST_BOX_ROW"; - const GtkTargetEntry entries[] = { - { entry_name, GTK_TARGET_SAME_APP, 0 } - }; - - GtkWidget *row = gtk_list_box_row_new(); - - GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - gtk_container_add(GTK_CONTAINER(row), box); - - GtkWidget *handle = gtk_event_box_new(); - GtkWidget *image = gtk_image_new_from_icon_name("open-menu-symbolic", GTK_ICON_SIZE_MENU); - gtk_container_add(GTK_CONTAINER(handle), image); - gtk_container_add(GTK_CONTAINER(box), handle); - - GtkComboBoxText *input_list = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); - for(const auto &audio_input : audio_inputs) { - gtk_combo_box_text_append(input_list, audio_input.name.c_str(), audio_input.description.c_str()); - } - gtk_widget_set_hexpand(GTK_WIDGET(input_list), true); - gtk_combo_box_set_active(GTK_COMBO_BOX(input_list), 0); - //gtk_combo_box_set_active_id(GTK_COMBO_BOX(combo), id); - gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(input_list)); - - GtkWidget *remove_button = gtk_button_new_with_label("Remove"); - gtk_widget_set_halign(remove_button, GTK_ALIGN_END); - gtk_container_add(GTK_CONTAINER(box), remove_button); - - gtk_drag_source_set(handle, GDK_BUTTON1_MASK, entries, 1, GDK_ACTION_MOVE); - g_signal_connect(handle, "drag-begin", G_CALLBACK(drag_begin), NULL); - g_signal_connect(handle, "drag-data-get", G_CALLBACK(drag_data_get), NULL); - - gtk_drag_dest_set(row, GTK_DEST_DEFAULT_ALL, entries, 1, GDK_ACTION_MOVE); - g_signal_connect(row, "drag-data-received", G_CALLBACK(drag_data_received), NULL); - - AudioRow *audio_row = new AudioRow(); - audio_row->row = row; - audio_row->input_list = input_list; - g_object_set_data(G_OBJECT(row), "audio-row", audio_row); - - g_signal_connect(remove_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer userdata){ - AudioRow *audio_row = (AudioRow*)userdata; - gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(audio_row->row)), audio_row->row); - delete audio_row; - return true; - }), audio_row); - - return row; +static gboolean scroll_event_ignore(GtkWidget*, GdkEvent*, void*) { + return TRUE; } // Return true from |callback_func| to continue to the next row @@ -727,6 +772,108 @@ static gint combo_box_text_get_row_by_label(GtkComboBox *combo_box, const char * return found_index; } +static GtkWidget* create_audio_device_combo_box_row(const std::string &selected_row_text) { + GtkGrid *grid = GTK_GRID(gtk_grid_new()); + g_object_set_data(G_OBJECT(grid), "audio-track-type", (void*)"device"); + gtk_grid_set_column_spacing(grid, 10); + gtk_widget_set_hexpand(GTK_WIDGET(grid), true); + + GtkLabel *label = GTK_LABEL(gtk_label_new("Device:")); + gtk_grid_attach(grid, GTK_WIDGET(label), 0, 0, 1, 1); + + GtkComboBoxText *audio_device_combo_box = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(audio_device_combo_box, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + for(const auto &audio_input : audio_inputs) { + gtk_combo_box_text_append(audio_device_combo_box, audio_input.name.c_str(), audio_input.description.c_str()); + } + + if(!audio_inputs.empty() && selected_row_text.empty()) { + gtk_combo_box_set_active(GTK_COMBO_BOX(audio_device_combo_box), 0); + } else if(!selected_row_text.empty()) { + std::string audio_id; + const gint target_combo_box_index = combo_box_text_get_row_by_label(GTK_COMBO_BOX(audio_device_combo_box), selected_row_text.c_str(), audio_id); + if(target_combo_box_index != -1) + gtk_combo_box_set_active(GTK_COMBO_BOX(audio_device_combo_box), target_combo_box_index); + else if(!audio_inputs.empty()) + gtk_combo_box_set_active(GTK_COMBO_BOX(audio_device_combo_box), 0); + } + + gtk_widget_set_hexpand(GTK_WIDGET(audio_device_combo_box), true); + gtk_grid_attach(grid, GTK_WIDGET(audio_device_combo_box), 1, 0, 1, 1); + + GtkButton *remove_button = GTK_BUTTON(gtk_button_new_with_label("Remove")); + gtk_grid_attach(grid, GTK_WIDGET(remove_button), 2, 0, 1, 1); + + g_signal_connect(remove_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer userdata){ + GtkGrid *grid = (GtkGrid*)userdata; + gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(GTK_WIDGET(grid))), GTK_WIDGET(grid)); + return true; + }), grid); + + return GTK_WIDGET(grid); +} + +static GtkWidget* create_application_audio_combo_box_row(const std::string &selected_row_id) { + GtkGrid *grid = GTK_GRID(gtk_grid_new()); + g_object_set_data(G_OBJECT(grid), "audio-track-type", (void*)"app"); + gtk_grid_set_column_spacing(grid, 10); + gtk_widget_set_hexpand(GTK_WIDGET(grid), true); + + GtkLabel *label = GTK_LABEL(gtk_label_new("Application:")); + gtk_grid_attach(grid, GTK_WIDGET(label), 0, 0, 1, 1); + + GtkComboBoxText *application_audio_combo_box = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(application_audio_combo_box, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + for(const std::string &app_audio : application_audio) { + gtk_combo_box_text_append(application_audio_combo_box, app_audio.c_str(), app_audio.c_str()); + } + + if(!application_audio.empty() && selected_row_id.empty()) + gtk_combo_box_set_active(GTK_COMBO_BOX(application_audio_combo_box), 0); + else if(!selected_row_id.empty()) + gtk_combo_box_set_active_id(GTK_COMBO_BOX(application_audio_combo_box), selected_row_id.c_str()); + + gtk_widget_set_hexpand(GTK_WIDGET(application_audio_combo_box), true); + gtk_grid_attach(grid, GTK_WIDGET(application_audio_combo_box), 1, 0, 1, 1); + + GtkButton *remove_button = GTK_BUTTON(gtk_button_new_with_label("Remove")); + gtk_grid_attach(grid, GTK_WIDGET(remove_button), 2, 0, 1, 1); + + g_signal_connect(remove_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer userdata){ + GtkGrid *grid = (GtkGrid*)userdata; + gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(GTK_WIDGET(grid))), GTK_WIDGET(grid)); + return true; + }), grid); + + return GTK_WIDGET(grid); +} + +static GtkWidget* create_application_audio_custom_row(const std::string &text) { + GtkGrid *grid = GTK_GRID(gtk_grid_new()); + g_object_set_data(G_OBJECT(grid), "audio-track-type", (void*)"app-custom"); + gtk_grid_set_column_spacing(grid, 10); + gtk_widget_set_hexpand(GTK_WIDGET(grid), true); + + GtkLabel *label = GTK_LABEL(gtk_label_new("Application:")); + gtk_grid_attach(grid, GTK_WIDGET(label), 0, 0, 1, 1); + + GtkEntry *application_audio_entry = GTK_ENTRY(gtk_entry_new()); + gtk_widget_set_hexpand(GTK_WIDGET(application_audio_entry), true); + gtk_entry_set_text(application_audio_entry, text.c_str()); + gtk_grid_attach(grid, GTK_WIDGET(application_audio_entry), 1, 0, 1, 1); + + GtkButton *remove_button = GTK_BUTTON(gtk_button_new_with_label("Remove")); + gtk_grid_attach(grid, GTK_WIDGET(remove_button), 2, 0, 1, 1); + + g_signal_connect(remove_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer userdata){ + GtkGrid *grid = (GtkGrid*)userdata; + gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(GTK_WIDGET(grid))), GTK_WIDGET(grid)); + return true; + }), grid); + + return GTK_WIDGET(grid); +} + static bool is_directory(const char *filepath) { struct stat file_stat; memset(&file_stat, 0, sizeof(file_stat)); @@ -746,337 +893,106 @@ static std::string get_date_str() { static void save_configs() { config.main_config.record_area_option = record_area_selection_menu_get_active_id(); - if(!wayland) { + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { config.main_config.record_area_width = gtk_spin_button_get_value_as_int(area_width_entry); config.main_config.record_area_height = gtk_spin_button_get_value_as_int(area_height_entry); } + config.main_config.video_width = gtk_spin_button_get_value_as_int(video_width_entry); + config.main_config.video_height = gtk_spin_button_get_value_as_int(video_height_entry); config.main_config.fps = gtk_spin_button_get_value_as_int(fps_entry); - config.main_config.merge_audio_tracks = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button)); + config.main_config.video_bitrate = gtk_spin_button_get_value_as_int(video_bitrate_entry); + config.main_config.merge_audio_tracks = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(split_audio_button)); + config.main_config.record_app_audio_inverted = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_app_audio_inverted_button)); + config.main_config.change_video_resolution = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(change_video_resolution_button)); config.main_config.audio_input.clear(); - for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [](const AudioRow *audio_row) { - config.main_config.audio_input.push_back(gtk_combo_box_text_get_active_text(audio_row->input_list)); - }); + gtk_container_foreach(GTK_CONTAINER(audio_devices_items_box), [](GtkWidget *widget, gpointer) { + const char *audio_track_type = (const char*)g_object_get_data(G_OBJECT(widget), "audio-track-type"); + GtkWidget *row_item_widget = gtk_grid_get_child_at(GTK_GRID(widget), 1, 0); + if(strcmp(audio_track_type, "device") == 0) { + std::string audio_input_name = "device:"; + audio_input_name += gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(row_item_widget)); + config.main_config.audio_input.push_back(std::move(audio_input_name)); + } else if(strcmp(audio_track_type, "app") == 0) { + std::string audio_input_name = "app:"; + audio_input_name += gtk_combo_box_get_active_id(GTK_COMBO_BOX(row_item_widget)); + config.main_config.audio_input.push_back(std::move(audio_input_name)); + } else if(strcmp(audio_track_type, "app-custom") == 0) { + std::string audio_input_name = "app:"; + audio_input_name += gtk_entry_get_text(GTK_ENTRY(row_item_widget)); + config.main_config.audio_input.push_back(std::move(audio_input_name)); + } + }, nullptr); + config.main_config.color_range = gtk_combo_box_get_active_id(GTK_COMBO_BOX(color_range_input_menu)); config.main_config.quality = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); - config.main_config.codec = gtk_combo_box_get_active_id(GTK_COMBO_BOX(video_codec_input_menu)); + config.main_config.codec = video_codec_selection_menu_get_active_id(); config.main_config.audio_codec = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_codec_input_menu)); config.main_config.framerate_mode = gtk_combo_box_get_active_id(GTK_COMBO_BOX(framerate_mode_input_menu)); config.main_config.advanced_view = strcmp(gtk_combo_box_get_active_id(GTK_COMBO_BOX(view_combo_box)), "advanced") == 0; config.main_config.overclock = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(overclock_button)); - config.main_config.show_notifications = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_notification_button)); + config.main_config.show_recording_started_notifications = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button)); + config.main_config.show_recording_stopped_notifications = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button)); + config.main_config.show_recording_saved_notifications = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button)); config.main_config.record_cursor = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_cursor_button)); config.main_config.hide_window_when_recording = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item)); + config.main_config.restore_portal_session = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(restore_portal_session_button)); config.streaming_config.streaming_service = gtk_combo_box_get_active_id(GTK_COMBO_BOX(stream_service_input_menu)); config.streaming_config.youtube.stream_key = gtk_entry_get_text(youtube_stream_id_entry); config.streaming_config.twitch.stream_key = gtk_entry_get_text(twitch_stream_id_entry); config.streaming_config.custom.url = gtk_entry_get_text(custom_stream_url_entry); config.streaming_config.custom.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(custom_stream_container)); - if(!wayland) { - config.streaming_config.start_recording_hotkey.keysym = streaming_hotkey.keysym; - config.streaming_config.start_recording_hotkey.modifiers = streaming_hotkey.modkey_mask; - } config.record_config.save_directory = gtk_button_get_label(record_file_chooser_button); config.record_config.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(record_container)); - if(!wayland) { - config.record_config.start_recording_hotkey.keysym = record_hotkey.keysym; - config.record_config.start_recording_hotkey.modifiers = record_hotkey.modkey_mask; - - config.record_config.pause_recording_hotkey.keysym = pause_unpause_hotkey.keysym; - config.record_config.pause_recording_hotkey.modifiers = pause_unpause_hotkey.modkey_mask; - } config.replay_config.save_directory = gtk_button_get_label(replay_file_chooser_button); config.replay_config.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(replay_container)); config.replay_config.replay_time = gtk_spin_button_get_value_as_int(replay_time_entry); - if(!wayland) { - config.replay_config.start_recording_hotkey.keysym = replay_start_stop_hotkey.keysym; - config.replay_config.start_recording_hotkey.modifiers = replay_start_stop_hotkey.modkey_mask; - - config.replay_config.save_recording_hotkey.keysym = replay_save_hotkey.keysym; - config.replay_config.save_recording_hotkey.modifiers = replay_save_hotkey.modkey_mask; - } - - save_config(config); -} - -typedef struct { - const char *name; - int name_len; - vec2i pos; - vec2i size; - XRRCrtcInfo *crt_info; /* Only on x11 */ - uint32_t connector_id; /* Only on drm */ -} gsr_monitor; - -typedef enum { - GSR_CONNECTION_X11, - GSR_CONNECTION_WAYLAND, - GSR_CONNECTION_DRM -} gsr_connection_type; - -using active_monitor_callback = std::function<void(const gsr_monitor *monitor, void *userdata)>; - -static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id) { - for(int i = 0; i < sr->nmode; ++i) { - if(sr->modes[i].id == id) - return &sr->modes[i]; - } - return NULL; -} - -static void for_each_active_monitor_output_x11(Display *display, active_monitor_callback callback, void *userdata) { - XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display)); - if(!screen_res) - return; - - char display_name[256]; - for(int i = 0; i < screen_res->noutput; ++i) { - XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]); - if(out_info && out_info->crtc && out_info->connection == RR_Connected) { - XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc); - if(crt_info && crt_info->mode) { - const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode); - if(mode_info && out_info->nameLen < (int)sizeof(display_name)) { - memcpy(display_name, out_info->name, out_info->nameLen); - display_name[out_info->nameLen] = '\0'; - - gsr_monitor monitor; - monitor.name = display_name; - monitor.name_len = out_info->nameLen; - monitor.pos = { (int)crt_info->x, (int)crt_info->y }; - monitor.size = { (int)crt_info->width, (int)crt_info->height }; - monitor.crt_info = crt_info; - monitor.connector_id = 0; // TODO: Get connector id - callback(&monitor, userdata); - } - } - if(crt_info) - XRRFreeCrtcInfo(crt_info); - } - if(out_info) - XRRFreeOutputInfo(out_info); - } - - XRRFreeScreenResources(screen_res); -} - -typedef struct { - int type; - int count; -} drm_connector_type_count; - -#define CONNECTOR_TYPE_COUNTS 32 - -static drm_connector_type_count* drm_connector_types_get_index(drm_connector_type_count *type_counts, int *num_type_counts, int connector_type) { - for(int i = 0; i < *num_type_counts; ++i) { - if(type_counts[i].type == connector_type) - return &type_counts[i]; - } - - if(*num_type_counts == CONNECTOR_TYPE_COUNTS) - return NULL; - - const int index = *num_type_counts; - type_counts[index].type = connector_type; - type_counts[index].count = 0; - ++*num_type_counts; - return &type_counts[index]; -} - -static bool connector_get_property_by_name(int drmfd, drmModeConnectorPtr props, const char *name, uint64_t *result) { - for(int i = 0; i < props->count_props; ++i) { - drmModePropertyPtr prop = drmModeGetProperty(drmfd, props->props[i]); - if(prop) { - if(strcmp(name, prop->name) == 0) { - *result = props->prop_values[i]; - drmModeFreeProperty(prop); - return true; - } - drmModeFreeProperty(prop); - } - } - return false; -} - -static void for_each_active_monitor_output_wayland(const gsr_egl *egl, active_monitor_callback callback, void *userdata) { - for(int i = 0; i < egl->wayland.num_outputs; ++i) { - if(!egl->wayland.outputs[i].name) - continue; - - gsr_monitor monitor; - monitor.name = egl->wayland.outputs[i].name; - monitor.name_len = strlen(egl->wayland.outputs[i].name); - monitor.pos = { egl->wayland.outputs[i].pos.x, egl->wayland.outputs[i].pos.y }; - monitor.size = { egl->wayland.outputs[i].size.x, egl->wayland.outputs[i].size.y }; - monitor.crt_info = NULL; - monitor.connector_id = 0; - callback(&monitor, userdata); - } -} - -static void for_each_active_monitor_output_drm(const gsr_egl *egl, active_monitor_callback callback, void *userdata) { - int fd = open(egl->card_path, O_RDONLY); - if(fd == -1) - return; - - drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1); - - drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS]; - int num_type_counts = 0; - - char display_name[256]; - drmModeResPtr resources = drmModeGetResources(fd); - if(resources) { - for(int i = 0; i < resources->count_connectors; ++i) { - drmModeConnectorPtr connector = drmModeGetConnectorCurrent(fd, resources->connectors[i]); - if(!connector) - continue; - - drm_connector_type_count *connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector->connector_type); - const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type); - const int connection_name_len = strlen(connection_name); - if(connector_type) - ++connector_type->count; - - if(connector->connection != DRM_MODE_CONNECTED) { - drmModeFreeConnector(connector); - continue; - } - uint64_t crtc_id = 0; - connector_get_property_by_name(fd, connector, "CRTC_ID", &crtc_id); - - drmModeCrtcPtr crtc = drmModeGetCrtc(fd, crtc_id); - if(connector_type && crtc_id > 0 && crtc && connection_name_len + 5 < (int)sizeof(display_name)) { - const int display_name_len = snprintf(display_name, sizeof(display_name), "%s-%d", connection_name, connector_type->count); - gsr_monitor monitor; - monitor.name = display_name; - monitor.name_len = display_name_len; - monitor.pos = { (int)crtc->x, (int)crtc->y }; - monitor.size = { (int)crtc->width, (int)crtc->height }; - monitor.crt_info = NULL; - monitor.connector_id = connector->connector_id; - callback(&monitor, userdata); + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { + for(int i = 0; i < num_hotkeys; ++i) { + // This can also happen if we run multiple instances of gpu screen recorder, in which case it will fail to grab keys for the other windows. + // We dont want to overwrite hotkeys in that case. + if(hotkeys[i]->grab_success) { + hotkeys[i]->config->keysym = hotkeys[i]->keysym; + hotkeys[i]->config->modifiers = hotkeys[i]->modkey_mask; } - - if(crtc) - drmModeFreeCrtc(crtc); - - drmModeFreeConnector(connector); } - drmModeFreeResources(resources); - } - - close(fd); -} - -static void for_each_active_monitor_output(const gsr_egl *egl, gsr_connection_type connection_type, active_monitor_callback callback, void *userdata) { - switch(connection_type) { - case GSR_CONNECTION_X11: - for_each_active_monitor_output_x11(egl->x11.dpy, callback, userdata); - break; - case GSR_CONNECTION_WAYLAND: - for_each_active_monitor_output_wayland(egl, callback, userdata); - break; - case GSR_CONNECTION_DRM: - for_each_active_monitor_output_drm(egl, callback, userdata); - break; } -} - -static bool try_card_has_valid_plane(const char *card_path) { - drmVersion *ver = NULL; - drmModePlaneResPtr planes = NULL; - bool found_screen_card = false; - int fd = open(card_path, O_RDONLY); - if(fd == -1) - return false; - - ver = drmGetVersion(fd); - if(!ver || strstr(ver->name, "nouveau")) - goto next; - - drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); - - planes = drmModeGetPlaneResources(fd); - if(!planes) - goto next; - - for(uint32_t j = 0; j < planes->count_planes; ++j) { - drmModePlanePtr plane = drmModeGetPlane(fd, planes->planes[j]); - if(!plane) - continue; - - if(plane->fb_id) - found_screen_card = true; - - drmModeFreePlane(plane); - if(found_screen_card) - break; - } - - next: - if(planes) - drmModeFreePlaneResources(planes); - if(ver) - drmFreeVersion(ver); - close(fd); - if(found_screen_card) - return true; - - return false; -} - -static void string_copy(char *dst, const char *src, int len) { - int src_len = strlen(src); - int min_len = src_len; - if(len - 1 < min_len) - min_len = len - 1; - memcpy(dst, src, min_len); - dst[min_len] = '\0'; + save_config(config); } -/* output should be >= 128 bytes */ -static bool gsr_get_valid_card_path(gsr_egl *egl, char *output) { - if(egl->dri_card_path) { - string_copy(output, egl->dri_card_path, 127); - return try_card_has_valid_plane(output); +static void show_notification(GtkApplication *app, const char *title, const char *body, GNotificationPriority priority) { + if(priority < G_NOTIFICATION_PRIORITY_URGENT) { + notification_timeout_seconds = 3.0; + } else { + notification_timeout_seconds = 10.0; } - for(int i = 0; i < 10; ++i) { - snprintf(output, 127, DRM_DEV_NAME, DRM_DIR_NAME, i); - if(try_card_has_valid_plane(output)) - return true; - } - return false; -} + // KDE doesn't show notifications when using desktop portal capture unless either DoNotDisturb.WhenScreenSharing kde config + // has been changed by the user or if the priority for the notification is set as urgent + if((recording || replaying || streaming) && wayland_compositor == WaylandCompositor::KDE) + priority = G_NOTIFICATION_PRIORITY_URGENT; -static void show_notification(GtkApplication *app, const char *title, const char *body, GNotificationPriority priority) { fprintf(stderr, "Notification: title: %s, body: %s\n", title, body); GNotification *notification = g_notification_new(title); g_notification_set_body(notification, body); g_notification_set_priority(notification, priority); g_application_send_notification(&app->parent, "gpu-screen-recorder", notification); - showing_notification = true; - if(priority < G_NOTIFICATION_PRIORITY_URGENT) { - notification_timeout_seconds = 2.0; - } else { - notification_timeout_seconds = 5.0; - } notification_start_seconds = clock_get_monotonic_seconds(); + showing_notification = true; } -static bool window_has_atom(Display *display, Window window, Atom atom) { +static bool window_has_atom(Display *display, Window _window, Atom atom) { Atom type; unsigned long len, bytes_left; int format; unsigned char *properties = nullptr; - if(XGetWindowProperty(display, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) + if(XGetWindowProperty(display, _window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) return false; if(properties) @@ -1085,22 +1001,22 @@ static bool window_has_atom(Display *display, Window window, Atom atom) { return type != None; } -static Window window_get_target_window_child(Display *display, Window window) { - if(window == None) +static Window window_get_target_window_child(Display *display, Window _window) { + if(_window == None) return None; Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False); if(!wm_state_atom) return None; - if(window_has_atom(display, window, wm_state_atom)) - return window; + if(window_has_atom(display, _window, wm_state_atom)) + return _window; Window root; Window parent; Window *children = nullptr; unsigned int num_children = 0; - if(!XQueryTree(display, window, &root, &parent, &children, &num_children) || !children) + if(!XQueryTree(display, _window, &root, &parent, &children, &num_children) || !children) return None; Window found_window = None; @@ -1126,40 +1042,40 @@ static Window window_get_target_window_child(Display *display, Window window) { return found_window; } -/* TODO: Look at xwininfo source to figure out how to make this work for different types of window managers */ static GdkFilterReturn filter_callback(GdkXEvent *xevent, GdkEvent*, gpointer userdata) { - SelectWindowUserdata *select_window_userdata = (SelectWindowUserdata*)userdata; + SelectWindowUserdata *_select_window_userdata = (SelectWindowUserdata*)userdata; XEvent *ev = (XEvent*)xevent; //assert(ev->type == ButtonPress); if(ev->type != ButtonPress) return GDK_FILTER_CONTINUE; Window target_win = ev->xbutton.subwindow; - Window new_window = window_get_target_window_child(select_window_userdata->display, target_win); + // TODO: Fix, this is incorrect when trying to record steam window. For steam window + Window new_window = window_get_target_window_child(_select_window_userdata->display, target_win); if(new_window) target_win = new_window; - int status = XUngrabPointer(select_window_userdata->display, CurrentTime); + int status = XUngrabPointer(_select_window_userdata->display, CurrentTime); if(!status) { fprintf(stderr, "failed to ungrab pointer!\n"); - show_notification(select_window_userdata->app, "GPU Screen Recorder", "Failed to ungrab pointer!", G_NOTIFICATION_PRIORITY_URGENT); + show_notification(_select_window_userdata->app, "GPU Screen Recorder", "Failed to ungrab pointer!", G_NOTIFICATION_PRIORITY_URGENT); exit(1); } if(target_win == None) { - show_notification(select_window_userdata->app, "GPU Screen Recorder", "No window selected!", G_NOTIFICATION_PRIORITY_URGENT); + show_notification(_select_window_userdata->app, "GPU Screen Recorder", "No window selected!", G_NOTIFICATION_PRIORITY_URGENT); GdkScreen *screen = gdk_screen_get_default(); GdkWindow *root_window = gdk_screen_get_root_window(screen); - gdk_window_remove_filter(root_window, filter_callback, select_window_userdata); + gdk_window_remove_filter(root_window, filter_callback, _select_window_userdata); return GDK_FILTER_REMOVE; } std::string window_name; XTextProperty wm_name_prop; - if(XGetWMName(select_window_userdata->display, target_win, &wm_name_prop) && wm_name_prop.nitems > 0) { + if(XGetWMName(_select_window_userdata->display, target_win, &wm_name_prop) && wm_name_prop.nitems > 0) { char **list_return = NULL; int num_items = 0; - int ret = XmbTextPropertyToTextList(select_window_userdata->display, &wm_name_prop, &list_return, &num_items); + int ret = XmbTextPropertyToTextList(_select_window_userdata->display, &wm_name_prop, &list_return, &num_items); if((ret == Success || ret > 0) && list_return) { for(int i = 0; i < num_items; ++i) { window_name += list_return[i]; @@ -1173,12 +1089,12 @@ static GdkFilterReturn filter_callback(GdkXEvent *xevent, GdkEvent*, gpointer us } fprintf(stderr, "window name: %s, window id: %ld\n", window_name.c_str(), target_win); - gtk_button_set_label(select_window_userdata->select_window_button, window_name.c_str()); - select_window_userdata->selected_window = target_win; + gtk_button_set_label(_select_window_userdata->select_window_button, window_name.c_str()); + _select_window_userdata->selected_window = target_win; GdkScreen *screen = gdk_screen_get_default(); GdkWindow *root_window = gdk_screen_get_root_window(screen); - gdk_window_remove_filter(root_window, filter_callback, select_window_userdata); + gdk_window_remove_filter(root_window, filter_callback, _select_window_userdata); enable_stream_record_button_if_info_filled(); @@ -1212,6 +1128,13 @@ static gboolean on_select_window_button_click(GtkButton *button, gpointer) { return true; } +static gboolean on_change_video_resolution_button_click(GtkButton *button, gpointer) { + const bool clicked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); + const std::string window_str = record_area_selection_menu_get_active_id(); + gtk_widget_set_visible(GTK_WIDGET(video_resolution_grid), clicked && window_str != "focused"); + return true; +} + static bool key_is_modifier(KeySym key_sym) { return key_sym >= XK_Shift_L && key_sym <= XK_Super_R && key_sym != XK_Caps_Lock && key_sym != XK_Shift_Lock; } @@ -1323,7 +1246,7 @@ static int xerror_grab_error(Display*, XErrorEvent*) { } static void ungrab_keyboard(Display *display) { - if(wayland) + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return; if(current_hotkey) { @@ -1338,7 +1261,7 @@ static void ungrab_keyboard(Display *display) { } static bool grab_ungrab_hotkey_combo(Display *display, Hotkey hotkey, bool grab) { - if(wayland) + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return true; if(hotkey.keysym == None && hotkey.modkey_mask == 0) @@ -1351,7 +1274,7 @@ static bool grab_ungrab_hotkey_combo(Display *display, Hotkey hotkey, bool grab) for(int i = 0; i < 8; ++i) { for(int j = 0; j < modmap->max_keypermod; ++j) { if(modmap->modifiermap[i * modmap->max_keypermod + j] == numlock_keycode) - numlockmask = (1 << i); + numlockmask = (1 << i); } } XFreeModifiermap(modmap); @@ -1368,7 +1291,7 @@ static bool grab_ungrab_hotkey_combo(Display *display, Hotkey hotkey, bool grab) XSync(display, False); x_failed = false; XErrorHandler prev_error_handler = XSetErrorHandler(xerror_grab_error); - + Window root_window = DefaultRootWindow(display); unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; if(key_sym != None) { @@ -1396,14 +1319,12 @@ static bool grab_ungrab_hotkey_combo(Display *display, Hotkey hotkey, bool grab) } static void ungrab_keys(Display *display) { - if(wayland) + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return; - grab_ungrab_hotkey_combo(display, streaming_hotkey, false); - grab_ungrab_hotkey_combo(display, record_hotkey, false); - grab_ungrab_hotkey_combo(display, pause_unpause_hotkey, false); - grab_ungrab_hotkey_combo(display, replay_start_stop_hotkey, false); - grab_ungrab_hotkey_combo(display, replay_save_hotkey, false); + for(int i = 0; i < num_hotkeys; ++i) { + grab_ungrab_hotkey_combo(display, *hotkeys[i], false); + } } static void set_hotkey_text_from_hotkey_data(GtkEntry *entry, Hotkey hotkey) { @@ -1446,63 +1367,67 @@ static void set_hotkey_text_from_hotkey_data(GtkEntry *entry, Hotkey hotkey) { gtk_entry_set_text(entry, hotkey_combo_str.c_str()); } -struct HotkeyResult { - bool record_hotkey_success = false; - bool pause_unpause_hotkey_success = false; - bool streaming_hotkey_success = false; - bool replay_start_stop_hotkey_success = false; - bool replay_save_hotkey_success = false; -}; +static bool replace_grabbed_keys_depending_on_active_page() { + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) + return true; + + for(int i = 0; i < num_hotkeys; ++i) { + hotkeys[i]->grab_success = false; + } -static HotkeyResult replace_grabbed_keys_depending_on_active_page() { - HotkeyResult hotkey_result; ungrab_keys(gdk_x11_get_default_xdisplay()); const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata.stack); - if(visible_page == page_navigation_userdata.recording_page) { - bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), record_hotkey, true); - bool grab_pause_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), pause_unpause_hotkey, true); - hotkey_mode = HotkeyMode::Record; - hotkey_result.record_hotkey_success = grab_record_success; - hotkey_result.pause_unpause_hotkey_success = grab_pause_success; - } else if(visible_page == page_navigation_userdata.streaming_page) { - bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), streaming_hotkey, true); - hotkey_mode = HotkeyMode::Record; - hotkey_result.streaming_hotkey_success = grab_record_success; - } else if(visible_page == page_navigation_userdata.replay_page) { - bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), replay_start_stop_hotkey, true); - bool grab_save_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), replay_save_hotkey, true); - hotkey_mode = HotkeyMode::Record; - - hotkey_result.replay_start_stop_hotkey_success = grab_record_success; - hotkey_result.replay_save_hotkey_success = grab_save_success; + bool keys_successfully_grabbed = true; + for(int i = 0; i < num_hotkeys; ++i) { + if(visible_page == hotkeys[i]->page) { + hotkey_mode = HotkeyMode::Record; + hotkeys[i]->grab_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), *hotkeys[i], true); + if(hotkeys[i]->grab_success) { + set_hotkey_text_from_hotkey_data(GTK_ENTRY(hotkeys[i]->hotkey_entry), *hotkeys[i]); + } else { + gtk_entry_set_text(GTK_ENTRY(hotkeys[i]->hotkey_entry), ""); + hotkeys[i]->keysym = 0; + hotkeys[i]->modkey_mask = 0; + keys_successfully_grabbed = false; + } + } } - return hotkey_result; + return keys_successfully_grabbed; +} + +static bool is_monitor_capture_drm() { + return gsr_info.system_info.display_server == DisplayServer::WAYLAND || gsr_info.gpu_info.vendor != GpuVendor::NVIDIA; } static bool show_pkexec_flatpak_error_if_needed() { + if(!flatpak) + return false; + const std::string window_str = record_area_selection_menu_get_active_id(); - if((wayland || gpu_inf.vendor != GPU_VENDOR_NVIDIA) && window_str != "window" && window_str != "focused") { + if(is_monitor_capture_drm() && window_str != "window" && window_str != "focused" && window_str != "portal") { if(!is_pkexec_installed()) { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "pkexec needs to be installed to record a monitor with an AMD/Intel GPU. Please install and run polkit. Alternatively, record a single window which doesn't require root access."); + "pkexec needs to be installed to record a monitor with an AMD/Intel GPU. Please install and run polkit. Alternatively, record a single window or use portal option which doesn't require root access."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); return true; } - if(flatpak && !flatpak_is_installed_as_system()) { - if(wayland) { + if(!flatpak_is_installed_as_system()) { + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "GPU Screen Recorder needs to be installed system-wide to record your monitor on Wayland. To install GPU Screen recorder system-wide, you can run this command:\n" - "flatpak install flathub --system com.dec05eba.gpu_screen_recorder\n"); + "GPU Screen Recorder needs to be installed system-wide to record your monitor on Wayland when not using the portal option. You can run this command to install GPU Screen recorder system-wide:\n" + "flatpak install --system com.dec05eba.gpu_screen_recorder\n"); + set_dialog_selectable(dialog); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); } else { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "GPU Screen Recorder needs to be installed system-wide to record your monitor on AMD/Intel. To install GPU Screen recorder system-wide, you can run this command:\n" - "flatpak install flathub --system com.dec05eba.gpu_screen_recorder\n" + "GPU Screen Recorder needs to be installed system-wide to record your monitor on AMD/Intel when not using the portal option. You can run this command to install GPU Screen recorder system-wide:\n" + "flatpak install --system com.dec05eba.gpu_screen_recorder\n" "Alternatively, record a single window which doesn't have this restriction."); + set_dialog_selectable(dialog); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); } @@ -1512,27 +1437,111 @@ static bool show_pkexec_flatpak_error_if_needed() { return false; } -static gboolean on_start_replay_click(GtkButton*, gpointer userdata) { - if(show_pkexec_flatpak_error_if_needed()) - return true; +static void show_bugged_driver_warning() { + if(gsr_info.gpu_info.vendor != GpuVendor::AMD) + return; + + const std::string video_codec = video_codec_selection_menu_get_active_id(); + if((video_codec == "hevc" || video_codec == "hevc_10bit" || video_codec == "hevc_hdr") && !config.main_config.hevc_amd_bug_warning_shown) { + GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, + "There is an AMD driver bug and FFmpeg bug that causes black bars to appear on the sides of the video at certain resolutions when using HEVC codec.\n" + "Select H264 video codec instead if this is an issue for you."); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + config.main_config.hevc_amd_bug_warning_shown = true; + } + + if((video_codec == "av1" || video_codec == "av1_10bit" || video_codec == "av1_hdr") && !config.main_config.av1_amd_bug_warning_shown) { + GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, + "There is an AMD hardware bug that causes black bars to appear on the sides of the video at certain resolutions when using AV1 codec.\n" + "Select H264 video codec instead if this is an issue for you."); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + config.main_config.av1_amd_bug_warning_shown = true; + } +} - PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; - gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->replay_page); - app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(page_navigation_userdata->app, SystrayPage::REPLAY))); +static void replace_meta_with_super(std::string &str) { + size_t index = str.find("meta"); + if(index != std::string::npos) + str.replace(index, 4, "Super"); + + index = str.find("Meta"); + if(index != std::string::npos) + str.replace(index, 4, "Super"); +} - if(!wayland) { - HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); - if(!hotkey_result.replay_start_stop_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(replay_start_stop_hotkey.hotkey_entry), ""); - replay_start_stop_hotkey.keysym = 0; - replay_start_stop_hotkey.modkey_mask = 0; +static void shortcut_changed_callback(gsr_shortcut shortcut, void *userdata) { + (void)userdata; + global_shortcuts_received = true; + std::string trigger = shortcut.trigger_description; + replace_meta_with_super(trigger); + for(int i = 0; i < num_hotkeys; ++i) { + if(strcmp(shortcut.id, hotkeys[i]->shortcut_id) == 0) { + gtk_entry_set_text(GTK_ENTRY(hotkeys[i]->hotkey_entry), trigger.c_str()); } - if(!hotkey_result.replay_save_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey.hotkey_entry), ""); - replay_save_hotkey.keysym = 0; - replay_save_hotkey.modkey_mask = 0; + } +} + +static gboolean on_register_hotkeys_button_clicked(GtkButton *button, gpointer userdata) { + (void)button; + (void)userdata; + + /* + Modifier key names are defined here: https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-names.h. + Remove the XKB_MOD_NAME_ prefix from the name and use the remaining part. + Key names are defined here: https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-keysyms.h. + Remove the XKB_KEY_ (or XKB_KEY_KP_) prefix from the name and user the remaining part. + */ + /* Unfortunately global shortcuts cant handle same key for different shortcuts, even though GPU Screen Recorder has page specific hotkeys */ + const gsr_bind_shortcut shortcuts[3] = { + { + "Start/stop recording/replay/streaming", + { SHORTCUT_ID_START_STOP_RECORDING, "ALT+1" } + }, + { + "Pause/unpause recording", + { SHORTCUT_ID_PAUSE_UNPAUSE_RECORDING, "ALT+2" } + }, + { + "Save replay", + { SHORTCUT_ID_SAVE_REPLAY, "ALT+3" } } + }; + + if(global_shortcuts_initialized) { + if(!gsr_global_shortcuts_bind_shortcuts(&global_shortcuts, shortcuts, 3, shortcut_changed_callback, NULL)) { + fprintf(stderr, "gsr error: failed to bind shortcuts\n"); + } + } + + return true; +} + +static void register_global_shortcuts_once() { + static bool registered = false; + // On KDE plasma the shortcut menu popup will show up everytime this is used, so we dont want to call it everytime. + // On Hyprland the global shortcut desktop portal is broken on older versions and crashes the desktop portal. + // On GNOME this needs to be called everytime to register the shortcuts. The shortcut popup menu will show the first time only. + if(wayland_compositor == WaylandCompositor::UNKNOWN && !registered && !global_shortcuts_received) { + registered = true; + on_register_hotkeys_button_clicked(nullptr, nullptr); } +} + +static gboolean on_start_replay_click(GtkButton*, gpointer userdata) { + if(show_pkexec_flatpak_error_if_needed()) + return true; + + show_bugged_driver_warning(); + register_global_shortcuts_once(); + + PageNavigationUserdata *_page_navigation_userdata = (PageNavigationUserdata*)userdata; + gtk_stack_set_visible_child(_page_navigation_userdata->stack, _page_navigation_userdata->replay_page); + app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(_page_navigation_userdata->app, SystrayPage::REPLAY))); + + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) + replace_grabbed_keys_depending_on_active_page(); return true; } @@ -1541,23 +1550,15 @@ static gboolean on_start_recording_click(GtkButton*, gpointer userdata) { if(show_pkexec_flatpak_error_if_needed()) return true; - PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; - gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->recording_page); - app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(page_navigation_userdata->app, SystrayPage::RECORDING))); + show_bugged_driver_warning(); + register_global_shortcuts_once(); - if(!wayland) { - HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); - if(!hotkey_result.record_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(record_hotkey.hotkey_entry), ""); - record_hotkey.keysym = 0; - record_hotkey.modkey_mask = 0; - } - if(!hotkey_result.pause_unpause_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(pause_unpause_hotkey.hotkey_entry), ""); - pause_unpause_hotkey.keysym = 0; - pause_unpause_hotkey.modkey_mask = 0; - } - } + PageNavigationUserdata *_page_navigation_userdata = (PageNavigationUserdata*)userdata; + gtk_stack_set_visible_child(_page_navigation_userdata->stack, _page_navigation_userdata->recording_page); + app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(_page_navigation_userdata->app, SystrayPage::RECORDING))); + + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) + replace_grabbed_keys_depending_on_active_page(); return true; } @@ -1572,12 +1573,18 @@ static gboolean on_start_streaming_click(GtkButton*, gpointer userdata) { if(show_pkexec_flatpak_error_if_needed()) return true; - int num_audio_tracks = 0; - for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&num_audio_tracks](const AudioRow*) { - ++num_audio_tracks; - }); + show_bugged_driver_warning(); + register_global_shortcuts_once(); - if(num_audio_tracks > 1 && !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button))) { + int num_audio_tracks = 0; + gtk_container_foreach(GTK_CONTAINER(audio_devices_items_box), [](GtkWidget *widget, gpointer userdata) { + int &num_audio_tracks = *(int*)userdata; + GtkWidget *row_item_widget = gtk_grid_get_child_at(GTK_GRID(widget), 0, 0); + if(GTK_IS_COMBO_BOX_TEXT(row_item_widget)) + ++num_audio_tracks; + }, &num_audio_tracks); + + if(num_audio_tracks > 1 && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(split_audio_button))) { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Streaming doesn't work with more than 1 audio track. Please remove all audio tracks or only use 1 audio track or select to merge audio tracks."); gtk_dialog_run(GTK_DIALOG(dialog)); @@ -1585,28 +1592,22 @@ static gboolean on_start_streaming_click(GtkButton*, gpointer userdata) { return true; } - PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; - gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->streaming_page); - app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(page_navigation_userdata->app, SystrayPage::STREAMING))); + PageNavigationUserdata *_page_navigation_userdata = (PageNavigationUserdata*)userdata; + gtk_stack_set_visible_child(_page_navigation_userdata->stack, _page_navigation_userdata->streaming_page); + app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(_page_navigation_userdata->app, SystrayPage::STREAMING))); - if(!wayland) { - HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); - if(!hotkey_result.streaming_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(streaming_hotkey.hotkey_entry), ""); - streaming_hotkey.keysym = 0; - streaming_hotkey.modkey_mask = 0; - } - } + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) + replace_grabbed_keys_depending_on_active_page(); return true; } static gboolean on_streaming_recording_replay_page_back_click(GtkButton*, gpointer userdata) { - PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; - gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->common_settings_page); + PageNavigationUserdata *_page_navigation_userdata = (PageNavigationUserdata*)userdata; + gtk_stack_set_visible_child(_page_navigation_userdata->stack, _page_navigation_userdata->common_settings_page); ungrab_keys(gdk_x11_get_default_xdisplay()); hotkey_mode = HotkeyMode::NoAction; - app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(page_navigation_userdata->app, SystrayPage::FRONT))); + app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(_page_navigation_userdata->app, SystrayPage::FRONT))); return true; } @@ -1643,9 +1644,11 @@ static gboolean on_replay_file_chooser_button_click(GtkButton *button, gpointer) return res; } -static bool kill_gpu_screen_recorder_get_result() { +static bool kill_gpu_screen_recorder_get_result(bool *already_dead) { + *already_dead = true; bool exit_success = true; if(gpu_screen_recorder_process != -1) { + *already_dead = false; int status; int wait_result = waitpid(gpu_screen_recorder_process, &status, WNOHANG); if(wait_result == -1) { @@ -1668,32 +1671,136 @@ static bool kill_gpu_screen_recorder_get_result() { return exit_success; } -static const gchar* audio_row_get_id(const AudioRow *audio_row) { - const char *text = gtk_combo_box_text_get_active_text(audio_row->input_list); - if(strcmp(text, "Default output") == 0 && !pa_default_sources.default_sink_name.empty()) - return pa_default_sources.default_sink_name.c_str(); - else if(strcmp(text, "Default input") == 0 && !pa_default_sources.default_source_name.empty()) - return pa_default_sources.default_source_name.c_str(); - else - return gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_row->input_list)); +static bool starts_with(const std::string &str, const char *substr) { + size_t len = strlen(substr); + return str.size() >= len && memcmp(str.data(), substr, len) == 0; } -static void add_audio_command_line_args(std::vector<const char*> &args, std::string &merge_audio_tracks_arg_value) { - pa_default_sources = get_pulseaudio_default_inputs(); +struct AudioTracksUserdata { + std::vector<std::string> &result; + bool application_audio_invert; + int num_app_audio = 0; +}; - if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button))) { - for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&merge_audio_tracks_arg_value](const AudioRow *audio_row) { - if(!merge_audio_tracks_arg_value.empty()) - merge_audio_tracks_arg_value += '|'; - merge_audio_tracks_arg_value += audio_row_get_id(audio_row); - }); +static std::vector<std::string> create_audio_tracks_real_names(std::string &merge_audio_tracks) { + std::vector<std::string> result; + AudioTracksUserdata audio_tracks_userdata { + result, + (bool)gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_app_audio_inverted_button)), + 0 + }; + + gtk_container_foreach(GTK_CONTAINER(audio_devices_items_box), [](GtkWidget *widget, gpointer userdata) { + AudioTracksUserdata &audio_tracks_userdata = *(AudioTracksUserdata*)userdata; + const char *audio_track_type = (const char*)g_object_get_data(G_OBJECT(widget), "audio-track-type"); + GtkWidget *row_item_widget = gtk_grid_get_child_at(GTK_GRID(widget), 1, 0); + if(strcmp(audio_track_type, "device") == 0) { + std::string audio_input_name = "device:"; + audio_input_name += gtk_combo_box_get_active_id(GTK_COMBO_BOX(row_item_widget)); + audio_tracks_userdata.result.push_back(std::move(audio_input_name)); + } else if(strcmp(audio_track_type, "app") == 0) { + if(!gsr_info.system_info.supports_app_audio) + return; + + std::string audio_input_name = audio_tracks_userdata.application_audio_invert ? "app-inverse:" : "app:"; + audio_input_name += gtk_combo_box_get_active_id(GTK_COMBO_BOX(row_item_widget)); + audio_tracks_userdata.result.push_back(std::move(audio_input_name)); + ++audio_tracks_userdata.num_app_audio; + } else if(strcmp(audio_track_type, "app-custom") == 0) { + if(!gsr_info.system_info.supports_app_audio) + return; + + std::string audio_input_name = audio_tracks_userdata.application_audio_invert ? "app-inverse:" : "app:"; + audio_input_name += gtk_entry_get_text(GTK_ENTRY(row_item_widget)); + audio_tracks_userdata.result.push_back(std::move(audio_input_name)); + ++audio_tracks_userdata.num_app_audio; + } + }, &audio_tracks_userdata); + + if(audio_tracks_userdata.num_app_audio == 0 && audio_tracks_userdata.application_audio_invert) + audio_tracks_userdata.result.push_back("app-inverse:"); - if(!merge_audio_tracks_arg_value.empty()) - args.insert(args.end(), { "-a", merge_audio_tracks_arg_value.c_str() }); + merge_audio_tracks.clear(); + for(size_t i = 0; i < result.size(); ++i) { + if(i > 0) + merge_audio_tracks += '|'; + merge_audio_tracks += result[i]; + } + + return result; +} + +static void add_audio_command_line_args(std::vector<const char*> &args, const std::vector<std::string> &audio_tracks, const std::string &merge_audio_tracks) { + if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(split_audio_button))) { + if(!merge_audio_tracks.empty()) + args.insert(args.end(), { "-a", merge_audio_tracks.c_str() }); } else { - for_each_used_audio_input(GTK_LIST_BOX(audio_input_used_list), [&args](const AudioRow *audio_row) { - args.insert(args.end(), { "-a", audio_row_get_id(audio_row) }); - }); + for(const std::string &audio_track : audio_tracks) { + args.insert(args.end(), { "-a", audio_track.c_str() }); + } + } +} + +static void change_container_if_codec_not_supported(const std::string &video_codec, const gchar **container_str) { + if(strcmp(video_codec.c_str(), "vp8") == 0 || strcmp(video_codec.c_str(), "vp9") == 0) { + if(strcmp(*container_str, "webm") != 0 && strcmp(*container_str, "matroska") != 0) { + fprintf(stderr, "Warning: container '%s' is not compatible with video codec '%s', using webm container instead\n", *container_str, video_codec.c_str()); + *container_str = "webm"; + } + } else if(strcmp(*container_str, "webm") == 0) { + fprintf(stderr, "Warning: container webm is not compatible with video codec '%s', using mp4 container instead\n", video_codec.c_str()); + *container_str = "mp4"; + } +} + +static bool switch_video_codec_to_usable_hardware_encoder(std::string &video_codec) { + if(gsr_info.supported_video_codecs.h264) { + video_codec = "h264"; + return true; + } else if(gsr_info.supported_video_codecs.hevc) { + video_codec = "hevc"; + return true; + } else if(gsr_info.supported_video_codecs.av1) { + video_codec = "av1"; + return true; + } else if(gsr_info.supported_video_codecs.vp8) { + video_codec = "vp8"; + return true; + } else if(gsr_info.supported_video_codecs.vp9) { + video_codec = "vp9"; + return true; + } + return false; +} + +static void add_quality_command_line_args(std::vector<const char*> &args, const char *quality_input_str, const char *video_bitrate_str) { + if(strcmp(quality_input_str, "custom") == 0) { + args.push_back("-bm"); + args.push_back("cbr"); + args.push_back("-q"); + args.push_back(video_bitrate_str); + } else { + args.push_back("-q"); + args.push_back(quality_input_str); + } +} + +static void debug_print_args(const char **args) { + fprintf(stderr, "info: running command:"); + while(*args) { + fprintf(stderr, " %s", *args); + ++args; + } + fprintf(stderr, "\n"); +} + +static bool validate_window(GtkApplication *app, Window window) { + XWindowAttributes attr; + if(XGetWindowAttributes(dpy, window, &attr)) { + return true; + } else { + show_notification(app, "GPU Screen Recorder", "The window you are trying to record no longer exists", G_NOTIFICATION_PRIORITY_URGENT); + return false; } } @@ -1704,7 +1811,8 @@ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdat int exit_status = prev_exit_status; prev_exit_status = -1; if(replaying) { - bool exit_success = kill_gpu_screen_recorder_get_result(); + bool already_dead = true; + bool exit_success = kill_gpu_screen_recorder_get_result(&already_dead); gtk_button_set_label(button, "Start replay"); replaying = false; @@ -1716,14 +1824,20 @@ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdat gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_replay_menu_item), "Start replay"); gtk_widget_set_sensitive(save_replay_menu_item, false); - app_indicator_set_icon_full(app_indicator, tray_idle_icon_name, "Idle"); + app_indicator_set_icon_full(app_indicator, get_tray_idle_icon_name(), "Idle"); if(exit_status == 10) { - show_notification(app, "GPU Screen Recorder", - "You need to have pkexec installed and a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); - } else if(!exit_success) { + show_notification(app, "GPU Screen Recorder", "You need to have pkexec installed and have a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); + } else if(exit_status == 50) { + show_notification(app, "GPU Screen Recorder", "Desktop portal capture failed. Either you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system", G_NOTIFICATION_PRIORITY_URGENT); + } else if(exit_status == 60) { + // Canceled by the user + } else if(!exit_success || (already_dead && exit_status != 0)) { show_notification(app, "GPU Screen Recorder", "Failed to start replay. Either your graphics card doesn't support GPU Screen Recorder with the settings you used or you don't have enough disk space to record a video", G_NOTIFICATION_PRIORITY_URGENT); + } else if(exit_success) { + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button))) + show_notification(app, "GPU Screen Recorder", "Stopped replay", G_NOTIFICATION_PRIORITY_NORMAL); } return true; @@ -1731,10 +1845,8 @@ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdat save_configs(); - int fps = gtk_spin_button_get_value_as_int(fps_entry); - int replay_time = gtk_spin_button_get_value_as_int(replay_time_entry); - int record_width = wayland ? 0 : gtk_spin_button_get_value_as_int(area_width_entry); - int record_height = wayland ? 0 : gtk_spin_button_get_value_as_int(area_height_entry); + const int fps = gtk_spin_button_get_value_as_int(fps_entry); + const int replay_time = gtk_spin_button_get_value_as_int(replay_time_entry); char dir_tmp[PATH_MAX]; strcpy(dir_tmp, dir); @@ -1752,40 +1864,65 @@ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdat return true; } window_str = std::to_string(select_window_userdata.selected_window); + if(!validate_window(app, select_window_userdata.selected_window)) + return true; } else if(window_str == "focused") { follow_focused = true; } - std::string fps_str = std::to_string(fps); - std::string replay_time_str = std::to_string(replay_time); + const std::string fps_str = std::to_string(fps); + const std::string replay_time_str = std::to_string(replay_time); + const bool change_video_resolution = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(change_video_resolution_button)); const gchar* container_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(replay_container)); const gchar* color_range_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(color_range_input_menu)); const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); - const gchar* video_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(video_codec_input_menu)); const gchar* audio_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_codec_input_menu)); const gchar* framerate_mode_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(framerate_mode_input_menu)); const bool record_cursor = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_cursor_button)); + const bool restore_portal_session = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(restore_portal_session_button)); + const std::string video_bitrate_str = std::to_string(gtk_spin_button_get_value_as_int(video_bitrate_entry)); + + const int record_width = follow_focused ? gtk_spin_button_get_value_as_int(area_width_entry) : gtk_spin_button_get_value_as_int(video_width_entry); + const int record_height = follow_focused ? gtk_spin_button_get_value_as_int(area_height_entry) : gtk_spin_button_get_value_as_int(video_height_entry); + + const char *encoder = "gpu"; + std::string video_codec_input_str = video_codec_selection_menu_get_active_id(); + if(video_codec_input_str == "h264_software") { + video_codec_input_str = "h264"; + encoder = "cpu"; + } else if(video_codec_input_str == "auto") { + if(!switch_video_codec_to_usable_hardware_encoder(video_codec_input_str)) { + video_codec_input_str = "h264"; + encoder = "cpu"; + } + } + + change_container_if_codec_not_supported(video_codec_input_str, &container_str); char area[64]; snprintf(area, sizeof(area), "%dx%d", record_width, record_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-q", quality_input_str, "-k", video_codec_input_str, "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-cr", color_range_input_str, "-r", replay_time_str.c_str(), "-o", dir + "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-k", video_codec_input_str.c_str(), "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-restore-portal-session", restore_portal_session ? "yes" : "no", "-cr", color_range_input_str, "-r", replay_time_str.c_str(), "-encoder", encoder, "-o", dir }; + add_quality_command_line_args(args, quality_input_str, video_bitrate_str.c_str()); + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(overclock_button))) args.insert(args.end(), { "-oc", "yes" }); if(strcmp(framerate_mode_input_str, "auto") != 0) args.insert(args.end(), { "-fm", framerate_mode_input_str }); - std::string merge_audio_tracks_arg_value; - add_audio_command_line_args(args, merge_audio_tracks_arg_value); + std::string merge_audio_tracks; + const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(merge_audio_tracks); + add_audio_command_line_args(args, audio_tracks, merge_audio_tracks); - if(follow_focused) + if(follow_focused || change_video_resolution) args.insert(args.end(), { "-s", area }); args.push_back(NULL); + debug_print_args(args.data()); if(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item))) { hide_window(); @@ -1805,7 +1942,7 @@ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdat if(getppid() != parent_pid) _exit(3); - + execvp(args[0], (char* const*)args.data()); perror("failed to launch gpu-screen-recorder"); _exit(127); @@ -1823,7 +1960,10 @@ static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdat gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_replay_menu_item), "Stop replay"); gtk_widget_set_sensitive(save_replay_menu_item, true); - app_indicator_set_icon_full(app_indicator, tray_recording_icon_name, "Recording"); + app_indicator_set_icon_full(app_indicator, get_tray_recording_icon_name(), "Recording"); + + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button))) + show_notification(app, "GPU Screen Recorder", "Started replay", G_NOTIFICATION_PRIORITY_NORMAL); record_start_time_sec = clock_get_monotonic_seconds(); return true; @@ -1835,12 +1975,15 @@ static gboolean on_replay_save_button_click(GtkButton*, gpointer userdata) { GtkApplication *app = (GtkApplication*)userdata; kill(gpu_screen_recorder_process, SIGUSR1); - if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_notification_button))) + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button))) show_notification(app, "GPU Screen Recorder", "Saved replay", G_NOTIFICATION_PRIORITY_NORMAL); return true; } static gboolean on_pause_unpause_button_click(GtkButton*, gpointer) { + if(!recording) + return true; + if(gpu_screen_recorder_process == -1) return true; @@ -1850,13 +1993,13 @@ static gboolean on_pause_unpause_button_click(GtkButton*, gpointer) { gtk_button_set_label(pause_recording_button, "Unpause recording"); gtk_image_set_from_icon_name(GTK_IMAGE(recording_record_icon), "media-playback-pause", GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_menu_item_set_label(GTK_MENU_ITEM(pause_recording_menu_item), "Unpause recording"); - app_indicator_set_icon_full(app_indicator, tray_paused_icon_name, "Paused"); + app_indicator_set_icon_full(app_indicator, get_tray_paused_icon_name(), "Paused"); pause_start_sec = clock_get_monotonic_seconds(); } else { gtk_button_set_label(pause_recording_button, "Pause recording"); gtk_image_set_from_icon_name(GTK_IMAGE(recording_record_icon), "media-record", GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_menu_item_set_label(GTK_MENU_ITEM(pause_recording_menu_item), "Pause recording"); - app_indicator_set_icon_full(app_indicator, tray_recording_icon_name, "Recording"); + app_indicator_set_icon_full(app_indicator, get_tray_recording_icon_name(), "Recording"); paused_time_offset_sec += (clock_get_monotonic_seconds() - pause_start_sec); } return true; @@ -1869,7 +2012,8 @@ static gboolean on_start_recording_button_click(GtkButton *button, gpointer user int exit_status = prev_exit_status; prev_exit_status = -1; if(recording) { - bool exit_success = kill_gpu_screen_recorder_get_result(); + bool already_dead = true; + bool exit_success = kill_gpu_screen_recorder_get_result(&already_dead); gtk_button_set_label(button, "Start recording"); recording = false; @@ -1886,37 +2030,28 @@ static gboolean on_start_recording_button_click(GtkButton *button, gpointer user gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_recording_menu_item), "Start recording"); gtk_menu_item_set_label(GTK_MENU_ITEM(pause_recording_menu_item), "Pause recording"); gtk_widget_set_sensitive(pause_recording_menu_item, false); - app_indicator_set_icon_full(app_indicator, tray_idle_icon_name, "Idle"); + app_indicator_set_icon_full(app_indicator, get_tray_idle_icon_name(), "Idle"); if(exit_status == 10) { - show_notification(app, "GPU Screen Recorder", - "You need to have pkexec installed and a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); + show_notification(app, "GPU Screen Recorder", "You need to have pkexec installed and have a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); + } else if(exit_status == 50) { + show_notification(app, "GPU Screen Recorder", "Desktop portal capture failed. Either you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system", G_NOTIFICATION_PRIORITY_URGENT); + } else if(exit_status == 60) { + // Canceled by the user + } else if(!exit_success || (already_dead && exit_status != 0)) { + show_notification(app, "GPU Screen Recorder", "Failed to save video. Either your graphics card doesn't support GPU Screen Recorder with the settings you used or you don't have enough disk space to record a video. Start this GPU Screen Recorder GUI application from the terminal to see more information when this failure happens", G_NOTIFICATION_PRIORITY_URGENT); } else if(exit_success) { - if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_notification_button))) { + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button))) { const std::string notification_body = std::string("The recording was saved to ") + record_file_current_filename; show_notification(app, "GPU Screen Recorder", notification_body.c_str(), G_NOTIFICATION_PRIORITY_NORMAL); } - } else { - show_notification(app, "GPU Screen Recorder", "Failed to save video. Either your graphics card doesn't support GPU Screen Recorder with the settings you used or you don't have enough disk space to record a video", G_NOTIFICATION_PRIORITY_URGENT); } return true; } save_configs(); - int fps = gtk_spin_button_get_value_as_int(fps_entry); - int record_width = wayland ? 0 : gtk_spin_button_get_value_as_int(area_width_entry); - int record_height = wayland ? 0 : gtk_spin_button_get_value_as_int(area_height_entry); - - char dir_tmp[PATH_MAX]; - strcpy(dir_tmp, dir); - if(create_directory_recursive(dir_tmp) != 0) { - std::string notification_body = std::string("Failed to start recording. Failed to create ") + dir_tmp; - show_notification(app, "GPU Screen Recorder", notification_body.c_str(), G_NOTIFICATION_PRIORITY_URGENT); - return true; - } - - record_file_current_filename = std::string(dir_tmp) + "/Video_" + get_date_str() + "." + gtk_combo_box_text_get_active_text(record_container); + const int fps = gtk_spin_button_get_value_as_int(fps_entry); bool follow_focused = false; std::string window_str = record_area_selection_menu_get_active_id(); @@ -1926,40 +2061,76 @@ static gboolean on_start_recording_button_click(GtkButton *button, gpointer user return true; } window_str = std::to_string(select_window_userdata.selected_window); + if(!validate_window(app, select_window_userdata.selected_window)) + return true; } else if(window_str == "focused") { follow_focused = true; } - std::string fps_str = std::to_string(fps); + const std::string fps_str = std::to_string(fps); + const bool change_video_resolution = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(change_video_resolution_button)); const gchar* container_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(record_container)); + const gchar* container_name = gtk_combo_box_text_get_active_text(record_container); const gchar* color_range_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(color_range_input_menu)); const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); - const gchar* video_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(video_codec_input_menu)); const gchar* audio_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_codec_input_menu)); const gchar* framerate_mode_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(framerate_mode_input_menu)); const bool record_cursor = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_cursor_button)); + const bool restore_portal_session = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(restore_portal_session_button)); + const std::string video_bitrate_str = std::to_string(gtk_spin_button_get_value_as_int(video_bitrate_entry)); + + const int record_width = follow_focused ? gtk_spin_button_get_value_as_int(area_width_entry) : gtk_spin_button_get_value_as_int(video_width_entry); + const int record_height = follow_focused ? gtk_spin_button_get_value_as_int(area_height_entry) : gtk_spin_button_get_value_as_int(video_height_entry); + + const char *encoder = "gpu"; + std::string video_codec_input_str = video_codec_selection_menu_get_active_id(); + if(video_codec_input_str == "h264_software") { + video_codec_input_str = "h264"; + encoder = "cpu"; + } else if(video_codec_input_str == "auto") { + if(!switch_video_codec_to_usable_hardware_encoder(video_codec_input_str)) { + video_codec_input_str = "h264"; + encoder = "cpu"; + } + } + + change_container_if_codec_not_supported(video_codec_input_str, &container_str); + + char dir_tmp[PATH_MAX]; + strcpy(dir_tmp, dir); + if(create_directory_recursive(dir_tmp) != 0) { + std::string notification_body = std::string("Failed to start recording. Failed to create ") + dir_tmp; + show_notification(app, "GPU Screen Recorder", notification_body.c_str(), G_NOTIFICATION_PRIORITY_URGENT); + return true; + } + + record_file_current_filename = std::string(dir_tmp) + "/Video_" + get_date_str() + "." + container_name; char area[64]; snprintf(area, sizeof(area), "%dx%d", record_width, record_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-q", quality_input_str, "-k", video_codec_input_str, "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-cr", color_range_input_str, "-o", record_file_current_filename.c_str() + "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-k", video_codec_input_str.c_str(), "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-restore-portal-session", restore_portal_session ? "yes" : "no", "-cr", color_range_input_str, "-encoder", encoder, "-o", record_file_current_filename.c_str() }; + add_quality_command_line_args(args, quality_input_str, video_bitrate_str.c_str()); + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(overclock_button))) args.insert(args.end(), { "-oc", "yes" }); if(strcmp(framerate_mode_input_str, "auto") != 0) args.insert(args.end(), { "-fm", framerate_mode_input_str }); - std::string merge_audio_tracks_arg_value; - add_audio_command_line_args(args, merge_audio_tracks_arg_value); + std::string merge_audio_tracks; + const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(merge_audio_tracks); + add_audio_command_line_args(args, audio_tracks, merge_audio_tracks); - if(follow_focused) + if(follow_focused || change_video_resolution) args.insert(args.end(), { "-s", area }); args.push_back(NULL); + debug_print_args(args.data()); if(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item))) { hide_window(); @@ -1979,7 +2150,7 @@ static gboolean on_start_recording_button_click(GtkButton *button, gpointer user if(getppid() != parent_pid) _exit(3); - + execvp(args[0], (char* const*)args.data()); perror("failed to launch gpu-screen-recorder"); _exit(127); @@ -1996,7 +2167,10 @@ static gboolean on_start_recording_button_click(GtkButton *button, gpointer user gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_recording_menu_item), "Stop recording"); gtk_widget_set_sensitive(pause_recording_menu_item, true); - app_indicator_set_icon_full(app_indicator, tray_recording_icon_name, "recording"); + app_indicator_set_icon_full(app_indicator, get_tray_recording_icon_name(), "Recording"); + + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button))) + show_notification(app, "GPU Screen Recorder", "Started recording", G_NOTIFICATION_PRIORITY_NORMAL); record_start_time_sec = clock_get_monotonic_seconds(); paused_time_offset_sec = 0.0; @@ -2009,7 +2183,8 @@ static gboolean on_start_streaming_button_click(GtkButton *button, gpointer user int exit_status = prev_exit_status; prev_exit_status = -1; if(streaming) { - bool exit_success = kill_gpu_screen_recorder_get_result(); + bool already_dead = true; + bool exit_success = kill_gpu_screen_recorder_get_result(&already_dead); gtk_button_set_label(button, "Start streaming"); streaming = false; @@ -2019,15 +2194,19 @@ static gboolean on_start_streaming_button_click(GtkButton *button, gpointer user gtk_label_set_text(GTK_LABEL(streaming_record_time_label), "00:00:00"); gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_streaming_menu_item), "Start streaming"); - app_indicator_set_icon_full(app_indicator, tray_idle_icon_name, "Idle"); + app_indicator_set_icon_full(app_indicator, get_tray_idle_icon_name(), "Idle"); if(exit_status == 10) { - show_notification(app, "GPU Screen Recorder", - "You need to have pkexec installed and a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); - } else if(exit_success) { - show_notification(app, "GPU Screen Recorder", "Stopped streaming", G_NOTIFICATION_PRIORITY_NORMAL); - } else { + show_notification(app, "GPU Screen Recorder", "You need to have pkexec installed and have a polkit agent running to record your monitor", G_NOTIFICATION_PRIORITY_URGENT); + } else if(exit_status == 50) { + show_notification(app, "GPU Screen Recorder", "Desktop portal capture failed. Either you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system", G_NOTIFICATION_PRIORITY_URGENT); + } else if(exit_status == 60) { + // Canceled by the user + } else if(!exit_success || (already_dead && exit_status != 0)) { show_notification(app, "GPU Screen Recorder", "Failed to stream video. There is either an error in your streaming config or your graphics card doesn't support GPU Screen Recorder with the settings you used", G_NOTIFICATION_PRIORITY_URGENT); + } else if(exit_success) { + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button))) + show_notification(app, "GPU Screen Recorder", "Stopped streaming", G_NOTIFICATION_PRIORITY_NORMAL); } return true; @@ -2035,9 +2214,7 @@ static gboolean on_start_streaming_button_click(GtkButton *button, gpointer user save_configs(); - int fps = gtk_spin_button_get_value_as_int(fps_entry); - int record_width = wayland ? 0 : gtk_spin_button_get_value_as_int(area_width_entry); - int record_height = wayland ? 0 : gtk_spin_button_get_value_as_int(area_height_entry); + const int fps = gtk_spin_button_get_value_as_int(fps_entry); bool follow_focused = false; std::string window_str = record_area_selection_menu_get_active_id(); @@ -2047,10 +2224,13 @@ static gboolean on_start_streaming_button_click(GtkButton *button, gpointer user return true; } window_str = std::to_string(select_window_userdata.selected_window); + if(!validate_window(app, select_window_userdata.selected_window)) + return true; } else if(window_str == "focused") { follow_focused = true; } - std::string fps_str = std::to_string(fps); + const std::string fps_str = std::to_string(fps); + const bool change_video_resolution = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(change_video_resolution_button)); std::string stream_url; const gchar *container_str = "flv"; @@ -2086,31 +2266,53 @@ static gboolean on_start_streaming_button_click(GtkButton *button, gpointer user const gchar* quality_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); const gchar* color_range_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(color_range_input_menu)); - const gchar* video_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(video_codec_input_menu)); const gchar* audio_codec_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_codec_input_menu)); const gchar* framerate_mode_input_str = gtk_combo_box_get_active_id(GTK_COMBO_BOX(framerate_mode_input_menu)); const bool record_cursor = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(record_cursor_button)); + const bool restore_portal_session = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(restore_portal_session_button)); + const std::string video_bitrate_str = std::to_string(gtk_spin_button_get_value_as_int(video_bitrate_entry)); + + const int record_width = follow_focused ? gtk_spin_button_get_value_as_int(area_width_entry) : gtk_spin_button_get_value_as_int(video_width_entry); + const int record_height = follow_focused ? gtk_spin_button_get_value_as_int(area_height_entry) : gtk_spin_button_get_value_as_int(video_height_entry); + + const char *encoder = "gpu"; + std::string video_codec_input_str = video_codec_selection_menu_get_active_id(); + if(video_codec_input_str == "h264_software") { + video_codec_input_str = "h264"; + encoder = "cpu"; + } else if(video_codec_input_str == "auto") { + if(!switch_video_codec_to_usable_hardware_encoder(video_codec_input_str)) { + video_codec_input_str = "h264"; + encoder = "cpu"; + } + } + + change_container_if_codec_not_supported(video_codec_input_str, &container_str); char area[64]; snprintf(area, sizeof(area), "%dx%d", record_width, record_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-q", quality_input_str, "-k", video_codec_input_str, "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-cr", color_range_input_str, "-o", stream_url.c_str() + "gpu-screen-recorder", "-w", window_str.c_str(), "-c", container_str, "-k", video_codec_input_str.c_str(), "-ac", audio_codec_input_str, "-f", fps_str.c_str(), "-cursor", record_cursor ? "yes" : "no", "-restore-portal-session", restore_portal_session ? "yes" : "no", "-cr", color_range_input_str, "-encoder", encoder, "-o", stream_url.c_str() }; + add_quality_command_line_args(args, quality_input_str, video_bitrate_str.c_str()); + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(overclock_button))) args.insert(args.end(), { "-oc", "yes" }); if(strcmp(framerate_mode_input_str, "auto") != 0) args.insert(args.end(), { "-fm", framerate_mode_input_str }); - std::string merge_audio_tracks_arg_value; - add_audio_command_line_args(args, merge_audio_tracks_arg_value); + std::string merge_audio_tracks; + const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(merge_audio_tracks); + add_audio_command_line_args(args, audio_tracks, merge_audio_tracks); - if(follow_focused) + if(follow_focused || change_video_resolution) args.insert(args.end(), { "-s", area }); args.push_back(NULL); + debug_print_args(args.data()); if(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item))) { hide_window(); @@ -2146,7 +2348,10 @@ static gboolean on_start_streaming_button_click(GtkButton *button, gpointer user gtk_widget_set_opacity(GTK_WIDGET(streaming_bottom_panel_grid), 1.0); gtk_menu_item_set_label(GTK_MENU_ITEM(start_stop_streaming_menu_item), "Stop streaming"); - app_indicator_set_icon_full(app_indicator, tray_recording_icon_name, "Recording"); + app_indicator_set_icon_full(app_indicator, get_tray_recording_icon_name(), "Recording"); + + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button))) + show_notification(app, "GPU Screen Recorder", "Started streaming", G_NOTIFICATION_PRIORITY_NORMAL); record_start_time_sec = clock_get_monotonic_seconds(); return true; @@ -2181,11 +2386,13 @@ static void gtk_widget_set_margin(GtkWidget *widget, int top, int bottom, int le static void record_area_item_change_callback(GtkComboBox *widget, gpointer userdata) { (void)widget; - GtkWidget *select_window_button = (GtkWidget*)userdata; + (void)userdata; const std::string selected_window_area = record_area_selection_menu_get_active_id(); - gtk_widget_set_visible(select_window_button, strcmp(selected_window_area.c_str(), "window") == 0); - gtk_widget_set_visible(GTK_WIDGET(area_size_label), strcmp(selected_window_area.c_str(), "focused") == 0); + gtk_widget_set_visible(GTK_WIDGET(select_window_button), strcmp(selected_window_area.c_str(), "window") == 0); gtk_widget_set_visible(GTK_WIDGET(area_size_grid), strcmp(selected_window_area.c_str(), "focused") == 0); + gtk_widget_set_visible(GTK_WIDGET(restore_portal_session_button), strcmp(selected_window_area.c_str(), "portal") == 0); + gtk_widget_set_visible(GTK_WIDGET(change_video_resolution_button), strcmp(selected_window_area.c_str(), "focused") != 0); + on_change_video_resolution_button_click(GTK_BUTTON(change_video_resolution_button), nullptr); enable_stream_record_button_if_info_filled(); } @@ -2197,8 +2404,16 @@ static void view_combo_box_change_callback(GtkComboBox *widget, gpointer userdat gtk_widget_set_visible(GTK_WIDGET(video_codec_grid), advanced_view); gtk_widget_set_visible(GTK_WIDGET(audio_codec_grid), advanced_view); gtk_widget_set_visible(GTK_WIDGET(framerate_mode_grid), advanced_view); - gtk_widget_set_visible(GTK_WIDGET(overclock_grid), advanced_view && gpu_inf.vendor == GPU_VENDOR_NVIDIA && !wayland); - gtk_widget_set_visible(GTK_WIDGET(show_notification_button), advanced_view); + gtk_widget_set_visible(GTK_WIDGET(overclock_grid), advanced_view && gsr_info.gpu_info.vendor == GpuVendor::NVIDIA && gsr_info.system_info.display_server != DisplayServer::WAYLAND); + gtk_widget_set_visible(GTK_WIDGET(notifications_frame), advanced_view); + gtk_widget_set_visible(GTK_WIDGET(split_audio_button), advanced_view); +} + +static void quality_combo_box_change_callback(GtkComboBox *widget, gpointer userdata) { + (void)userdata; + const gchar *selected_view = gtk_combo_box_get_active_id(widget); + const bool custom_selected = strcmp(selected_view, "custom") == 0; + gtk_widget_set_visible(GTK_WIDGET(video_bitrate_grid), custom_selected); } static void stream_service_item_change_callback(GtkComboBox *widget, gpointer userdata) { @@ -2250,26 +2465,22 @@ static bool is_cuda_installed() { return lib != nullptr; } -typedef gboolean (*KeyPressHandler)(GtkButton *button, gpointer userdata); -static void keypress_toggle_recording(bool recording_state, GtkButton *record_button, KeyPressHandler keypress_handler, GtkApplication *app) { - if(!gtk_widget_get_sensitive(GTK_WIDGET(record_button))) - return; - - if(!recording_state) { - keypress_handler(record_button, app); - } else if(recording_state) { - keypress_handler(record_button, app); +static bool is_hotkey_already_bound_to_another_action_on_same_page(const Hotkey *_current_hotkey, const Hotkey new_hotkey, GtkWidget *page) { + for(int i = 0; i < num_hotkeys; ++i) { + if(hotkeys[i] != _current_hotkey && hotkeys[i]->page == page && hotkeys[i]->keysym == new_hotkey.keysym && hotkeys[i]->modkey_mask == new_hotkey.modkey_mask) + return true; } + return false; } static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpointer userdata) { - if(wayland) + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return GDK_FILTER_CONTINUE; if(hotkey_mode == HotkeyMode::NoAction) return GDK_FILTER_CONTINUE; - PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; + PageNavigationUserdata *_page_navigation_userdata = (PageNavigationUserdata*)userdata; XEvent *ev = (XEvent*)xevent; if(ev->type != KeyPress && ev->type != KeyRelease) return GDK_FILTER_CONTINUE; @@ -2278,23 +2489,13 @@ static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpoi KeySym key_sym = XLookupKeysym(&ev->xkey, 0); if(hotkey_mode == HotkeyMode::Record && ev->type == KeyRelease) { - const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata->stack); - if(visible_page == page_navigation_userdata->recording_page) { - if(key_sym == record_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(record_hotkey.modkey_mask)) { - keypress_toggle_recording(recording, start_recording_button, on_start_recording_button_click, page_navigation_userdata->app); - } else if(key_sym == pause_unpause_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(pause_unpause_hotkey.modkey_mask) && recording) { - on_pause_unpause_button_click(nullptr, page_navigation_userdata->app); - } - } else if(visible_page == page_navigation_userdata->streaming_page) { - if(key_sym == streaming_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(streaming_hotkey.modkey_mask)) { - keypress_toggle_recording(streaming, start_streaming_button, on_start_streaming_button_click, page_navigation_userdata->app); - } - } else if(visible_page == page_navigation_userdata->replay_page) { - if(key_sym == replay_start_stop_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(replay_start_stop_hotkey.modkey_mask)) { - keypress_toggle_recording(replaying, start_replay_button, on_start_replay_button_click, page_navigation_userdata->app); - } else if(key_sym == replay_save_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(replay_save_hotkey.modkey_mask) && replaying && gpu_screen_recorder_process != -1) { - on_replay_save_button_click(nullptr, page_navigation_userdata->app); - } + const GtkWidget *visible_page = gtk_stack_get_visible_child(_page_navigation_userdata->stack); + for(int i = 0; i < num_hotkeys; ++i) { + if(visible_page != hotkeys[i]->page) + continue; + + if(key_sym == hotkeys[i]->keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(hotkeys[i]->modkey_mask)) + hotkeys[i]->trigger_handler(hotkeys[i]->associated_button, _page_navigation_userdata->app); } return GDK_FILTER_CONTINUE; } @@ -2314,7 +2515,9 @@ static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpoi if(ev->type == KeyPress && key_sym == XK_BackSpace) { if(pressed_hotkey.modkey_mask == None && pressed_hotkey.keysym == None) { ungrab_keyboard(display); + grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), *current_hotkey, false); gtk_entry_set_text(GTK_ENTRY(current_hotkey->hotkey_entry), ""); + current_hotkey->grab_success = true; current_hotkey->keysym = None; current_hotkey->modkey_mask = 0; current_hotkey = nullptr; @@ -2339,7 +2542,7 @@ static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpoi latest_hotkey = pressed_hotkey; set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), latest_hotkey); } - + if(ev->type == KeyRelease) { if(key_is_modifier(key_sym)) { pressed_hotkey.modkey_mask &= ~modkey_to_mask(key_sym); @@ -2354,57 +2557,43 @@ static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpoi if(pressed_hotkey.modkey_mask == None && pressed_hotkey.keysym == None) { ungrab_keyboard(display); - ungrab_keys(gdk_x11_get_default_xdisplay()); - bool hotkey_already_used_by_another_hotkey = false; - if(current_hotkey == &replay_start_stop_hotkey) - hotkey_already_used_by_another_hotkey = (latest_hotkey.keysym == replay_save_hotkey.keysym && latest_hotkey.modkey_mask == replay_save_hotkey.modkey_mask); - else if(current_hotkey == &replay_save_hotkey) - hotkey_already_used_by_another_hotkey = (latest_hotkey.keysym == replay_start_stop_hotkey.keysym && latest_hotkey.modkey_mask == replay_start_stop_hotkey.modkey_mask); - - if(hotkey_already_used_by_another_hotkey) { - std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); - std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used for something else. Please choose another hotkey."; + if(is_hotkey_already_bound_to_another_action_on_same_page(current_hotkey, latest_hotkey, gtk_stack_get_visible_child(_page_navigation_userdata->stack))) { + const std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); + const std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used for something else. Please choose another hotkey."; set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), *current_hotkey); GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); current_hotkey = nullptr; + hotkey_mode = HotkeyMode::Record; return GDK_FILTER_CONTINUE; } - Hotkey prev_current_hotkey = *current_hotkey; + const Hotkey prev_current_hotkey = *current_hotkey; current_hotkey->keysym = latest_hotkey.keysym; current_hotkey->modkey_mask = latest_hotkey.modkey_mask; - HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); - bool hotkey_success = false; - if(current_hotkey == &record_hotkey) - hotkey_success = hotkey_result.record_hotkey_success; - else if(current_hotkey == &pause_unpause_hotkey) - hotkey_success = hotkey_result.pause_unpause_hotkey_success; - else if(current_hotkey == &streaming_hotkey) - hotkey_success = hotkey_result.streaming_hotkey_success; - else if(current_hotkey == &replay_start_stop_hotkey) - hotkey_success = hotkey_result.replay_start_stop_hotkey_success; - else if(current_hotkey == &replay_save_hotkey) - hotkey_success = hotkey_result.replay_save_hotkey_success; - - if(hotkey_success) { + const std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); + if(replace_grabbed_keys_depending_on_active_page()) { save_configs(); + current_hotkey = nullptr; + hotkey_mode = HotkeyMode::Record; + return GDK_FILTER_CONTINUE; } else { - *current_hotkey = prev_current_hotkey; - std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); - std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey."; + const std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey."; + + current_hotkey->keysym = prev_current_hotkey.keysym; + current_hotkey->modkey_mask = prev_current_hotkey.modkey_mask; set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), *current_hotkey); + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - } - current_hotkey = nullptr; - return GDK_FILTER_CONTINUE; + return GDK_FILTER_CONTINUE; + } } } @@ -2412,35 +2601,30 @@ static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpoi } static gboolean on_hotkey_entry_click(GtkWidget *button, gpointer) { - if(wayland) + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return true; - hotkey_mode = HotkeyMode::NewHotkey; - pressed_hotkey.hotkey_entry = nullptr; pressed_hotkey.hotkey_active_label = nullptr; pressed_hotkey.keysym = None; pressed_hotkey.modkey_mask = 0; latest_hotkey = pressed_hotkey; - if(button == record_hotkey_button) { - current_hotkey = &record_hotkey; - } else if(button == pause_unpause_hotkey_button) { - current_hotkey = &pause_unpause_hotkey; - } else if(button == streaming_hotkey_button) { - current_hotkey = &streaming_hotkey; - } else if(button == replay_start_stop_hotkey_button) { - current_hotkey = &replay_start_stop_hotkey; - } else if(button == replay_save_hotkey_button) { - current_hotkey = &replay_save_hotkey; - } else { - current_hotkey = nullptr; + current_hotkey = nullptr; + for(int i = 0; i < num_hotkeys; ++i) { + if(button == hotkeys[i]->hotkey_entry) { + current_hotkey = hotkeys[i]; + break; + } } - if(current_hotkey) { - gtk_grab_add(current_hotkey->hotkey_entry); - gtk_widget_set_visible(current_hotkey->hotkey_active_label, true); - } + if(!current_hotkey) + return true; + + hotkey_mode = HotkeyMode::NewHotkey; + + gtk_grab_add(current_hotkey->hotkey_entry); + gtk_widget_set_visible(current_hotkey->hotkey_active_label, true); Display *display = gdk_x11_get_default_xdisplay(); XErrorHandler prev_error_handler = XSetErrorHandler(xerror_dummy); @@ -2450,87 +2634,340 @@ static gboolean on_hotkey_entry_click(GtkWidget *button, gpointer) { return true; } -static bool audio_inputs_contains(const std::vector<AudioInput> &audio_inputs, const std::string &audio_input_name) { - for(auto &audio_input : audio_inputs) { - if(audio_input.name == audio_input_name) - return true; +static void parse_system_info_line(GsrInfo *_gsr_info, const std::string &line) { + const size_t space_index = line.find('|'); + if(space_index == std::string::npos) + return; + + const StringView attribute_name = {line.c_str(), space_index}; + const StringView attribute_value = {line.c_str() + space_index + 1, line.size() - (space_index + 1)}; + if(attribute_name == "display_server") { + if(attribute_value == "x11") + _gsr_info->system_info.display_server = DisplayServer::X11; + else if(attribute_value == "wayland") + _gsr_info->system_info.display_server = DisplayServer::WAYLAND; + } else if(attribute_name == "is_steam_deck") { + _gsr_info->system_info.is_steam_deck = attribute_value == "yes"; + } else if(attribute_name == "supports_app_audio") { + _gsr_info->system_info.supports_app_audio = attribute_value == "yes"; } - return false; } -static gsr_connection_type get_connection_type() { - if(wayland || gpu_inf.vendor != GPU_VENDOR_NVIDIA) { - return GSR_CONNECTION_DRM; - } else { - return GSR_CONNECTION_X11; +static void parse_gpu_info_line(GsrInfo *_gsr_info, const std::string &line) { + const size_t space_index = line.find('|'); + if(space_index == std::string::npos) + return; + + const StringView attribute_name = {line.c_str(), space_index}; + const StringView attribute_value = {line.c_str() + space_index + 1, line.size() - (space_index + 1)}; + if(attribute_name == "vendor") { + if(attribute_value == "amd") + _gsr_info->gpu_info.vendor = GpuVendor::AMD; + else if(attribute_value == "intel") + _gsr_info->gpu_info.vendor = GpuVendor::INTEL; + else if(attribute_value == "nvidia") + _gsr_info->gpu_info.vendor = GpuVendor::NVIDIA; + else if(attribute_value == "broadcom") + _gsr_info->gpu_info.vendor = GpuVendor::BROADCOM; } } -// Returns the exit status -static int get_supported_video_codecs(SupportedVideoCodecs *supported_video_codecs) { - supported_video_codecs->h264 = false; - supported_video_codecs->hevc = false; - supported_video_codecs->av1 = false; +static void parse_video_codecs_line(GsrInfo *_gsr_info, const std::string &line) { + if(line == "h264") + _gsr_info->supported_video_codecs.h264 = true; + else if(line == "h264_software") + _gsr_info->supported_video_codecs.h264_software = true; + else if(line == "hevc") + _gsr_info->supported_video_codecs.hevc = true; + else if(line == "hevc_hdr") + _gsr_info->supported_video_codecs.hevc_hdr = true; + else if(line == "hevc_10bit") + _gsr_info->supported_video_codecs.hevc_10bit = true; + else if(line == "av1") + _gsr_info->supported_video_codecs.av1 = true; + else if(line == "av1_hdr") + _gsr_info->supported_video_codecs.av1_hdr = true; + else if(line == "av1_10bit") + _gsr_info->supported_video_codecs.av1_10bit = true; + else if(line == "vp8") + _gsr_info->supported_video_codecs.vp8 = true; + else if(line == "vp9") + _gsr_info->supported_video_codecs.vp9 = true; +} + +static GsrMonitor capture_option_line_to_monitor(const std::string &line) { + size_t space_index = line.find('|'); + if(space_index == std::string::npos) + return { line, {0, 0} }; + + vec2i size = {0, 0}; + if(sscanf(line.c_str() + space_index + 1, "%dx%d", &size.x, &size.y) != 2) + size = {0, 0}; + + return { line.substr(0, space_index), size }; +} + +static void parse_capture_options_line(GsrInfo *_gsr_info, const std::string &line) { + if(line == "window") + _gsr_info->supported_capture_options.window = true; + else if(line == "focused") + _gsr_info->supported_capture_options.focused = true; + else if(line == "portal") + _gsr_info->supported_capture_options.portal = true; + else if(line != "region") // We dont support region capture in the gtk application + _gsr_info->supported_capture_options.monitors.push_back(capture_option_line_to_monitor(line)); +} + +enum class GsrInfoSection { + UNKNOWN, + SYSTEM_INFO, + GPU_INFO, + VIDEO_CODECS, + CAPTURE_OPTIONS +}; + +static GsrInfoExitStatus get_gpu_screen_recorder_info(GsrInfo *_gsr_info) { + *_gsr_info = GsrInfo{}; - FILE *f = popen("gpu-screen-recorder --list-supported-video-codecs", "r"); + FILE *f = popen("gpu-screen-recorder --info", "r"); if(!f) { - fprintf(stderr, "error: 'gpu-screen-recorder --list-supported-video-codecs' failed\n"); - return -1; + fprintf(stderr, "error: 'gpu-screen-recorder --info' failed\n"); + return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; } - char output[1024]; + char output[8192]; ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f); if(bytes_read < 0 || ferror(f)) { - fprintf(stderr, "error: failed to read 'gpu-screen-recorder --list-supported-video-codecs' output\n"); + fprintf(stderr, "error: failed to read 'gpu-screen-recorder --info' output\n"); pclose(f); - return -1; + return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; } output[bytes_read] = '\0'; - if(strstr(output, "h264")) - supported_video_codecs->h264 = true; - if(strstr(output, "hevc")) - supported_video_codecs->hevc = true; - if(strstr(output, "av1")) - supported_video_codecs->av1 = true; + GsrInfoSection section = GsrInfoSection::UNKNOWN; + string_split_char(output, '\n', [&](StringView line) { + const std::string line_str(line.str, line.size); + + if(starts_with(line_str, "section=")) { + const char *section_name = line_str.c_str() + 8; + if(strcmp(section_name, "system_info") == 0) + section = GsrInfoSection::SYSTEM_INFO; + else if(strcmp(section_name, "gpu_info") == 0) + section = GsrInfoSection::GPU_INFO; + else if(strcmp(section_name, "video_codecs") == 0) + section = GsrInfoSection::VIDEO_CODECS; + else if(strcmp(section_name, "capture_options") == 0) + section = GsrInfoSection::CAPTURE_OPTIONS; + else + section = GsrInfoSection::UNKNOWN; + return true; + } + + switch(section) { + case GsrInfoSection::UNKNOWN: { + break; + } + case GsrInfoSection::SYSTEM_INFO: { + parse_system_info_line(_gsr_info, line_str); + break; + } + case GsrInfoSection::GPU_INFO: { + parse_gpu_info_line(_gsr_info, line_str); + break; + } + case GsrInfoSection::VIDEO_CODECS: { + parse_video_codecs_line(_gsr_info, line_str); + break; + } + case GsrInfoSection::CAPTURE_OPTIONS: { + parse_capture_options_line(_gsr_info, line_str); + break; + } + } + + return true; + }); int status = pclose(f); - if(WIFEXITED(status)) - return WEXITSTATUS(status); - return 0; + if(WIFEXITED(status)) { + switch(WEXITSTATUS(status)) { + case 0: return GsrInfoExitStatus::OK; + case 22: return GsrInfoExitStatus::OPENGL_FAILED; + case 23: return GsrInfoExitStatus::NO_DRM_CARD; + default: return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; + } + } + + return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; } static void record_area_set_sensitive(GtkCellLayout *cell_layout, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data) { (void)cell_layout; (void)data; - if(wayland) { - gchar *id; - gtk_tree_model_get(tree_model, iter, 1, &id, -1); - gboolean sensitive = g_strcmp0("window", id) != 0 && g_strcmp0("focused", id) != 0; - g_free(id); - g_object_set(cell, "sensitive", sensitive, NULL); - } else { - g_object_set(cell, "sensitive", true, NULL); + + gchar *id; + gtk_tree_model_get(tree_model, iter, 1, &id, -1); + g_object_set(cell, "sensitive", is_video_capture_option_enabled(id), NULL); + g_free(id); +} + +static void video_codec_set_sensitive(GtkCellLayout *cell_layout, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data) { + (void)cell_layout; + (void)data; + + gchar *id; + gtk_tree_model_get(tree_model, iter, 1, &id, -1); + g_object_set(cell, "sensitive", is_video_codec_enabled(id), NULL); + g_free(id); +} + +static void launch_gsr_ui(bool launched_by_daemon) { + const char *args[] = { "gsr-ui", launched_by_daemon ? "launch-daemon" : "launch-show", nullptr }; + execvp(args[0], (char* const*)args); + // TODO: This is incorrect because window wont be defined here if this is called from startup. + // This is fine for not because this is only called inside the flatpak where gsr-ui is always available. + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "gsr-ui (gpu-screen-recorder-ui) isn't installed. Please install it first."); + set_dialog_selectable(dialog); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +static bool kms_server_proxy_setup_gsr_ui(const char *msg) { + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "%s", msg); + const gint response = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + switch(response) { + case GTK_RESPONSE_YES: + break; + case GTK_RESPONSE_NO: + default: { + config.main_config.use_new_ui = false; + save_config(config); + return false; + } } + + const int exit_code = system("flatpak-spawn --host -- /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy setup-gsr-ui"); + if(exit_code != 0) { + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Failed to setup the new UI. You either cancelled the installation or you don't have pkexec installed and a polkit agent running."); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + config.main_config.use_new_ui = false; + save_configs(); + return false; + } + + config.main_config.use_new_ui = true; + config.main_config.installed_gsr_global_hotkeys_version = GSR_CURRENT_GLOBAL_HOTKEYS_CODE_VERSION; + save_config(config); + return true; } -static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *app, const gpu_info &gpu_inf) { - GtkGrid *grid = GTK_GRID(gtk_grid_new()); - gtk_stack_add_named(stack, GTK_WIDGET(grid), "common-settings"); - gtk_widget_set_vexpand(GTK_WIDGET(grid), true); - gtk_widget_set_hexpand(GTK_WIDGET(grid), true); - gtk_grid_set_row_spacing(grid, 10); - gtk_grid_set_column_spacing(grid, 10); - gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); +static gboolean on_click_switch_to_new_ui(GtkButton*, gpointer) { + if(!dpy) { + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "The new UI only works on X11 or through XWayland on Wayland. Native Wayland is not supported because Wayland is missing features required by this software.\n" + "Install X11 on your system to use the new UI."); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return true; + } + + if(!is_pkexec_installed()) { + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "pkexec needs to be installed to switch to the new UI"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return true; + } + + if(!flatpak_is_installed_as_system()) { + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "GPU Screen Recorder needs to be installed system-wide to use the new UI. You can run this command to install GPU Screen recorder system-wide:\n" + "flatpak install --system com.dec05eba.gpu_screen_recorder\n"); + set_dialog_selectable(dialog); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return true; + } + + GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, + "You are about to try out the new UI, which is a ShadowPlay-like fullscreen UI. It runs in the background and you have to show/hide it by pressing Left Alt+Z.\n" + "This new UI is still experimental and you may experience issues depending on your system, especially on Wayland since Wayland doesn't support this software properly.\n" + "You can switch back to the old UI at any time by opening the UI and clicking on the settings button and clicking on the \"Go back to the old UI\" button.\n" + "\n" + "This new UI comes with new features, such as being able to automatically launch it on system startup by enabling it in settings, and hotkey support on any Wayland compositor.\n" + "\n" + "If you are using keyboard remapping software such as keyd then make sure it ignores \"gsr-ui virtual keyboard\" (dec0:5eba device id), or it will prevent you from using your keyboard.\n" + "You can go back to the old UI by pressing (left) ctrl+shift+alt+esc if this happens.\n" + "\n" + "If you are using an NVIDIA GPU then you may experience issue with recording/replay if a suspend happens while recording/using replay. This is an NVIDIA driver issue and it also happens in the old UI.\n" + "See this for a workaround: <a href=\"https://wiki.archlinux.org/title/NVIDIA/Tips_and_tricks#Preserve_video_memory_after_suspend\">Arch Wiki - Preserve video memory after suspend</a>.\n" + "\n" + "Are you sure you want to switch to the new UI?"); + gint response = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + switch(response) { + case GTK_RESPONSE_YES: + break; + case GTK_RESPONSE_NO: + default: + return true; + } + + const bool kms_server_setup_finished = kms_server_proxy_setup_gsr_ui( + "The new UI needs root privileges to finish setup to make global hotkeys and recording work on any system. The new UI will also be added to system startup.\n" + "\n" + "Are you sure you want to continue?"); + if(!kms_server_setup_finished) + return true; + + bool service_install_successful = (system( + "data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && " + "flatpak-spawn --host -- install -Dm644 /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gpu-screen-recorder/gpu-screen-recorder-ui.service \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0); + service_install_successful &= (system("flatpak-spawn --host -- systemctl --user daemon-reload") == 0); + service_install_successful &= (system("flatpak-spawn --host -- systemctl enable --user gpu-screen-recorder-ui") == 0); + if(!service_install_successful) { + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, + "Failed to add GPU Screen Recorder to system startup. If you want the new UI to start on system startup then you need to add this command to system startup:\n" + "flatpak run com.dec05eba.gpu_screen_recorder gsr-ui"); + set_dialog_selectable(dialog); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + } + launch_gsr_ui(false); + g_application_quit(G_APPLICATION(select_window_userdata.app)); + return true; +} + +static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *app) { + GtkGrid *main_grid = GTK_GRID(gtk_grid_new()); + gtk_stack_add_named(stack, GTK_WIDGET(main_grid), "common-settings"); + gtk_widget_set_vexpand(GTK_WIDGET(main_grid), true); + gtk_widget_set_hexpand(GTK_WIDGET(main_grid), true); + gtk_grid_set_row_spacing(main_grid, 10); + gtk_grid_set_column_spacing(main_grid, 10); + gtk_widget_set_margin(GTK_WIDGET(main_grid), 10, 10, 10, 10); + + int main_grid_row = 0; int grid_row = 0; int record_area_row = 0; int audio_input_area_row = 0; + int video_input_area_row = 0; + int notifications_area_row = 0; GtkGrid *simple_advanced_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(simple_advanced_grid), 0, grid_row++, 2, 1); + gtk_grid_set_column_spacing(simple_advanced_grid, 10); + gtk_grid_attach(main_grid, GTK_WIDGET(simple_advanced_grid), 0, main_grid_row++, flatpak ? 3 : 2, 1); + gtk_grid_attach(simple_advanced_grid, gtk_label_new("View: "), 0, 0, 1, 1); view_combo_box = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(view_combo_box, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); gtk_combo_box_text_append(view_combo_box, "simple", "Simple"); gtk_combo_box_text_append(view_combo_box, "advanced", "Advanced"); gtk_widget_set_hexpand(GTK_WIDGET(view_combo_box), true); @@ -2538,8 +2975,41 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_combo_box_set_active(GTK_COMBO_BOX(view_combo_box), 0); g_signal_connect(view_combo_box, "changed", G_CALLBACK(view_combo_box_change_callback), view_combo_box); - GtkFrame *record_area_frame = GTK_FRAME(gtk_frame_new("Record area")); - gtk_grid_attach(grid, GTK_WIDGET(record_area_frame), 0, grid_row++, 2, 1); + if(flatpak) { + GtkButton *switch_to_new_ui_button = GTK_BUTTON(gtk_button_new_with_label("Try out the new UI")); + gtk_grid_attach(simple_advanced_grid, GTK_WIDGET(switch_to_new_ui_button), 2, 0, 1, 1); + g_signal_connect(switch_to_new_ui_button, "clicked", G_CALLBACK(on_click_switch_to_new_ui), nullptr); + } + + GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new(NULL, NULL)); + gtk_scrolled_window_set_min_content_width(scrolled_window, 100); + gtk_scrolled_window_set_min_content_height(scrolled_window, 100); + gtk_scrolled_window_set_max_content_width(scrolled_window, 1200); + gtk_scrolled_window_set_max_content_height(scrolled_window, 700); + gtk_scrolled_window_set_propagate_natural_width(scrolled_window, true); + gtk_scrolled_window_set_propagate_natural_height(scrolled_window, true); + gtk_grid_attach(main_grid, GTK_WIDGET(scrolled_window), 0, main_grid_row++, 2, 1); + + GtkGrid *scrolled_window_grid = GTK_GRID(gtk_grid_new()); + gtk_container_add(GTK_CONTAINER(scrolled_window), GTK_WIDGET(scrolled_window_grid)); + gtk_widget_set_vexpand(GTK_WIDGET(scrolled_window_grid), true); + gtk_widget_set_hexpand(GTK_WIDGET(scrolled_window_grid), true); + gtk_grid_set_row_spacing(scrolled_window_grid, 0); + gtk_grid_set_column_spacing(scrolled_window_grid, 0); + gtk_widget_set_margin(GTK_WIDGET(scrolled_window_grid), 0, 0, 0, 0); + + GtkGrid *grid = GTK_GRID(gtk_grid_new()); + gtk_container_add(GTK_CONTAINER(scrolled_window_grid), GTK_WIDGET(grid)); + gtk_widget_set_halign(GTK_WIDGET(grid), GTK_ALIGN_CENTER); + gtk_widget_set_valign(GTK_WIDGET(grid), GTK_ALIGN_START); + gtk_widget_set_vexpand(GTK_WIDGET(grid), true); + gtk_widget_set_hexpand(GTK_WIDGET(grid), true); + gtk_grid_set_row_spacing(grid, 10); + gtk_grid_set_column_spacing(grid, 10); + gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 25, 25); + + GtkFrame *capture_target_frame = GTK_FRAME(gtk_frame_new("Capture target")); + gtk_grid_attach(grid, GTK_WIDGET(capture_target_frame), 0, grid_row++, 2, 1); GtkGrid *record_area_grid = GTK_GRID(gtk_grid_new()); gtk_widget_set_vexpand(GTK_WIDGET(record_area_grid), false); @@ -2547,114 +3017,158 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_grid_set_row_spacing(record_area_grid, 10); gtk_grid_set_column_spacing(record_area_grid, 10); gtk_widget_set_margin(GTK_WIDGET(record_area_grid), 10, 10, 10, 10); - gtk_container_add(GTK_CONTAINER(record_area_frame), GTK_WIDGET(record_area_grid)); + gtk_container_add(GTK_CONTAINER(capture_target_frame), GTK_WIDGET(record_area_grid)); GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); GtkTreeIter iter; record_area_selection_model = GTK_TREE_MODEL(store); - if(wayland) { + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, "Window (Unavailable on Wayland)", -1); + gtk_list_store_set(store, &iter, 0, "Window (Not available on Wayland)", -1); gtk_list_store_set(store, &iter, 1, "window", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, "Follow focused window (Unavailable on Wayland)", -1); + gtk_list_store_set(store, &iter, 0, "Follow focused window (Not available on Wayland)", -1); gtk_list_store_set(store, &iter, 1, "focused", -1); } else { gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, "Window", -1, "window", -1); + gtk_list_store_set(store, &iter, 0, "Window", -1); gtk_list_store_set(store, &iter, 1, "window", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, "Follow focused window", -1, "focused", -1); + gtk_list_store_set(store, &iter, 0, "Follow focused window", -1); gtk_list_store_set(store, &iter, 1, "focused", -1); } - const bool allow_screen_capture = wayland || nvfbc_installed || gpu_inf.vendor != GPU_VENDOR_NVIDIA; + const bool allow_screen_capture = is_monitor_capture_drm() || nvfbc_installed; if(allow_screen_capture) { - if(!wayland && gpu_inf.vendor == GPU_VENDOR_NVIDIA) { - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, "All monitors", -1); - gtk_list_store_set(store, &iter, 1, "screen", -1); - } - - const gsr_connection_type connection_type = get_connection_type(); - int num_monitors = 0; - for_each_active_monitor_output(&egl, connection_type, [&](const gsr_monitor *monitor, void*) { + for(const auto &monitor : gsr_info.supported_capture_options.monitors) { std::string label = "Monitor "; - label.append(monitor->name, monitor->name_len); + label += monitor.name; label += " ("; - label += std::to_string(monitor->size.x); + label += std::to_string(monitor.size.x); label += "x"; - label += std::to_string(monitor->size.y); - if(flatpak && (wayland || gpu_inf.vendor != GPU_VENDOR_NVIDIA)) { + label += std::to_string(monitor.size.y); + if(flatpak && is_monitor_capture_drm()) { label += ", requires root access"; } label += ")"; // Leak on purpose, what are you gonna do? stab me? - char *id = (char*)malloc(monitor->name_len + 1); - memcpy(id, monitor->name, monitor->name_len); - id[monitor->name_len] = '\0'; + char *id = (char*)malloc(monitor.name.size() + 1); + memcpy(id, monitor.name.c_str(), monitor.name.size()); + id[monitor.name.size()] = '\0'; gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, label.c_str(), -1); gtk_list_store_set(store, &iter, 1, id, -1); + } - ++num_monitors; - }, NULL); - - if(num_monitors == 0 && (wayland || gpu_inf.vendor != GPU_VENDOR_NVIDIA)) { + if(gsr_info.supported_capture_options.monitors.empty() && gsr_info.system_info.display_server == DisplayServer::WAYLAND && !gsr_info.supported_capture_options.portal) { GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "No monitors to record found. Make sure GPU Screen Recorder is running on the same GPU device that is displaying graphics on the screen."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); g_application_quit(G_APPLICATION(select_window_userdata.app)); - return GTK_WIDGET(grid); + return GTK_WIDGET(main_grid); // TODO: ??? } } + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_capture_options.portal ? "Desktop portal (HDR not supported)" : "Desktop portal (Not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "portal", -1); + } else { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, "Desktop portal (Not available on X11)", -1); + gtk_list_store_set(store, &iter, 1, "portal", -1); + } + record_area_selection_menu = GTK_COMBO_BOX(gtk_combo_box_new_with_model(record_area_selection_model)); + g_signal_connect(record_area_selection_menu, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(record_area_selection_menu), renderer, TRUE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(record_area_selection_menu), renderer, "text", 0, NULL); gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(record_area_selection_menu), renderer, record_area_set_sensitive, NULL, NULL); - gtk_combo_box_set_active(record_area_selection_menu, allow_screen_capture ? 2 : 0); + if(allow_screen_capture && !gsr_info.supported_capture_options.portal && gsr_info.supported_capture_options.monitors.empty()) + gtk_combo_box_set_active(record_area_selection_menu, 0); + else if(allow_screen_capture || gsr_info.supported_capture_options.portal) + gtk_combo_box_set_active(record_area_selection_menu, 2); + else + gtk_combo_box_set_active(record_area_selection_menu, 0); gtk_widget_set_hexpand(GTK_WIDGET(record_area_selection_menu), true); gtk_grid_attach(record_area_grid, GTK_WIDGET(record_area_selection_menu), 0, record_area_row++, 3, 1); - if(!wayland) { - select_window_button = GTK_BUTTON(gtk_button_new_with_label("Select window...")); - gtk_widget_set_hexpand(GTK_WIDGET(select_window_button), true); - g_signal_connect(select_window_button, "clicked", G_CALLBACK(on_select_window_button_click), app); - gtk_grid_attach(record_area_grid, GTK_WIDGET(select_window_button), 0, record_area_row++, 3, 1); + g_signal_connect(record_area_selection_menu, "changed", G_CALLBACK(record_area_item_change_callback), NULL); - g_signal_connect(record_area_selection_menu, "changed", G_CALLBACK(record_area_item_change_callback), select_window_button); + select_window_button = GTK_BUTTON(gtk_button_new_with_label("Select window...")); + gtk_widget_set_hexpand(GTK_WIDGET(select_window_button), true); + g_signal_connect(select_window_button, "clicked", G_CALLBACK(on_select_window_button_click), app); + gtk_grid_attach(record_area_grid, GTK_WIDGET(select_window_button), 0, record_area_row++, 3, 1); - area_size_label = GTK_LABEL(gtk_label_new("Area size: ")); - gtk_label_set_xalign(area_size_label, 0.0f); - gtk_grid_attach(record_area_grid, GTK_WIDGET(area_size_label), 0, record_area_row++, 2, 1); + change_video_resolution_button = gtk_check_button_new_with_label("Change video resolution"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(change_video_resolution_button), false); + gtk_widget_set_halign(change_video_resolution_button, GTK_ALIGN_START); + g_signal_connect(change_video_resolution_button, "clicked", G_CALLBACK(on_change_video_resolution_button_click), app); + gtk_grid_attach(record_area_grid, change_video_resolution_button, 0, record_area_row++, 3, 1); + { area_size_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_set_row_spacing(area_size_grid, 10); gtk_grid_attach(record_area_grid, GTK_WIDGET(area_size_grid), 0, record_area_row++, 3, 1); + GtkLabel *video_resolution_label = GTK_LABEL(gtk_label_new("Video resolution limit: ")); + gtk_label_set_xalign(video_resolution_label, 0.0f); + gtk_grid_attach(area_size_grid, GTK_WIDGET(video_resolution_label), 0, 0, 3, 1); + area_width_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 10000.0, 1.0)); + g_signal_connect(area_width_entry, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); gtk_spin_button_set_value(area_width_entry, 1920.0); gtk_widget_set_hexpand(GTK_WIDGET(area_width_entry), true); - gtk_grid_attach(area_size_grid, GTK_WIDGET(area_width_entry), 0, 0, 1, 1); + gtk_grid_attach(area_size_grid, GTK_WIDGET(area_width_entry), 0, 1, 1, 1); - gtk_grid_attach(area_size_grid, gtk_label_new("x"), 1, 0, 1, 1); + gtk_grid_attach(area_size_grid, gtk_label_new("x"), 1, 1, 1, 1); area_height_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 10000.0, 1.0)); + g_signal_connect(area_height_entry, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); gtk_spin_button_set_value(area_height_entry, 1080.0); gtk_widget_set_hexpand(GTK_WIDGET(area_height_entry), true); - gtk_grid_attach(area_size_grid, GTK_WIDGET(area_height_entry), 2, 0, 1, 1); + gtk_grid_attach(area_size_grid, GTK_WIDGET(area_height_entry), 2, 1, 1, 1); + } + + { + video_resolution_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_set_row_spacing(video_resolution_grid, 10); + gtk_grid_attach(record_area_grid, GTK_WIDGET(video_resolution_grid), 0, record_area_row++, 3, 1); + + GtkLabel *video_resolution_label = GTK_LABEL(gtk_label_new("Video resolution: ")); + gtk_label_set_xalign(video_resolution_label, 0.0f); + gtk_grid_attach(video_resolution_grid, GTK_WIDGET(video_resolution_label), 0, 0, 3, 1); + + video_width_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 10000.0, 1.0)); + g_signal_connect(video_width_entry, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + gtk_spin_button_set_value(video_width_entry, 1920.0); + gtk_widget_set_hexpand(GTK_WIDGET(video_width_entry), true); + gtk_grid_attach(video_resolution_grid, GTK_WIDGET(video_width_entry), 0, 1, 1, 1); + + gtk_grid_attach(video_resolution_grid, gtk_label_new("x"), 1, 1, 1, 1); + + video_height_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 10000.0, 1.0)); + g_signal_connect(video_height_entry, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + gtk_spin_button_set_value(video_height_entry, 1080.0); + gtk_widget_set_hexpand(GTK_WIDGET(video_height_entry), true); + gtk_grid_attach(video_resolution_grid, GTK_WIDGET(video_height_entry), 2, 1, 1, 1); } + restore_portal_session_button = gtk_check_button_new_with_label("Restore portal session"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(restore_portal_session_button), true); + gtk_widget_set_halign(restore_portal_session_button, GTK_ALIGN_START); + gtk_grid_attach(record_area_grid, restore_portal_session_button, 0, record_area_row++, 3, 1); + GtkFrame *audio_input_frame = GTK_FRAME(gtk_frame_new("Audio")); gtk_grid_attach(grid, GTK_WIDGET(audio_input_frame), 0, grid_row++, 2, 1); @@ -2666,120 +3180,212 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_widget_set_margin(GTK_WIDGET(audio_grid), 10, 10, 10, 10); gtk_container_add(GTK_CONTAINER(audio_input_frame), GTK_WIDGET(audio_grid)); - GtkGrid *add_audio_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_set_row_spacing(add_audio_grid, 10); - gtk_grid_set_column_spacing(add_audio_grid, 10); - gtk_grid_attach(audio_grid, GTK_WIDGET(add_audio_grid), 0, audio_input_area_row++, 1, 1); - - // TODO: - //const PulseAudioServerInfo pa_server_info = get_pulseaudio_default_inputs(); - - // if(!pa_server_info.default_sink_name.empty() && audio_inputs_contains(audio_inputs, pa_server_info.default_sink_name)) { - // gtk_combo_box_text_append(audio_input_menu_todo, pa_server_info.default_sink_name.c_str(), "Default output"); - // ++num_audio_inputs_addable; - // } - - // if(!pa_server_info.default_source_name.empty() && audio_inputs_contains(audio_inputs, pa_server_info.default_source_name)) { - // gtk_combo_box_text_append(audio_input_menu_todo, pa_server_info.default_source_name.c_str(), "Default input"); - // ++num_audio_inputs_addable; - // } - - // for(const AudioInput &audio_input : audio_inputs) { - // std::string text = audio_input.description; - // gtk_combo_box_text_append(audio_input_menu_todo, audio_input.name.c_str(), audio_input.description.c_str()); - // ++num_audio_inputs_addable; - // } - - add_audio_input_button = gtk_button_new_with_label("Add audio track"); - gtk_grid_attach(add_audio_grid, add_audio_input_button, 0, 0, 1, 1); - g_signal_connect(add_audio_input_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer){ - GtkWidget *row = create_used_audio_input_row(); - gtk_widget_show_all(row); - gtk_list_box_insert(GTK_LIST_BOX(audio_input_used_list), row, -1); - return true; - }), nullptr); + { + int audio_devices_row = 0; + + audio_devices_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_set_row_spacing(audio_devices_grid, 10); + gtk_grid_set_column_spacing(audio_devices_grid, 10); + gtk_widget_set_margin(GTK_WIDGET(audio_devices_grid), 0, 0, 0, 0); + gtk_grid_attach(audio_grid, GTK_WIDGET(audio_devices_grid), 0, audio_input_area_row++, 2, 1); + + GtkGrid *add_audio_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_set_row_spacing(add_audio_grid, 10); + gtk_grid_set_column_spacing(add_audio_grid, 10); + gtk_grid_attach(audio_devices_grid, GTK_WIDGET(add_audio_grid), 0, audio_devices_row++, 2, 1); + + GtkWidget *add_audio_device_button = gtk_button_new_with_label("Add audio device"); + gtk_grid_attach(add_audio_grid, add_audio_device_button, 0, 0, 1, 1); + g_signal_connect(add_audio_device_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer){ + audio_inputs = get_audio_devices(); + GtkWidget *row = create_audio_device_combo_box_row(""); + gtk_widget_show_all(row); + gtk_box_pack_start(audio_devices_items_box, row, false, false, 0); + return true; + }), nullptr); + + add_application_audio_button = gtk_button_new_with_label("Add application audio"); + gtk_grid_attach(add_audio_grid, add_application_audio_button, 1, 0, 1, 1); + g_signal_connect(add_application_audio_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer){ + application_audio = get_application_audio(); + GtkWidget *row = create_application_audio_combo_box_row(""); + gtk_widget_show_all(row); + gtk_box_pack_start(audio_devices_items_box, row, false, false, 0); + return true; + }), nullptr); + + add_custom_application_audio_button = gtk_button_new_with_label("Add custom application audio"); + gtk_grid_attach(add_audio_grid, add_custom_application_audio_button, 3, 0, 1, 1); + g_signal_connect(add_custom_application_audio_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer){ + GtkWidget *row = create_application_audio_custom_row(""); + gtk_widget_show_all(row); + gtk_box_pack_start(audio_devices_items_box, row, false, false, 0); + return true; + }), nullptr); - audio_input_used_list = gtk_list_box_new(); - gtk_widget_set_hexpand (audio_input_used_list, TRUE); - gtk_list_box_set_selection_mode (GTK_LIST_BOX (audio_input_used_list), GTK_SELECTION_NONE); - gtk_grid_attach(audio_grid, audio_input_used_list, 0, audio_input_area_row++, 2, 1); + audio_devices_items_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 10)); + gtk_grid_attach(audio_devices_grid, GTK_WIDGET(audio_devices_items_box), 0, audio_devices_row++, 2, 1); + } - merge_audio_tracks_button = gtk_check_button_new_with_label("Merge audio tracks"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button), true); - gtk_widget_set_halign(merge_audio_tracks_button, GTK_ALIGN_START); - gtk_grid_attach(audio_grid, merge_audio_tracks_button, 0, audio_input_area_row++, 2, 1); + split_audio_button = gtk_check_button_new_with_label("Split each device/app audio into separate audio tracks"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(split_audio_button), false); + gtk_widget_set_halign(split_audio_button, GTK_ALIGN_START); + gtk_grid_attach(audio_grid, split_audio_button, 0, audio_input_area_row++, 2, 1); - GtkGrid *fps_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(fps_grid), 0, grid_row++, 2, 1); - gtk_grid_attach(fps_grid, gtk_label_new("Frame rate: "), 0, 0, 1, 1); - fps_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(1.0, 5000.0, 1.0)); - gtk_spin_button_set_value(fps_entry, 60.0); - gtk_widget_set_hexpand(GTK_WIDGET(fps_entry), true); - gtk_grid_attach(fps_grid, GTK_WIDGET(fps_entry), 1, 0, 1, 1); + record_app_audio_inverted_button = gtk_check_button_new_with_label("Record audio from all applications except the selected ones"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record_app_audio_inverted_button), false); + gtk_widget_set_halign(record_app_audio_inverted_button, GTK_ALIGN_START); + gtk_grid_attach(audio_grid, record_app_audio_inverted_button, 0, audio_input_area_row++, 2, 1); - color_range_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(color_range_grid), 0, grid_row++, 2, 1); - gtk_grid_attach(color_range_grid, gtk_label_new("Color range: "), 0, 0, 1, 1); - color_range_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); - gtk_combo_box_text_append(color_range_input_menu, "limited", "Limited"); - gtk_combo_box_text_append(color_range_input_menu, "full", "Full"); - gtk_widget_set_hexpand(GTK_WIDGET(color_range_input_menu), true); - gtk_grid_attach(color_range_grid, GTK_WIDGET(color_range_input_menu), 1, 0, 1, 1); - gtk_combo_box_set_active(GTK_COMBO_BOX(color_range_input_menu), 0); + audio_codec_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_attach(audio_grid, GTK_WIDGET(audio_codec_grid), 0, audio_input_area_row++, 2, 1); + gtk_grid_attach(audio_codec_grid, gtk_label_new("Audio codec: "), 0, 0, 1, 1); + audio_codec_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(audio_codec_input_menu, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + gtk_combo_box_text_append(audio_codec_input_menu, "opus", "Opus (Recommended)"); + gtk_combo_box_text_append(audio_codec_input_menu, "aac", "AAC"); + gtk_widget_set_hexpand(GTK_WIDGET(audio_codec_input_menu), true); + gtk_grid_attach(audio_codec_grid, GTK_WIDGET(audio_codec_input_menu), 1, 0, 1, 1); + gtk_combo_box_set_active(GTK_COMBO_BOX(audio_codec_input_menu), 0); + + GtkFrame *video_input_frame = GTK_FRAME(gtk_frame_new("Video")); + gtk_grid_attach(grid, GTK_WIDGET(video_input_frame), 0, grid_row++, 2, 1); - GtkGrid *quality_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(quality_grid), 0, grid_row++, 2, 1); - gtk_grid_attach(quality_grid, gtk_label_new("Video quality: "), 0, 0, 1, 1); + GtkGrid *video_grid = GTK_GRID(gtk_grid_new()); + gtk_widget_set_vexpand(GTK_WIDGET(video_grid), false); + gtk_widget_set_hexpand(GTK_WIDGET(video_grid), true); + gtk_grid_set_row_spacing(video_grid, 10); + gtk_grid_set_column_spacing(video_grid, 10); + gtk_widget_set_margin(GTK_WIDGET(video_grid), 10, 10, 10, 10); + gtk_container_add(GTK_CONTAINER(video_input_frame), GTK_WIDGET(video_grid)); + + GtkGrid *video_quality_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_attach(video_grid, GTK_WIDGET(video_quality_grid), 0, video_input_area_row++, 2, 1); + gtk_grid_attach(video_quality_grid, gtk_label_new("Video quality: "), 0, 0, 1, 1); quality_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(quality_input_menu, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + gtk_combo_box_text_append(quality_input_menu, "custom", "Constant bitrate (Recommended for live streaming and replay)"); gtk_combo_box_text_append(quality_input_menu, "medium", "Medium"); - gtk_combo_box_text_append(quality_input_menu, "high", "High (Recommended for live streaming)"); - gtk_combo_box_text_append(quality_input_menu, "very_high", "Very High (Recommended)"); + gtk_combo_box_text_append(quality_input_menu, "high", "High"); + gtk_combo_box_text_append(quality_input_menu, "very_high", "Very High (Recommended for recording)"); gtk_combo_box_text_append(quality_input_menu, "ultra", "Ultra"); gtk_widget_set_hexpand(GTK_WIDGET(quality_input_menu), true); - gtk_grid_attach(quality_grid, GTK_WIDGET(quality_input_menu), 1, 0, 1, 1); + gtk_grid_attach(video_quality_grid, GTK_WIDGET(quality_input_menu), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(quality_input_menu), 0); + g_signal_connect(quality_input_menu, "changed", G_CALLBACK(quality_combo_box_change_callback), quality_input_menu); + + video_bitrate_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_attach(video_grid, GTK_WIDGET(video_bitrate_grid), 0, video_input_area_row++, 2, 1); + gtk_grid_attach(video_bitrate_grid, gtk_label_new("Video bitrate (kbps): "), 0, 0, 1, 1); + video_bitrate_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(1.0, 500000.0, 1.0)); + g_signal_connect(video_bitrate_entry, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + gtk_spin_button_set_value(video_bitrate_entry, 15000.0); + gtk_widget_set_hexpand(GTK_WIDGET(video_bitrate_entry), true); + gtk_grid_attach(video_bitrate_grid, GTK_WIDGET(video_bitrate_entry), 1, 0, 1, 1); video_codec_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(video_codec_grid), 0, grid_row++, 2, 1); + gtk_grid_attach(video_grid, GTK_WIDGET(video_codec_grid), 0, video_input_area_row++, 2, 1); gtk_grid_attach(video_codec_grid, gtk_label_new("Video codec: "), 0, 0, 1, 1); - video_codec_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); - gtk_combo_box_text_append(video_codec_input_menu, "auto", "Auto (Recommended)"); - if(supported_video_codecs_exit_status == 0) { - if(supported_video_codecs.h264) - gtk_combo_box_text_append(video_codec_input_menu, "h264", "H264"); - if(supported_video_codecs.hevc) - gtk_combo_box_text_append(video_codec_input_menu, "hevc", "HEVC"); - if(supported_video_codecs.av1) - gtk_combo_box_text_append(video_codec_input_menu, "av1", "AV1"); - - if(wayland) { - if(supported_video_codecs.hevc) - gtk_combo_box_text_append(video_codec_input_menu, "hevc_hdr", "HEVC (HDR)"); - if(supported_video_codecs.av1) - gtk_combo_box_text_append(video_codec_input_menu, "av1_hdr", "AV1 (HDR)"); + + { + store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); + video_codec_selection_model = GTK_TREE_MODEL(store); + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, "Auto (Recommended, H264)", -1); + gtk_list_store_set(store, &iter, 1, "auto", -1); + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.h264 ? "H264 (Largest file size, best software compatibility)" : "H264 (Not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "h264", -1); + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC" : "HEVC (Not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "hevc", -1); + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC (10 bit, reduces banding)" : "HEVC (10 bit, not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "hevc_10bit", -1); + + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC (HDR)" : "HEVC (HDR, not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "hevc_hdr", -1); + } else { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, "HEVC (HDR, not available on X11)", -1); + gtk_list_store_set(store, &iter, 1, "hevc_hdr", -1); } - } else { - gtk_combo_box_text_append(video_codec_input_menu, "h264", "H264"); - gtk_combo_box_text_append(video_codec_input_menu, "hevc", "HEVC"); + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1 (Smallest file size, worst software compatibility)" : "AV1 (Not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "av1", -1); + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1 (10 bit, reduces banding)" : "AV1 (10 bit, not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "av1_10bit", -1); + + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1 (HDR)" : "AV1 (HDR, not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "av1_hdr", -1); + } else { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, "AV1 (HDR, not available on X11)", -1); + gtk_list_store_set(store, &iter, 1, "av1_hdr", -1); + } + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.vp8 ? "VP8" : "VP8 (Not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "vp8", -1); + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.vp9 ? "VP9" : "VP9 (Not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "vp9", -1); + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.h264_software ? "H264 Software Encoder (Slow, not recommeded)" : "H264 Software Encoder (Not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "h264_software", -1); + + video_codec_selection_menu = GTK_COMBO_BOX(gtk_combo_box_new_with_model(video_codec_selection_model)); + g_signal_connect(video_codec_selection_menu, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + + renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(video_codec_selection_menu), renderer, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(video_codec_selection_menu), renderer, "text", 0, NULL); + gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(video_codec_selection_menu), renderer, video_codec_set_sensitive, NULL, NULL); + + gtk_combo_box_set_active(video_codec_selection_menu, 0); + + gtk_widget_set_hexpand(GTK_WIDGET(video_codec_selection_menu), true); + gtk_grid_attach(video_codec_grid, GTK_WIDGET(video_codec_selection_menu), 1, 0, 1, 1); } - gtk_widget_set_hexpand(GTK_WIDGET(video_codec_input_menu), true); - gtk_grid_attach(video_codec_grid, GTK_WIDGET(video_codec_input_menu), 1, 0, 1, 1); - gtk_combo_box_set_active(GTK_COMBO_BOX(video_codec_input_menu), 0); - audio_codec_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(audio_codec_grid), 0, grid_row++, 2, 1); - gtk_grid_attach(audio_codec_grid, gtk_label_new("Audio codec: "), 0, 0, 1, 1); - audio_codec_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); - gtk_combo_box_text_append(audio_codec_input_menu, "opus", "Opus (Recommended)"); - gtk_combo_box_text_append(audio_codec_input_menu, "aac", "AAC"); - gtk_widget_set_hexpand(GTK_WIDGET(audio_codec_input_menu), true); - gtk_grid_attach(audio_codec_grid, GTK_WIDGET(audio_codec_input_menu), 1, 0, 1, 1); - gtk_combo_box_set_active(GTK_COMBO_BOX(audio_codec_input_menu), 0); + color_range_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_attach(video_grid, GTK_WIDGET(color_range_grid), 0, video_input_area_row++, 2, 1); + gtk_grid_attach(color_range_grid, gtk_label_new("Color range: "), 0, 0, 1, 1); + color_range_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(color_range_input_menu, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + gtk_combo_box_text_append(color_range_input_menu, "limited", "Limited"); + gtk_combo_box_text_append(color_range_input_menu, "full", "Full"); + gtk_widget_set_hexpand(GTK_WIDGET(color_range_input_menu), true); + gtk_grid_attach(color_range_grid, GTK_WIDGET(color_range_input_menu), 1, 0, 1, 1); + gtk_combo_box_set_active(GTK_COMBO_BOX(color_range_input_menu), 0); + + GtkGrid *fps_grid = GTK_GRID(gtk_grid_new()); + gtk_grid_attach(video_grid, GTK_WIDGET(fps_grid), 0, video_input_area_row++, 2, 1); + gtk_grid_attach(fps_grid, gtk_label_new("Frame rate: "), 0, 0, 1, 1); + fps_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(1.0, 5000.0, 1.0)); + g_signal_connect(fps_entry, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); + gtk_spin_button_set_value(fps_entry, 60.0); + gtk_widget_set_hexpand(GTK_WIDGET(fps_entry), true); + gtk_grid_attach(fps_grid, GTK_WIDGET(fps_entry), 1, 0, 1, 1); framerate_mode_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(framerate_mode_grid), 0, grid_row++, 2, 1); + gtk_grid_attach(video_grid, GTK_WIDGET(framerate_mode_grid), 0, video_input_area_row++, 2, 1); gtk_grid_attach(framerate_mode_grid, gtk_label_new("Frame rate mode: "), 0, 0, 1, 1); framerate_mode_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(framerate_mode_input_menu, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); gtk_combo_box_text_append(framerate_mode_input_menu, "auto", "Auto (Recommended)"); gtk_combo_box_text_append(framerate_mode_input_menu, "cfr", "Constant"); gtk_combo_box_text_append(framerate_mode_input_menu, "vfr", "Variable"); @@ -2788,7 +3394,7 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_combo_box_set_active(GTK_COMBO_BOX(framerate_mode_input_menu), 0); overclock_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(overclock_grid), 0, grid_row++, 2, 1); + gtk_grid_attach(video_grid, GTK_WIDGET(overclock_grid), 0, video_input_area_row++, 2, 1); overclock_button = gtk_check_button_new_with_label("Overclock memory transfer rate to workaround NVIDIA driver performance bug"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(overclock_button), false); gtk_widget_set_halign(overclock_button, GTK_ALIGN_START); @@ -2808,24 +3414,46 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a "Note that this only works when Xorg server is running as root.\n" "\n" "Note! use at your own risk!"); + set_dialog_selectable(dialog); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); return true; }), nullptr); - show_notification_button = gtk_check_button_new_with_label("Show video saved notification"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_notification_button), true); - gtk_widget_set_halign(show_notification_button, GTK_ALIGN_START); - gtk_grid_attach(grid, show_notification_button, 0, grid_row++, 2, 1); - record_cursor_button = gtk_check_button_new_with_label("Record cursor"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record_cursor_button), true); gtk_widget_set_halign(record_cursor_button, GTK_ALIGN_START); - gtk_grid_attach(grid, record_cursor_button, 0, grid_row++, 2, 1); + gtk_grid_attach(video_grid, record_cursor_button, 0, video_input_area_row++, 2, 1); + + notifications_frame = GTK_FRAME(gtk_frame_new("Notifications")); + gtk_grid_attach(grid, GTK_WIDGET(notifications_frame), 0, grid_row++, 2, 1); + + GtkGrid *notifications_grid = GTK_GRID(gtk_grid_new()); + gtk_widget_set_vexpand(GTK_WIDGET(notifications_grid), false); + gtk_widget_set_hexpand(GTK_WIDGET(notifications_grid), true); + gtk_grid_set_row_spacing(notifications_grid, 10); + gtk_grid_set_column_spacing(notifications_grid, 10); + gtk_widget_set_margin(GTK_WIDGET(notifications_grid), 10, 10, 10, 10); + gtk_container_add(GTK_CONTAINER(notifications_frame), GTK_WIDGET(notifications_grid)); + + show_recording_started_notification_button = gtk_check_button_new_with_label("Show recording/streaming/replay started notification"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button), false); + gtk_widget_set_halign(show_recording_started_notification_button, GTK_ALIGN_START); + gtk_grid_attach(notifications_grid, show_recording_started_notification_button, 0, notifications_area_row++, 2, 1); + + show_recording_stopped_notification_button = gtk_check_button_new_with_label("Show streaming/replay stopped notification"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button), false); + gtk_widget_set_halign(show_recording_stopped_notification_button, GTK_ALIGN_START); + gtk_grid_attach(notifications_grid, show_recording_stopped_notification_button, 0, notifications_area_row++, 2, 1); + + show_recording_saved_notification_button = gtk_check_button_new_with_label("Show video saved notification"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button), true); + gtk_widget_set_halign(show_recording_saved_notification_button, GTK_ALIGN_START); + gtk_grid_attach(notifications_grid, show_recording_saved_notification_button, 0, notifications_area_row++, 2, 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, grid_row++, 2, 1); + gtk_grid_attach(main_grid, GTK_WIDGET(start_button_grid), 0, main_grid_row++, 2, 1); gtk_grid_set_column_spacing(start_button_grid, 10); stream_button = GTK_BUTTON(gtk_button_new_with_label("Stream")); @@ -2856,42 +3484,82 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_widget_set_sensitive(GTK_WIDGET(record_button), false); gtk_widget_set_sensitive(GTK_WIDGET(stream_button), false); - return GTK_WIDGET(grid); + return GTK_WIDGET(main_grid); } -static void add_wayland_global_hotkeys_ui(GtkGrid *grid, int &row, int width) { - GtkWidget *label = gtk_label_new("Hotkeys not supported because Wayland doesn't support global hotkeys"); - gtk_widget_set_hexpand(label, true); - gtk_grid_attach(grid, label, 0, row, width - 1, 1); +static void deactivated_callback(const char *description, void *userdata) { + (void)userdata; + const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata.stack); + for(int i = 0; i < num_hotkeys; ++i) { + if(visible_page != hotkeys[i]->page) + continue; - GtkButton *question_button = GTK_BUTTON(gtk_button_new_with_label("?")); - gtk_grid_attach(grid, GTK_WIDGET(question_button), width - 1, row, 1, 1); + if(strcmp(description, hotkeys[i]->shortcut_id) == 0) + hotkeys[i]->trigger_handler(hotkeys[i]->associated_button, page_navigation_userdata.app); + } +} +static void add_wayland_global_hotkeys_ui(GtkGrid *grid, int &row, int width) { + GtkGrid *aa_grid = GTK_GRID(gtk_grid_new()); + gtk_widget_set_halign(GTK_WIDGET(aa_grid), GTK_ALIGN_CENTER); + gtk_grid_attach(grid, GTK_WIDGET(aa_grid), 0, row++, width, 1); + gtk_grid_set_column_spacing(aa_grid, 10); + + if(wayland_compositor == WaylandCompositor::KDE) { + gtk_grid_attach(aa_grid, gtk_label_new("Hotkeys are managed externally on KDE Plasma Wayland. Click here to change hotkeys:"), 0, 0, 1, 1); + + GtkButton *register_hotkeys_button = GTK_BUTTON(gtk_button_new_with_label("Change hotkeys")); + gtk_widget_set_hexpand(GTK_WIDGET(register_hotkeys_button), true); + //gtk_widget_set_halign(GTK_WIDGET(register_hotkeys_button), GTK_ALIGN_START); + g_signal_connect(register_hotkeys_button, "clicked", G_CALLBACK(on_register_hotkeys_button_clicked), nullptr); + gtk_grid_attach(aa_grid, GTK_WIDGET(register_hotkeys_button), 1, 0, 1, 1); + } else { + gtk_grid_attach(aa_grid, gtk_label_new("Hotkeys are managed externally on Wayland. Go into your system application/hotkey settings to change hotkeys."), 0, 0, 1, 1); + } + + row++; + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, width, 1); row++; +} - g_signal_connect(question_button, "clicked", G_CALLBACK(+[](GtkButton *button, gpointer userdata){ - (void)button; - (void)userdata; - GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Wayland (desktop portal) doesn't support global hotkeys in any meaningful manner to most applications, including GPU Screen Recorder.\n" - "If you want to use global hotkeys in GPU Screen Recorder then either use X11 or bind the following commands in your Wayland desktop environment hotkey settings to keys:\n" - "Stop recording (saves video as well when not in replay mode):\n" - " killall -SIGINT gpu-screen-recorder\n" - "Save a replay:\n" - " killall -SIGUSR1 gpu-screen-recorder\n" - "Pause/unpause recording:\n" - " killall -SIGUSR2 gpu-screen-recorder\n"); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); +static void create_replay_hotkey_items(GtkGrid *parent_grid, int row, int num_columns) { + replay_hotkeys_grid = GTK_GRID(gtk_grid_new()); + gtk_widget_set_halign(GTK_WIDGET(replay_hotkeys_grid), GTK_ALIGN_START); + gtk_grid_set_row_spacing(replay_hotkeys_grid, 10); + gtk_grid_set_column_spacing(replay_hotkeys_grid, 10); + gtk_grid_attach(parent_grid, GTK_WIDGET(replay_hotkeys_grid), 0, row, num_columns, 1); + int hotkeys_row = 0; - return true; - }), nullptr); + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) + add_wayland_global_hotkeys_ui(replay_hotkeys_grid, hotkeys_row, num_columns); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, width, 1); + { + gtk_grid_attach(replay_hotkeys_grid, gtk_label_new("Press"), 0, hotkeys_row, 1, 1); + + replay_start_stop_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(replay_start_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Alt + 1"); + g_signal_connect(replay_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_start_stop_hotkey_button); + gtk_grid_attach(replay_hotkeys_grid, replay_start_stop_hotkey_button, 1, hotkeys_row, 1, 1); + + gtk_grid_attach(replay_hotkeys_grid, gtk_label_new("to start/stop the replay and"), 2, hotkeys_row, 1, 1); + + replay_save_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Alt + 2"); + g_signal_connect(replay_save_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_save_hotkey_button); + gtk_grid_attach(replay_hotkeys_grid, replay_save_hotkey_button, 3, hotkeys_row, 1, 1); + + GtkWidget *save_replay_label = gtk_label_new("to save the replay"); + gtk_widget_set_halign(save_replay_label, GTK_ALIGN_START); + gtk_widget_set_hexpand(save_replay_label, true); + gtk_grid_attach(replay_hotkeys_grid, save_replay_label, 4, hotkeys_row, 1, 1); + + ++hotkeys_row; + } } static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { int row = 0; + const int num_columns = 5; std::string video_filepath = get_videos_dir(); @@ -2903,54 +3571,29 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); - GtkWidget *hotkey_active_label = NULL; - if(wayland) { - add_wayland_global_hotkeys_ui(grid, row, 5); + GtkWidget *hotkey_active_label = nullptr; + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + replay_hotkeys_not_supported_label = gtk_label_new("Your Wayland compositor doesn't support global hotkeys. Use X11 or KDE Plasma on Wayland if you want to use hotkeys."); + gtk_grid_attach(grid, replay_hotkeys_not_supported_label, 0, row++, num_columns, 1); } else { - hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel"); - gtk_grid_attach(grid, hotkey_active_label, 0, row++, 5, 1); - - GtkWidget *a = gtk_label_new("Press"); - gtk_widget_set_halign(a, GTK_ALIGN_END); - - GtkWidget *b = gtk_label_new("to start/stop the replay and"); - gtk_widget_set_halign(b, GTK_ALIGN_START); - - GtkWidget *c = gtk_label_new("to save"); - gtk_widget_set_halign(c, GTK_ALIGN_START); - - replay_start_stop_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(replay_start_stop_hotkey_button), "Alt + F1"); - g_signal_connect(replay_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_start_stop_hotkey_button); + hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel."); + gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); + } - replay_save_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey_button), "Alt + F2"); - gtk_widget_set_halign(replay_save_hotkey_button, GTK_ALIGN_START); - g_signal_connect(replay_save_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_save_hotkey_button); + create_replay_hotkey_items(grid, row, num_columns); + ++row; - gtk_grid_attach(grid, a, 0, row, 1, 1); - gtk_grid_attach(grid, replay_start_stop_hotkey_button, 1, row, 1, 1); - gtk_grid_attach(grid, b, 2, row, 1, 1); - gtk_grid_attach(grid, replay_save_hotkey_button, 3, row, 1, 1); - gtk_grid_attach(grid, c, 4, row, 1, 1); - ++row; - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 5, 1); + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_grid), false); + gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_not_supported_label), false); } - replay_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - replay_start_stop_hotkey.keysym = XK_F1; - replay_start_stop_hotkey.hotkey_entry = replay_start_stop_hotkey_button; - replay_start_stop_hotkey.hotkey_active_label = hotkey_active_label; - - replay_save_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - replay_save_hotkey.keysym = XK_F2; - replay_save_hotkey.hotkey_entry = replay_save_hotkey_button; - replay_save_hotkey.hotkey_active_label = hotkey_active_label; + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); GtkWidget *save_icon = gtk_image_new_from_icon_name("document-save", GTK_ICON_SIZE_BUTTON); GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(file_chooser_grid, 10); GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the replays?"); gtk_grid_attach(file_chooser_grid, file_chooser_label, 0, 0, 1, 1); @@ -2963,26 +3606,31 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(file_chooser_grid, GTK_WIDGET(replay_file_chooser_button), 1, 0, 1, 1); GtkGrid *container_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, row++, num_columns, 1); gtk_grid_attach(container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); replay_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(replay_container, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); for(auto &supported_container : supported_containers) { gtk_combo_box_text_append(replay_container, supported_container.container_name, supported_container.file_extension); } + if(gsr_info.supported_video_codecs.vp8 || gsr_info.supported_video_codecs.vp9) { + gtk_combo_box_text_append(replay_container, "webm", "webm"); + } gtk_widget_set_hexpand(GTK_WIDGET(replay_container), true); gtk_grid_attach(container_grid, GTK_WIDGET(replay_container), 1, 0, 1, 1); - gtk_combo_box_set_active(GTK_COMBO_BOX(replay_container), 0); // TODO: + gtk_combo_box_set_active(GTK_COMBO_BOX(replay_container), 0); GtkGrid *replay_time_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(replay_time_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(replay_time_grid), 0, row++, num_columns, 1); gtk_grid_attach(replay_time_grid, gtk_label_new("Replay time in seconds: "), 0, 0, 1, 1); replay_time_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 1200.0, 1.0)); + g_signal_connect(replay_time_entry, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); gtk_spin_button_set_value(replay_time_entry, 30.0); gtk_widget_set_hexpand(GTK_WIDGET(replay_time_entry), true); gtk_grid_attach(replay_time_grid, GTK_WIDGET(replay_time_entry), 1, 0, 1, 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); replay_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); @@ -3004,10 +3652,10 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_sensitive(GTK_WIDGET(replay_save_button), false); gtk_grid_attach(start_button_grid, GTK_WIDGET(replay_save_button), 2, 0, 1, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 5, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); replay_bottom_panel_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(replay_bottom_panel_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(replay_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(replay_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(replay_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(replay_bottom_panel_grid), GTK_ALIGN_END); @@ -3020,11 +3668,67 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_valign(replay_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(replay_bottom_panel_grid, replay_record_time_label, 1, 0, 1, 1); + replay_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + replay_start_stop_hotkey.keysym = XK_1; + replay_start_stop_hotkey.hotkey_entry = replay_start_stop_hotkey_button; + replay_start_stop_hotkey.hotkey_active_label = hotkey_active_label; + replay_start_stop_hotkey.config = &config.replay_config.start_stop_recording_hotkey; + replay_start_stop_hotkey.page = GTK_WIDGET(grid); + replay_start_stop_hotkey.trigger_handler = on_start_replay_button_click; + replay_start_stop_hotkey.associated_button = start_replay_button; + replay_start_stop_hotkey.shortcut_id = SHORTCUT_ID_START_STOP_RECORDING; + + replay_save_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + replay_save_hotkey.keysym = XK_2; + replay_save_hotkey.hotkey_entry = replay_save_hotkey_button; + replay_save_hotkey.hotkey_active_label = hotkey_active_label; + replay_save_hotkey.config = &config.replay_config.save_recording_hotkey; + replay_save_hotkey.page = GTK_WIDGET(grid); + replay_save_hotkey.trigger_handler = on_replay_save_button_click; + replay_save_hotkey.associated_button = replay_save_button; + replay_save_hotkey.shortcut_id = SHORTCUT_ID_SAVE_REPLAY; + return GTK_WIDGET(grid); } +static void create_recording_hotkey_items(GtkGrid *parent_grid, int row, int num_columns) { + recording_hotkeys_grid = GTK_GRID(gtk_grid_new()); + gtk_widget_set_halign(GTK_WIDGET(recording_hotkeys_grid), GTK_ALIGN_START); + gtk_grid_set_row_spacing(recording_hotkeys_grid, 10); + gtk_grid_set_column_spacing(recording_hotkeys_grid, 10); + gtk_grid_attach(parent_grid, GTK_WIDGET(recording_hotkeys_grid), 0, row, num_columns, 1); + int hotkeys_row = 0; + + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) + add_wayland_global_hotkeys_ui(recording_hotkeys_grid, hotkeys_row, num_columns); + + { + gtk_grid_attach(recording_hotkeys_grid, gtk_label_new("Press"), 0, hotkeys_row, 1, 1); + + record_start_stop_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(record_start_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Alt + 1"); + g_signal_connect(record_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), record_start_stop_hotkey_button); + gtk_grid_attach(recording_hotkeys_grid, record_start_stop_hotkey_button, 1, hotkeys_row, 1, 1); + + gtk_grid_attach(recording_hotkeys_grid, gtk_label_new("to start/stop recording and"), 2, hotkeys_row, 1, 1); + + pause_unpause_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(pause_unpause_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Alt + 2"); + g_signal_connect(pause_unpause_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), pause_unpause_hotkey_button); + gtk_grid_attach(recording_hotkeys_grid, pause_unpause_hotkey_button, 3, hotkeys_row, 1, 1); + + GtkWidget *pause_unpause_recording_label = gtk_label_new("to pause/unpause recording"); + gtk_widget_set_halign(pause_unpause_recording_label, GTK_ALIGN_START); + gtk_widget_set_hexpand(pause_unpause_recording_label, true); + gtk_grid_attach(recording_hotkeys_grid, pause_unpause_recording_label, 4, hotkeys_row, 1, 1); + + ++hotkeys_row; + } +} + static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { int row = 0; + const int num_columns = 5; std::string video_filepath = get_videos_dir(); @@ -3036,54 +3740,29 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); - GtkWidget *hotkey_active_label = NULL; - if(wayland) { - add_wayland_global_hotkeys_ui(grid, row, 5); + GtkWidget *hotkey_active_label = nullptr; + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + recording_hotkeys_not_supported_label = gtk_label_new("Your Wayland compositor doesn't support global hotkeys. Use X11 or KDE Plasma on Wayland if you want to use hotkeys."); + gtk_grid_attach(grid, recording_hotkeys_not_supported_label, 0, row++, num_columns, 1); } else { - hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel"); - gtk_grid_attach(grid, hotkey_active_label, 0, row++, 5, 1); - - GtkWidget *a = gtk_label_new("Press"); - gtk_widget_set_halign(a, GTK_ALIGN_END); - - GtkWidget *b = gtk_label_new("to start/stop recording and"); - gtk_widget_set_halign(b, GTK_ALIGN_START); - - GtkWidget *c = gtk_label_new("to pause/unpause"); - gtk_widget_set_halign(c, GTK_ALIGN_START); - - record_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(record_hotkey_button), "Alt + F1"); - g_signal_connect(record_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), record_hotkey_button); + hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel."); + gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); + } - pause_unpause_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(pause_unpause_hotkey_button), "Alt + F2"); - gtk_widget_set_halign(pause_unpause_hotkey_button, GTK_ALIGN_START); - g_signal_connect(pause_unpause_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), pause_unpause_hotkey_button); + create_recording_hotkey_items(grid, row, num_columns); + ++row; - gtk_grid_attach(grid, a, 0, row, 1, 1); - gtk_grid_attach(grid, record_hotkey_button, 1, row, 1, 1); - gtk_grid_attach(grid, b, 2, row, 1, 1); - gtk_grid_attach(grid, pause_unpause_hotkey_button, 3, row, 1, 1); - gtk_grid_attach(grid, c, 4, row, 1, 1); - ++row; - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 5, 1); + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_grid), false); + gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_not_supported_label), false); } - record_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - record_hotkey.keysym = XK_F1; - record_hotkey.hotkey_entry = record_hotkey_button; - record_hotkey.hotkey_active_label = hotkey_active_label; - - pause_unpause_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - pause_unpause_hotkey.keysym = XK_F2; - pause_unpause_hotkey.hotkey_entry = pause_unpause_hotkey_button; - pause_unpause_hotkey.hotkey_active_label = hotkey_active_label; + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); GtkWidget *save_icon = gtk_image_new_from_icon_name("document-save", GTK_ICON_SIZE_BUTTON); GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(file_chooser_grid, 10); GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the video?"); gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_label), 0, 0, 1, 1); @@ -3096,18 +3775,22 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(file_chooser_grid, GTK_WIDGET(record_file_chooser_button), 1, 0, 1, 1); GtkGrid *container_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, row++, num_columns, 1); gtk_grid_attach(container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); record_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(record_container, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); for(auto &supported_container : supported_containers) { gtk_combo_box_text_append(record_container, supported_container.container_name, supported_container.file_extension); } + if(gsr_info.supported_video_codecs.vp8 || gsr_info.supported_video_codecs.vp9) { + gtk_combo_box_text_append(record_container, "webm", "webm"); + } gtk_widget_set_hexpand(GTK_WIDGET(record_container), true); gtk_grid_attach(container_grid, GTK_WIDGET(record_container), 1, 0, 1, 1); - gtk_combo_box_set_active(GTK_COMBO_BOX(record_container), 0); // TODO: + gtk_combo_box_set_active(GTK_COMBO_BOX(record_container), 0); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); record_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); @@ -3129,10 +3812,10 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_sensitive(GTK_WIDGET(pause_recording_button), false); gtk_grid_attach(start_button_grid, GTK_WIDGET(pause_recording_button), 2, 0, 1, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 5, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); recording_bottom_panel_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(recording_bottom_panel_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(recording_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(recording_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(recording_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(recording_bottom_panel_grid), GTK_ALIGN_END); @@ -3145,11 +3828,60 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_valign(recording_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(recording_bottom_panel_grid, recording_record_time_label, 1, 0, 1, 1); + record_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + record_start_stop_hotkey.keysym = XK_1; + record_start_stop_hotkey.hotkey_entry = record_start_stop_hotkey_button; + record_start_stop_hotkey.hotkey_active_label = hotkey_active_label; + record_start_stop_hotkey.config = &config.record_config.start_stop_recording_hotkey; + record_start_stop_hotkey.page = GTK_WIDGET(grid); + record_start_stop_hotkey.trigger_handler = on_start_recording_button_click; + record_start_stop_hotkey.associated_button = start_recording_button; + record_start_stop_hotkey.shortcut_id = SHORTCUT_ID_START_STOP_RECORDING; + + pause_unpause_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + pause_unpause_hotkey.keysym = XK_2; + pause_unpause_hotkey.hotkey_entry = pause_unpause_hotkey_button; + pause_unpause_hotkey.hotkey_active_label = hotkey_active_label; + pause_unpause_hotkey.config = &config.record_config.pause_unpause_recording_hotkey; + pause_unpause_hotkey.page = GTK_WIDGET(grid); + pause_unpause_hotkey.trigger_handler = on_pause_unpause_button_click; + pause_unpause_hotkey.associated_button = pause_recording_button; + pause_unpause_hotkey.shortcut_id = SHORTCUT_ID_PAUSE_UNPAUSE_RECORDING; + return GTK_WIDGET(grid); } +static void create_streaming_hotkey_items(GtkGrid *parent_grid, int row, int num_columns) { + streaming_hotkeys_grid = GTK_GRID(gtk_grid_new()); + gtk_widget_set_halign(GTK_WIDGET(streaming_hotkeys_grid), GTK_ALIGN_START); + gtk_grid_set_row_spacing(streaming_hotkeys_grid, 10); + gtk_grid_set_column_spacing(streaming_hotkeys_grid, 10); + gtk_grid_attach(parent_grid, GTK_WIDGET(streaming_hotkeys_grid), 0, row, num_columns, 1); + int hotkeys_row = 0; + + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) + add_wayland_global_hotkeys_ui(streaming_hotkeys_grid, hotkeys_row, num_columns); + + { + gtk_grid_attach(streaming_hotkeys_grid, gtk_label_new("Press"), 0, hotkeys_row, 1, 1); + + streaming_start_stop_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(streaming_start_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Alt + 1"); + g_signal_connect(streaming_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), streaming_start_stop_hotkey_button); + gtk_grid_attach(streaming_hotkeys_grid, streaming_start_stop_hotkey_button, 1, hotkeys_row, 1, 1); + + GtkWidget *start_stop_streaming_label = gtk_label_new("to start/stop streaming"); + gtk_widget_set_halign(start_stop_streaming_label, GTK_ALIGN_START); + gtk_widget_set_hexpand(start_stop_streaming_label, true); + gtk_grid_attach(streaming_hotkeys_grid, start_stop_streaming_label, 2, hotkeys_row, 1, 1); + + ++hotkeys_row; + } +} + static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { int row = 0; + const int num_columns = 3; GtkGrid *grid = GTK_GRID(gtk_grid_new()); gtk_stack_add_named(stack, GTK_WIDGET(grid), "streaming"); @@ -3159,38 +3891,30 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_grid_set_column_spacing(grid, 10); gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10); - GtkWidget *hotkey_active_label = NULL; - if(wayland) { - add_wayland_global_hotkeys_ui(grid, row, 3); + GtkWidget *hotkey_active_label = nullptr; + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + streaming_hotkeys_not_supported_label = gtk_label_new("Your Wayland compositor doesn't support global hotkeys. Use X11 or KDE Plasma on Wayland if you want to use hotkeys."); + gtk_grid_attach(grid, streaming_hotkeys_not_supported_label, 0, row++, num_columns, 1); } else { - hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel"); - gtk_grid_attach(grid, hotkey_active_label, 0, row++, 3, 1); - - GtkWidget *a = gtk_label_new("Press"); - gtk_widget_set_halign(a, GTK_ALIGN_END); + hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel."); + gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); + } - GtkWidget *b = gtk_label_new("to start/stop streaming"); - gtk_widget_set_halign(b, GTK_ALIGN_START); + create_streaming_hotkey_items(grid, row, num_columns); + ++row; - streaming_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(streaming_hotkey_button), "Alt + F1"); - g_signal_connect(streaming_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), streaming_hotkey_button); - gtk_grid_attach(grid, a, 0, row, 1, 1); - gtk_grid_attach(grid, streaming_hotkey_button, 1, row, 1, 1); - gtk_grid_attach(grid, b, 2, row, 1, 1); - ++row; - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 3, 1); + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_grid), false); + gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_not_supported_label), false); } - streaming_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - streaming_hotkey.keysym = XK_F1; - streaming_hotkey.hotkey_entry = streaming_hotkey_button; - streaming_hotkey.hotkey_active_label = hotkey_active_label; + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); GtkGrid *stream_service_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, row++, num_columns, 1); gtk_grid_attach(stream_service_grid, gtk_label_new("Stream service: "), 0, 0, 1, 1); stream_service_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(stream_service_input_menu, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); gtk_combo_box_text_append(stream_service_input_menu, "twitch", "Twitch"); gtk_combo_box_text_append(stream_service_input_menu, "youtube", "Youtube"); gtk_combo_box_text_append(stream_service_input_menu, "custom", "Custom"); @@ -3200,7 +3924,7 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(stream_service_grid, GTK_WIDGET(stream_service_input_menu), 1, 0, 1, 1); GtkGrid *stream_id_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(stream_id_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(stream_id_grid), 0, row++, num_columns, 1); stream_key_label = GTK_LABEL(gtk_label_new("Stream key: ")); gtk_grid_attach(stream_id_grid, GTK_WIDGET(stream_key_label), 0, 0, 1, 1); @@ -3218,18 +3942,22 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { } custom_stream_container_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(custom_stream_container_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(custom_stream_container_grid), 0, row++, num_columns, 1); gtk_grid_attach(custom_stream_container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); custom_stream_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); + g_signal_connect(custom_stream_container, "scroll-event", G_CALLBACK(scroll_event_ignore), NULL); for(auto &supported_container : supported_containers) { gtk_combo_box_text_append(custom_stream_container, supported_container.container_name, supported_container.file_extension); } + if(gsr_info.supported_video_codecs.vp8 || gsr_info.supported_video_codecs.vp9) { + gtk_combo_box_text_append(custom_stream_container, "webm", "webm"); + } gtk_widget_set_hexpand(GTK_WIDGET(custom_stream_container), true); gtk_grid_attach(custom_stream_container_grid, GTK_WIDGET(custom_stream_container), 1, 0, 1, 1); gtk_combo_box_set_active(GTK_COMBO_BOX(custom_stream_container), 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); stream_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); @@ -3244,10 +3972,10 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { g_signal_connect(start_streaming_button, "clicked", G_CALLBACK(on_start_streaming_button_click), app); gtk_grid_attach(start_button_grid, GTK_WIDGET(start_streaming_button), 1, 0, 1, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 3, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); streaming_bottom_panel_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(streaming_bottom_panel_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(streaming_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(streaming_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(streaming_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(streaming_bottom_panel_grid), GTK_ALIGN_END); @@ -3260,6 +3988,16 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_valign(streaming_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(streaming_bottom_panel_grid, streaming_record_time_label, 1, 0, 1, 1); + streaming_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); + streaming_start_stop_hotkey.keysym = XK_1; + streaming_start_stop_hotkey.hotkey_entry = streaming_start_stop_hotkey_button; + streaming_start_stop_hotkey.hotkey_active_label = hotkey_active_label; + streaming_start_stop_hotkey.config = &config.streaming_config.start_stop_recording_hotkey; + streaming_start_stop_hotkey.page = GTK_WIDGET(grid); + streaming_start_stop_hotkey.trigger_handler = on_start_streaming_button_click; + streaming_start_stop_hotkey.associated_button = start_streaming_button; + streaming_start_stop_hotkey.shortcut_id = SHORTCUT_ID_START_STOP_RECORDING; + return GTK_WIDGET(grid); } @@ -3350,51 +4088,38 @@ static gboolean timer_timeout_handler(gpointer userdata) { return G_SOURCE_CONTINUE; } -static void add_audio_input_track(const char *name) { - GtkWidget *row = create_used_audio_input_row(); - - const AudioRow *audio_row = (AudioRow*)g_object_get_data(G_OBJECT(row), "audio-row"); - std::string audio_id; - gint target_combo_box_index = combo_box_text_get_row_by_label(GTK_COMBO_BOX(audio_row->input_list), name, audio_id); - if(target_combo_box_index != -1) - gtk_combo_box_set_active(GTK_COMBO_BOX(audio_row->input_list), target_combo_box_index); - - gtk_widget_show_all(row); - gtk_list_box_insert (GTK_LIST_BOX(audio_input_used_list), row, -1); +static const std::string* get_application_audio_by_name_case_insensitive(const std::vector<std::string> &application_audio, const std::string &name) { + for(const auto &app_audio : application_audio) { + if(strcasecmp(app_audio.c_str(), name.c_str()) == 0) + return &app_audio; + } + return nullptr; } -static void load_config(const gpu_info &gpu_inf) { - bool config_empty = false; - config = read_config(config_empty); - +static void load_config() { std::string first_monitor; - if(!wayland && strcmp(config.main_config.record_area_option.c_str(), "window") == 0) { + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && strcmp(config.main_config.record_area_option.c_str(), "window") == 0) { // - } else if(!wayland && strcmp(config.main_config.record_area_option.c_str(), "focused") == 0) { + } else if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && strcmp(config.main_config.record_area_option.c_str(), "focused") == 0) { // - } else if(!wayland && gpu_inf.vendor == GPU_VENDOR_NVIDIA && strcmp(config.main_config.record_area_option.c_str(), "screen") == 0) { + } else if(config.main_config.record_area_option == "portal" && gsr_info.supported_capture_options.portal && gsr_info.system_info.display_server == DisplayServer::WAYLAND) { // } else { - gsr_connection_type connection_type = get_connection_type(); - bool found_monitor = false; - int monitor_name_size = strlen(config.main_config.record_area_option.c_str()); - for_each_active_monitor_output(&egl, connection_type, [&](const gsr_monitor *monitor, void*) { - if(first_monitor.empty()) { - first_monitor.assign(monitor->name, monitor->name_len); - } + for(const auto &monitor : gsr_info.supported_capture_options.monitors) { + if(first_monitor.empty()) + first_monitor = monitor.name; - if(monitor_name_size == monitor->name_len && strncmp(config.main_config.record_area_option.c_str(), monitor->name, monitor->name_len) == 0) { + if(config.main_config.record_area_option == monitor.name) found_monitor = true; - } - }, NULL); + } if(!found_monitor) config.main_config.record_area_option.clear(); } if(config.main_config.record_area_option.empty()) { - const bool allow_screen_capture = wayland || nvfbc_installed || gpu_inf.vendor != GPU_VENDOR_NVIDIA; + const bool allow_screen_capture = is_monitor_capture_drm() || nvfbc_installed; if(allow_screen_capture) { config.main_config.record_area_option = first_monitor; } else { @@ -3402,11 +4127,9 @@ static void load_config(const gpu_info &gpu_inf) { } } - if(!wayland) { - gtk_widget_set_visible(GTK_WIDGET(select_window_button), strcmp(config.main_config.record_area_option.c_str(), "window") == 0); - gtk_widget_set_visible(GTK_WIDGET(area_size_label), strcmp(config.main_config.record_area_option.c_str(), "focused") == 0); - gtk_widget_set_visible(GTK_WIDGET(area_size_grid), strcmp(config.main_config.record_area_option.c_str(), "focused") == 0); - } + gtk_widget_set_visible(GTK_WIDGET(select_window_button), strcmp(config.main_config.record_area_option.c_str(), "window") == 0); + //gtk_widget_set_visible(GTK_WIDGET(area_size_grid), strcmp(config.main_config.record_area_option.c_str(), "focused") == 0); + gtk_widget_set_visible(GTK_WIDGET(restore_portal_session_button), strcmp(config.main_config.record_area_option.c_str(), "portal") == 0); if(config.main_config.record_area_width == 0) config.main_config.record_area_width = 1920; @@ -3414,6 +4137,12 @@ static void load_config(const gpu_info &gpu_inf) { if(config.main_config.record_area_height == 0) config.main_config.record_area_height = 1080; + if(config.main_config.video_width == 0) + config.main_config.video_width = 1920; + + if(config.main_config.video_height == 0) + config.main_config.video_height = 1080; + if(config.main_config.fps == 0) config.main_config.fps = 60; else if(config.main_config.fps < 1) @@ -3424,15 +4153,9 @@ static void load_config(const gpu_info &gpu_inf) { if(config.main_config.color_range != "limited" && config.main_config.color_range != "full") config.main_config.color_range = "limited"; - if(config.main_config.quality != "medium" && config.main_config.quality != "high" && config.main_config.quality != "very_high" && config.main_config.quality != "ultra") + if(config.main_config.quality != "custom" && config.main_config.quality != "medium" && config.main_config.quality != "high" && config.main_config.quality != "very_high" && config.main_config.quality != "ultra") config.main_config.quality = "very_high"; - if(config.main_config.codec != "auto" && config.main_config.codec != "h264" && config.main_config.codec != "h265" && config.main_config.codec != "hevc" && config.main_config.codec != "av1" && config.main_config.codec != "hevc_hdr" && config.main_config.codec != "av1_hdr") - config.main_config.codec = "auto"; - - if(!wayland && (config.main_config.codec == "hevc_hdr" || config.main_config.codec == "av1_hdr")) - config.main_config.codec = "auto"; - if(config.main_config.audio_codec != "opus" && config.main_config.audio_codec != "aac") config.main_config.audio_codec = "opus"; @@ -3454,252 +4177,223 @@ static void load_config(const gpu_info &gpu_inf) { config.replay_config.replay_time = 1200; record_area_selection_menu_set_active_id(config.main_config.record_area_option.c_str()); - if(!wayland) { + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { gtk_spin_button_set_value(area_width_entry, config.main_config.record_area_width); gtk_spin_button_set_value(area_height_entry, config.main_config.record_area_height); } + gtk_spin_button_set_value(video_width_entry, config.main_config.video_width); + gtk_spin_button_set_value(video_height_entry, config.main_config.video_height); gtk_spin_button_set_value(fps_entry, config.main_config.fps); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(merge_audio_tracks_button), config.main_config.merge_audio_tracks); + gtk_spin_button_set_value(video_bitrate_entry, config.main_config.video_bitrate); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(split_audio_button), !config.main_config.merge_audio_tracks); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record_app_audio_inverted_button), config.main_config.record_app_audio_inverted); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(change_video_resolution_button), config.main_config.change_video_resolution); for(const std::string &audio_input : config.main_config.audio_input) { - add_audio_input_track(audio_input.c_str()); + GtkWidget *row = nullptr; + if(starts_with(audio_input, "app:")) { + if(!gsr_info.system_info.supports_app_audio) + continue; + + std::string audio_input_name = audio_input.substr(4); + const std::string *app_audio_existing = get_application_audio_by_name_case_insensitive(application_audio, audio_input_name); + if(app_audio_existing) + row = create_application_audio_combo_box_row(*app_audio_existing); + else + row = create_application_audio_custom_row(std::move(audio_input_name)); + } else if(starts_with(audio_input, "device:")) { + row = create_audio_device_combo_box_row(audio_input.substr(7)); + } else { + row = create_audio_device_combo_box_row(audio_input); + } + + gtk_widget_show_all(row); + gtk_box_pack_start(audio_devices_items_box, row, false, false, 0); } - if(config_empty && config.main_config.audio_input.empty()) - add_audio_input_track("Default output"); + if(config_empty && config.main_config.audio_input.empty()) { + GtkWidget *row = create_audio_device_combo_box_row("Default output"); + gtk_widget_show_all(row); + gtk_box_pack_start(audio_devices_items_box, row, false, false, 0); + } gtk_combo_box_set_active_id(GTK_COMBO_BOX(color_range_input_menu), config.main_config.color_range.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(quality_input_menu), config.main_config.quality.c_str()); - gtk_combo_box_set_active_id(GTK_COMBO_BOX(video_codec_input_menu), config.main_config.codec.c_str()); + video_codec_selection_menu_set_active_id("auto"); + video_codec_selection_menu_set_active_id(config.main_config.codec.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(audio_codec_input_menu), config.main_config.audio_codec.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(framerate_mode_input_menu), config.main_config.framerate_mode.c_str()); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(overclock_button), config.main_config.overclock); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_notification_button), config.main_config.show_notifications); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_started_notification_button), config.main_config.show_recording_started_notifications); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_stopped_notification_button), config.main_config.show_recording_stopped_notifications); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(show_recording_saved_notification_button), config.main_config.show_recording_saved_notifications); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record_cursor_button), config.main_config.record_cursor); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(hide_window_when_recording_menu_item), config.main_config.hide_window_when_recording); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(restore_portal_session_button), config.main_config.restore_portal_session); gtk_combo_box_set_active_id(GTK_COMBO_BOX(stream_service_input_menu), config.streaming_config.streaming_service.c_str()); gtk_entry_set_text(youtube_stream_id_entry, config.streaming_config.youtube.stream_key.c_str()); gtk_entry_set_text(twitch_stream_id_entry, config.streaming_config.twitch.stream_key.c_str()); gtk_entry_set_text(custom_stream_url_entry, config.streaming_config.custom.url.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(custom_stream_container), config.streaming_config.custom.container.c_str()); - if(!wayland && streaming_hotkey_button && !config_empty) { - streaming_hotkey.keysym = config.streaming_config.start_recording_hotkey.keysym; - streaming_hotkey.modkey_mask = config.streaming_config.start_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(streaming_hotkey_button), streaming_hotkey); - } gtk_button_set_label(record_file_chooser_button, config.record_config.save_directory.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(record_container), config.record_config.container.c_str()); - if(!wayland && record_hotkey_button && !config_empty) { - record_hotkey.keysym = config.record_config.start_recording_hotkey.keysym; - record_hotkey.modkey_mask = config.record_config.start_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(record_hotkey_button), record_hotkey); - } - if(!wayland && pause_unpause_hotkey_button && !config_empty) { - pause_unpause_hotkey.keysym = config.record_config.pause_recording_hotkey.keysym; - pause_unpause_hotkey.modkey_mask = config.record_config.pause_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(pause_unpause_hotkey_button), pause_unpause_hotkey); - } gtk_button_set_label(replay_file_chooser_button, config.replay_config.save_directory.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(replay_container), config.replay_config.container.c_str()); gtk_spin_button_set_value(replay_time_entry, config.replay_config.replay_time); - if(!wayland && replay_start_stop_hotkey_button && !config_empty) { - replay_start_stop_hotkey.keysym = config.replay_config.start_recording_hotkey.keysym; - replay_start_stop_hotkey.modkey_mask = config.replay_config.start_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(replay_start_stop_hotkey_button), replay_start_stop_hotkey); - } - if(!wayland && replay_save_hotkey_button && !config_empty) { - replay_save_hotkey.keysym = config.replay_config.save_recording_hotkey.keysym; - replay_save_hotkey.modkey_mask = config.replay_config.save_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(replay_save_hotkey_button), replay_save_hotkey); - } gtk_combo_box_set_active_id(GTK_COMBO_BOX(view_combo_box), config.main_config.advanced_view ? "advanced" : "simple"); view_combo_box_change_callback(GTK_COMBO_BOX(view_combo_box), view_combo_box); - if(!wayland) { - gtk_widget_set_visible(record_hotkey.hotkey_active_label, false); - gtk_widget_set_visible(pause_unpause_hotkey.hotkey_active_label, false); - gtk_widget_set_visible(streaming_hotkey.hotkey_active_label, false); + quality_combo_box_change_callback(GTK_COMBO_BOX(quality_input_menu), quality_input_menu); + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { + if(!config_empty) { + for(int i = 0; i < num_hotkeys; ++i) { + hotkeys[i]->keysym = hotkeys[i]->config->keysym; + hotkeys[i]->modkey_mask = hotkeys[i]->config->modifiers; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(hotkeys[i]->hotkey_entry), *hotkeys[i]); + } + } + + gtk_widget_set_visible(record_start_stop_hotkey.hotkey_active_label, false); + gtk_widget_set_visible(streaming_start_stop_hotkey.hotkey_active_label, false); gtk_widget_set_visible(replay_start_stop_hotkey.hotkey_active_label, false); - gtk_widget_set_visible(replay_save_hotkey.hotkey_active_label, false); } - enable_stream_record_button_if_info_filled(); + record_area_item_change_callback(nullptr, nullptr); stream_service_item_change_callback(GTK_COMBO_BOX(stream_service_input_menu), nullptr); - if(supported_video_codecs_exit_status != 0) { - const char *cmd = flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder -w screen -f 60 -o video.mp4" : "gpu-screen-recorder -w screen -f 60 -o video.mp4"; - GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Failed to run 'gpu-screen-recorder' command. If you are using gpu-screen-recorder flatpak then this is a bug. Otherwise you need to make sure gpu-screen-recorder is installed on your system and working properly. Run:\n" - "%s\n" - "in a terminal to see more information about the issue.", cmd); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(select_window_userdata.app)); - return; - } + on_change_video_resolution_button_click(GTK_BUTTON(change_video_resolution_button), nullptr); - if(!supported_video_codecs.h264 && !supported_video_codecs.hevc && gpu_inf.vendor != GPU_VENDOR_NVIDIA && config.main_config.codec != "av1") { - if(supported_video_codecs.av1) { - GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, - "Switched video codec to AV1 since H264/HEVC video encoding is either missing or disabled on your system. If you know that your system supports H264/HEVC video encoding and " - "you are using the flatpak version of GPU Screen Recorder then try installing mesa-extra freedesktop runtime by running this command:\n" - "flatpak install --system org.freedesktop.Platform.GL.default//23.08-extra\n" - "and then restart GPU Screen Recorder. If that doesn't work then you may have to install another mesa package for your distro.\n" - "If you are using a distro such as manjaro which disables hardware accelerated video encoding then you can also try the <a href=\"https://flathub.org/apps/com.dec05eba.gpu_screen_recorder\">flatpak version of GPU Screen Recorder</a> instead which doesn't have this issue."); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - config.main_config.codec = "av1"; - gtk_combo_box_set_active_id(GTK_COMBO_BOX(video_codec_input_menu), config.main_config.codec.c_str()); - } else { - GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "H264/HEVC video encoding is either missing or disabled on your system. If you know that your system supports H264/HEVC video encoding and " - "you are using the flatpak version of GPU Screen Recorder then try installing mesa-extra freedesktop runtime by running this command:\n" - "flatpak install --system org.freedesktop.Platform.GL.default//23.08-extra\n" - "and then restart GPU Screen Recorder. If that doesn't work then you may have to install another mesa package for your distro.\n" - "If you are using a distro such as manjaro which disables hardware accelerated video encoding then you can also try the <a href=\"https://flathub.org/apps/com.dec05eba.gpu_screen_recorder\">flatpak version of GPU Screen Recorder</a> instead which doesn't have this issue."); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(select_window_userdata.app)); - return; - } + if(!gsr_info.system_info.supports_app_audio) { + gtk_widget_set_visible(GTK_WIDGET(add_application_audio_button), false); + gtk_widget_set_visible(GTK_WIDGET(add_custom_application_audio_button), false); + gtk_widget_set_visible(GTK_WIDGET(record_app_audio_inverted_button), false); } - if(!supported_video_codecs.h264 && !supported_video_codecs.hevc && gpu_inf.vendor == GPU_VENDOR_NVIDIA) { - GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Failed to find H264/HEVC video codecs. Your NVIDIA GPU may be missing support for H264/HEVC video codecs for video encoding."); + std::string dummy; + if(!config.main_config.software_encoding_warning_shown && !switch_video_codec_to_usable_hardware_encoder(dummy)) { + GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, + "Unable to find a hardware video encoder on your system, using software video encoder instead (slow!). If you know that your system supports H264/HEVC hardware video encoding and " + "you are using the flatpak version of GPU Screen Recorder then try installing mesa-extra freedesktop runtime by running this command:\n" + "flatpak install --system org.freedesktop.Platform.GL.default//23.08-extra\n" + "and then restart GPU Screen Recorder. If that doesn't work then you may have to install another mesa package for your distro if you are using AMD.\n" + "If you are using NVIDIA then you might need to run the \"nvidia-smi\" command first before starting GPU Screen Recorder if NVIDIA is incorrectly setup on your distro.\n" + "If you are using a distro such as manjaro which disables hardware accelerated video encoding then you can also try the <a href=\"https://flathub.org/apps/com.dec05eba.gpu_screen_recorder\">flatpak version of GPU Screen Recorder</a> instead which doesn't have this issue."); + set_dialog_selectable(dialog); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(select_window_userdata.app)); - return; + config.main_config.software_encoding_warning_shown = true; + video_codec_selection_menu_set_active_id("h264_software"); + config.main_config.advanced_view = true; + gtk_combo_box_set_active_id(GTK_COMBO_BOX(view_combo_box), "advanced"); } -} - -static bool gl_get_gpu_info(gsr_egl *egl, gpu_info *info) { - const char *software_renderers[] = { "llvmpipe", "SWR", "softpipe", NULL }; - bool supported = true; - const unsigned char *gl_vendor = egl->glGetString(GL_VENDOR); - const unsigned char *gl_renderer = egl->glGetString(GL_RENDERER); - info->gpu_version = 0; - - if(!gl_vendor) { - fprintf(stderr, "Error: failed to get gpu vendor\n"); - supported = false; - goto end; + if(gsr_info.system_info.is_steam_deck && !config.main_config.steam_deck_warning_shown) { + GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, + "Steam deck has multiple driver bugs, some which have been introduced in the last few months. For example one of them has been reported here: " + "<a href=\"https://github.com/ValveSoftware/SteamOS/issues/1609\">https://github.com/ValveSoftware/SteamOS/issues/1609</a>.\n" + "If you have issues with GPU Screen Recorder on steam deck but not on a desktop computer then report the issue to Valve and/or AMD."); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + config.main_config.steam_deck_warning_shown = true; } +} - if(gl_renderer) { - for(int i = 0; software_renderers[i]; ++i) { - if(strstr((const char*)gl_renderer, software_renderers[i])) { - fprintf(stderr, "gsr error: your opengl environment is not properly setup. It's using %s (software rendering) for opengl instead of your graphics card. Please make sure your graphics driver is properly installed\n", software_renderers[i]); - supported = false; - goto end; - } +static void init_shortcuts_callback(bool success, void *userdata) { + (void)userdata; + global_shortcuts_initialized = success; + if(success) { + gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_grid), true); + gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_grid), true); + gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_grid), true); + + gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_not_supported_label), false); + gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_not_supported_label), false); + gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_not_supported_label), false); + + if(!gsr_global_shortcuts_list_shortcuts(&global_shortcuts, shortcut_changed_callback, NULL)) { + fprintf(stderr, "gsr error: failed to list shortcuts\n"); } - } - - if(strstr((const char*)gl_vendor, "AMD")) - info->vendor = GPU_VENDOR_AMD; - else if(strstr((const char*)gl_vendor, "Intel")) - info->vendor = GPU_VENDOR_INTEL; - else if(strstr((const char*)gl_vendor, "NVIDIA")) - info->vendor = GPU_VENDOR_NVIDIA; - else { - fprintf(stderr, "Error: unknown gpu vendor: %s\n", gl_vendor); - supported = false; - goto end; - } + gsr_global_shortcuts_subscribe_activated_signal(&global_shortcuts, deactivated_callback, shortcut_changed_callback, NULL); + } else { + gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_grid), false); + gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_grid), false); + gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_grid), false); - if(gl_renderer) { - if(info->vendor == GPU_VENDOR_NVIDIA) - sscanf((const char*)gl_renderer, "%*s %*s %*s %d", &info->gpu_version); + gtk_widget_set_visible(GTK_WIDGET(recording_hotkeys_not_supported_label), true); + gtk_widget_set_visible(GTK_WIDGET(replay_hotkeys_not_supported_label), true); + gtk_widget_set_visible(GTK_WIDGET(streaming_hotkeys_not_supported_label), true); } - - end: - return supported; -} - -static bool is_xwayland(Display *dpy) { - int opcode, event, error; - if(XQueryExtension(dpy, "XWAYLAND", &opcode, &event, &error)) - return true; - - bool xwayland_found = false; - for_each_active_monitor_output_x11(dpy, [&xwayland_found](const gsr_monitor *monitor, void*) { - if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0) - xwayland_found = true; - else if(memmem(monitor->name, monitor->name_len, "X11", 3)) - xwayland_found = true; - }, NULL); - return xwayland_found; } -static const char* gpu_vendor_to_name(gpu_vendor vendor) { +static const char* gpu_vendor_to_name(GpuVendor vendor) { switch(vendor) { - case GPU_VENDOR_AMD: return "AMD"; - case GPU_VENDOR_INTEL: return "Intel"; - case GPU_VENDOR_NVIDIA: return "NVIDIA"; + case GpuVendor::UNKNOWN: return "Unknown"; + case GpuVendor::AMD: return "AMD"; + case GpuVendor::INTEL: return "Intel"; + case GpuVendor::NVIDIA: return "NVIDIA"; + case GpuVendor::BROADCOM: return "Broadcom"; } return ""; } -static void activate(GtkApplication *app, gpointer) { - flatpak = is_inside_flatpak(); +static bool gsr_startup_validation() { + if(gsr_info_exit_status == GsrInfoExitStatus::FAILED_TO_RUN_COMMAND) { + const char *cmd = flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder -w screen -f 60 -o video.mp4" : "gpu-screen-recorder -w screen -f 60 -o video.mp4"; + GtkWidget *dialog = gtk_message_dialog_new_with_markup(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "Failed to run 'gpu-screen-recorder' command. If you are using gpu-screen-recorder flatpak then this is a bug. Otherwise you need to make sure gpu-screen-recorder is installed on your system and working properly (install necessary depedencies depending on your GPU, such as libva-mesa-driver, libva-intel-driver, intel-media-driver and linux-firmware). Run:\n" + "%s\n" + "in a terminal to see more information about the issue.", cmd); + set_dialog_selectable(dialog); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return false; + } - if(!wayland && !dpy) { + if(gsr_info_exit_status == GsrInfoExitStatus::OPENGL_FAILED) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Neither X11 nor Wayland is running."); + "Failed to get OpenGL information. Make sure your GPU drivers are properly installed.\n" + "If you are using nvidia then make sure to run \"flatpak update\" to make sure that your flatpak nvidia driver version matches your distros nvidia driver version. If this doesn't work then you might need to manually install a flatpak nvidia driver version that matches your distros nvidia driver version.\n" + "If you are using nvidia and have recently updated your nvidia driver then make sure to reboot your computer first."); + set_dialog_selectable(dialog); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(app)); - return; + return false; } - nvfbc_installed = !wayland && is_nv_fbc_installed(); - - if(!gsr_egl_load(&egl, dpy, wayland)) { + if(gsr_info_exit_status == GsrInfoExitStatus::NO_DRM_CARD) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Failed to load OpenGL. Make sure your GPU drivers are properly installed."); + "Failed to find a valid DRM card. If you are running GPU Screen Recorder with prime-run then try running without it."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(app)); - return; + return false; } - if(!gl_get_gpu_info(&egl, &gpu_inf)) { + if(gsr_info.system_info.display_server == DisplayServer::UNKNOWN) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Failed to get OpenGL information. Make sure your GPU drivers are properly installed. " - "If you are using nvidia then make sure to run \"flatpak update\" to make sure that your flatpak nvidia driver version matches your distros nvidia driver version. If this doesn't work then you might need to manually install a flatpak nvidia driver version that matches your distros nvidia driver version."); + "Neither X11 nor Wayland is running."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(app)); - return; + return false; } - if((gpu_inf.vendor != GPU_VENDOR_NVIDIA) || wayland) { - if(!gsr_get_valid_card_path(&egl, egl.card_path)) { - GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Failed to find a valid DRM card. If you are running GPU Screen Recorder with prime-run then try running without it."); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(app)); - return; - } - } else { - egl.card_path[0] = '\0'; + if(gsr_info.system_info.display_server == DisplayServer::X11 && !dpy) { + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "Failed to connect to the X11 server"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + return false; } - if(gpu_inf.vendor == GPU_VENDOR_NVIDIA) { + if(gsr_info.gpu_info.vendor == GpuVendor::NVIDIA) { if(!is_cuda_installed()) { GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "CUDA is not installed on your system. GPU Screen Recorder requires CUDA to be installed to work with a NVIDIA GPU."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(app)); - return; + return false; } if(!is_nvenc_installed()) { @@ -3707,32 +4401,57 @@ static void activate(GtkApplication *app, gpointer) { "NVENC is not installed on your system. GPU Screen Recorder requires NVENC to be installed to work with a NVIDIA GPU."); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - g_application_quit(G_APPLICATION(app)); - return; + return false; } } - supported_video_codecs_exit_status = get_supported_video_codecs(&supported_video_codecs); + return true; +} + +static void activate(GtkApplication *app, gpointer) { + if(!gsr_startup_validation()) { + g_application_quit(G_APPLICATION(app)); + return; + } + + page_navigation_userdata.app = app; std::string window_title = "GPU Screen Recorder | Running on "; - window_title += gpu_vendor_to_name(gpu_inf.vendor); + window_title += gpu_vendor_to_name(gsr_info.gpu_info.vendor); window = gtk_application_window_new(app); g_signal_connect(window, "destroy", G_CALLBACK(on_destroy_window), nullptr); gtk_window_set_title(GTK_WINDOW(window), window_title.c_str()); gtk_window_set_resizable(GTK_WINDOW(window), false); - select_window_userdata.app = app; - audio_inputs = get_pulseaudio_inputs(); - pa_default_sources = get_pulseaudio_default_inputs(); - - if(!pa_default_sources.default_source_name.empty() && audio_inputs_contains(audio_inputs, pa_default_sources.default_source_name)) - audio_inputs.insert(audio_inputs.begin(), { pa_default_sources.default_source_name.c_str(), "Default input" }); + GtkIconTheme *icon_theme = gtk_icon_theme_get_default(); +#ifdef GSR_ICONS_PATH + const char *icon_path = GSR_ICONS_PATH; +#else + const char *icon_path = "/usr/share/icons"; +#endif + gtk_icon_theme_prepend_search_path(icon_theme, icon_path); + + const char *icon_name = "com.dec05eba.gpu_screen_recorder"; + if(!gtk_icon_theme_has_icon(icon_theme, icon_name)) + fprintf(stderr, "Error: failed to find icon %s in %s\n", icon_name, icon_path); + + gtk_window_set_default_icon_name(icon_name); + gtk_window_set_icon_name(GTK_WINDOW(window), icon_name); + + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + if(gdk_wayland_display_query_registry(gdk_display_get_default(), "hyprland_global_shortcuts_manager_v1")) { + wayland_compositor = WaylandCompositor::HYPRLAND; + } else if(gdk_wayland_display_query_registry(gdk_display_get_default(), "org_kde_plasma_shell")) { + wayland_compositor = WaylandCompositor::KDE; + } + } - if(!pa_default_sources.default_sink_name.empty() && audio_inputs_contains(audio_inputs, pa_default_sources.default_sink_name)) - audio_inputs.insert(audio_inputs.begin(), { pa_default_sources.default_sink_name.c_str(), "Default output" }); + select_window_userdata.app = app; + audio_inputs = get_audio_devices(); + application_audio = get_application_audio(); - if(!wayland) + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) crosshair_cursor = XCreateFontCursor(gdk_x11_get_default_xdisplay(), XC_crosshair); GtkStack *stack = GTK_STACK(gtk_stack_new()); @@ -3740,13 +4459,12 @@ static void activate(GtkApplication *app, gpointer) { gtk_stack_set_transition_type(stack, GTK_STACK_TRANSITION_TYPE_NONE); gtk_stack_set_transition_duration(stack, 0); gtk_stack_set_homogeneous(stack, false); - GtkWidget *common_settings_page = create_common_settings_page(stack, app, gpu_inf); + GtkWidget *common_settings_page = create_common_settings_page(stack, app); GtkWidget *replay_page = create_replay_page(app, stack); GtkWidget *recording_page = create_recording_page(app, stack); GtkWidget *streaming_page = create_streaming_page(app, stack); gtk_stack_set_visible_child(stack, common_settings_page); - page_navigation_userdata.app = app; page_navigation_userdata.stack = stack; page_navigation_userdata.common_settings_page = common_settings_page; page_navigation_userdata.replay_page = replay_page; @@ -3762,7 +4480,7 @@ static void activate(GtkApplication *app, gpointer) { g_signal_connect(stream_button, "clicked", G_CALLBACK(on_start_streaming_click), &page_navigation_userdata); g_signal_connect(stream_back_button, "clicked", G_CALLBACK(on_streaming_recording_replay_page_back_click), &page_navigation_userdata); - if(!wayland) { + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { xim = XOpenIM(gdk_x11_get_default_xdisplay(), NULL, NULL, NULL); xic = XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, NULL); @@ -3775,36 +4493,169 @@ static void activate(GtkApplication *app, gpointer) { g_timeout_add(500, timer_timeout_handler, app); gtk_widget_show_all(window); - load_config(gpu_inf); + load_config(); + + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + init_shortcuts_callback(false, nullptr); + // TODO: + // Disable global hotkeys on Hyprland for now. It crashes the hyprland desktop portal. + // When it's re-enabled on Hyprland it will need special handing where it does BindShortcuts immediately on startup + // instead of having a "register hotkeys" button. This needed because Hyprland doesn't remember registered hotkeys after + // the desktop portal is restarted (when the computer is restarted for example). + + if(wayland_compositor == WaylandCompositor::HYPRLAND) { + const char *hotkeys_not_supported_text = "Hotkeys have been disabled on your system because of a Hyprland bug.\nUse X11 or KDE Plasma on Wayland if you want to use hotkeys."; + gtk_label_set_text(GTK_LABEL(recording_hotkeys_not_supported_label), hotkeys_not_supported_text); + gtk_label_set_text(GTK_LABEL(replay_hotkeys_not_supported_label), hotkeys_not_supported_text); + gtk_label_set_text(GTK_LABEL(streaming_hotkeys_not_supported_label), hotkeys_not_supported_text); + } else { + if(!gsr_global_shortcuts_init(&global_shortcuts, init_shortcuts_callback, NULL)) { + fprintf(stderr, "gsr error: failed to initialize global shortcuts\n"); + } + } + } +} + +static bool is_kms_server_proxy_installed() { + const int exit_code = system("flatpak-spawn --host -- /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy is-setup"); + return exit_code == 0; +} + +static void gtk_activate_handler_run_and_quit(GtkApplication *app, gpointer userdata) { + std::function<void()> *handler = (std::function<void()>*)userdata; + (*handler)(); + g_application_quit(G_APPLICATION(app)); +} + +static void start_gtk_run_handler(std::function<void()> handler) { + char app_id[] = "com.dec05eba.gpu_screen_recorder"; + // Gtk sets wayland app id / x11 wm class from the binary name, so we override it here. + // This is needed for the correct window icon on wayland (app id needs to match the desktop file name). + char *argv[1] = { app_id }; + GtkApplication *app = gtk_application_new(app_id, G_APPLICATION_NON_UNIQUE); + g_signal_connect(app, "activate", G_CALLBACK(gtk_activate_handler_run_and_quit), &handler); + g_application_run(G_APPLICATION(app), 1, argv); + g_object_unref(app); +} + +static void startup_new_ui(bool launched_by_daemon) { + if(!dpy) { + if(launched_by_daemon) { + fprintf(stderr, "Error: failed to connect to the X11 server, assuming no graphical session has started yet\n"); + exit(1); + } else { + start_gtk_run_handler([]() { + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "Failed to connect to the X11 server while trying to start the new GPU Screen Recorder UI. Please install X11 on your system to use the new UI"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + }); + + config.main_config.use_new_ui = false; + save_config(config); + return; + } + } + + start_gtk_run_handler([]() { + if(!gsr_startup_validation()) + exit(1); + }); + + if(!flatpak_is_installed_as_system()) { + start_gtk_run_handler([]() { + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "GPU Screen Recorder needs to be installed system-wide to use the new UI. You can run this command to install GPU Screen recorder system-wide:\n" + "flatpak install --system com.dec05eba.gpu_screen_recorder\n"); + set_dialog_selectable(dialog); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + }); + exit(1); + } + + if(config.main_config.installed_gsr_global_hotkeys_version != GSR_CURRENT_GLOBAL_HOTKEYS_CODE_VERSION) { + bool finished = false; + start_gtk_run_handler([&finished]() { + finished = kms_server_proxy_setup_gsr_ui( + "An update is available. The new GPU Screen Recorder UI needs root privileges to finish update to make global hotkeys and recording work on any system.\n" + "You will need to restart the application to apply the update.\n" + "\n" + "Are you sure you want to continue?"); + }); + + if(!finished) + return; + } else if(!is_kms_server_proxy_installed()) { + bool finished = false; + start_gtk_run_handler([&finished]() { + finished = kms_server_proxy_setup_gsr_ui( + "Required files are missing to launch the new GPU Screen Recorder UI. These files will be installed again.\n" + "\n" + "Are you sure you want to continue?"); + }); + + if(!finished) + return; + } + + if(dpy) + XCloseDisplay(dpy); + + launch_gsr_ui(launched_by_daemon); + exit(0); } int main(int argc, char **argv) { setlocale(LC_ALL, "C"); - // Stop nvidia driver from buffering frames - setenv("__GL_MaxFramesAllowed", "1", true); - // If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context, - // so we overwrite it - setenv("__GL_THREADED_OPTIMIZATIONS", "0", true); - // Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since - // nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet. - // Let vaapi find the match vaapi driver instead of forcing a specific one. - unsetenv("LIBVA_DRIVER_NAME"); - // Some people set this to force all applications to vsync on nvidia, but this makes eglSwapBuffers never return. - unsetenv("__GL_SYNC_TO_VBLANK"); - // Same as above, but for amd/intel - unsetenv("vblank_mode"); + const bool use_old_ui_opt = argc >= 2 && strcmp(argv[1], "use-old-ui") == 0; + const bool launched_by_daemon_opt = argc >= 2 && strcmp(argv[1], "gsr-ui") == 0; + argc = 1; + + if(geteuid() == 0) { + fprintf(stderr, "Error: don't run gpu-screen-recorder-gtk as the root user\n"); + return 1; + } dpy = XOpenDisplay(NULL); - wayland = !dpy || is_xwayland(dpy); - if(wayland) { - setenv("GDK_BACKEND", "wayland", true); - } else { - setenv("GDK_BACKEND", "x11", true); + config_empty = false; + config = read_config(config_empty); + + if(!dpy && launched_by_daemon_opt && config.main_config.use_new_ui) { + fprintf(stderr, "Error: failed to connect to the X11 server, assuming no graphical session has started yet\n"); + exit(1); } - GtkApplication *app = gtk_application_new("com.dec05eba.gpu_screen_recorder", G_APPLICATION_NON_UNIQUE); + gsr_info_exit_status = get_gpu_screen_recorder_info(&gsr_info); + if(gsr_info_exit_status == GsrInfoExitStatus::OK) { + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + setenv("GDK_BACKEND", "wayland", true); + } else { + setenv("GDK_BACKEND", "x11", true); + } + } + + flatpak = is_inside_flatpak(); + nvfbc_installed = gsr_info.system_info.display_server != DisplayServer::WAYLAND && is_nv_fbc_installed(); + + if(use_old_ui_opt) { + system("flatpak-spawn --host -- systemctl disable --user gpu-screen-recorder-ui"); + system("flatpak-spawn --host -- systemctl stop --user gpu-screen-recorder-ui"); + config.main_config.use_new_ui = false; + save_config(config); + } + + if(config.main_config.use_new_ui) + startup_new_ui(launched_by_daemon_opt); + + char app_id[] = "com.dec05eba.gpu_screen_recorder"; + // Gtk sets wayland app id / x11 wm class from the binary name, so we override it here. + // This is needed for the correct window icon on wayland (app id needs to match the desktop file name). + argv[0] = app_id; + + GtkApplication *app = gtk_application_new(app_id, G_APPLICATION_NON_UNIQUE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); |