aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--README.md25
-rw-r--r--TODO86
m---------depends/mglpp0
-rw-r--r--extra/gpu-screen-recorder-ui.service2
-rw-r--r--images/delete.pngbin0 -> 511 bytes
-rw-r--r--images/ps4_cross.pngbin0 -> 1270 bytes
-rw-r--r--images/ps4_triangle.pngbin0 -> 1446 bytes
-rw-r--r--images/screenshot.pngbin7276 -> 2020 bytes
-rw-r--r--images/settings_extra_small.pngbin0 -> 959 bytes
-rw-r--r--images/trash.pngbin0 -> 410 bytes
-rw-r--r--include/Config.hpp30
-rw-r--r--include/CursorTracker/CursorTracker.hpp23
-rw-r--r--include/CursorTracker/CursorTrackerWayland.hpp43
-rw-r--r--include/CursorTracker/CursorTrackerX11.hpp20
-rw-r--r--include/GlobalHotkeys/GlobalHotkeys.hpp (renamed from include/GlobalHotkeys.hpp)0
-rw-r--r--include/GlobalHotkeys/GlobalHotkeysJoystick.hpp (renamed from include/GlobalHotkeysJoystick.hpp)8
-rw-r--r--include/GlobalHotkeys/GlobalHotkeysLinux.hpp (renamed from include/GlobalHotkeysLinux.hpp)2
-rw-r--r--include/GlobalHotkeys/GlobalHotkeysX11.hpp (renamed from include/GlobalHotkeysX11.hpp)0
-rw-r--r--include/Overlay.hpp40
-rw-r--r--include/RegionSelector.hpp1
-rw-r--r--include/SafeVector.hpp137
-rw-r--r--include/Theme.hpp4
-rw-r--r--include/Utils.hpp3
-rw-r--r--include/WindowSelector.hpp33
-rw-r--r--include/WindowUtils.hpp2
-rw-r--r--include/gui/DropdownButton.hpp2
-rw-r--r--include/gui/GlobalSettingsPage.hpp5
-rw-r--r--include/gui/List.hpp5
-rw-r--r--include/gui/Page.hpp4
-rw-r--r--include/gui/RadioButton.hpp3
-rw-r--r--include/gui/ScreenshotSettingsPage.hpp2
-rw-r--r--include/gui/ScrollablePage.hpp1
-rw-r--r--include/gui/SettingsPage.hpp51
-rw-r--r--include/gui/Subsection.hpp5
-rw-r--r--include/gui/Widget.hpp6
-rw-r--r--meson.build19
-rw-r--r--project.conf5
-rw-r--r--protocol/meson.build25
-rw-r--r--protocol/xdg-output-unstable-v1.xml222
-rw-r--r--src/Config.cpp109
-rw-r--r--src/CursorTracker/CursorTrackerWayland.cpp538
-rw-r--r--src/CursorTracker/CursorTrackerX11.cpp29
-rw-r--r--src/GlobalHotkeys/GlobalHotkeysJoystick.cpp (renamed from src/GlobalHotkeysJoystick.cpp)118
-rw-r--r--src/GlobalHotkeys/GlobalHotkeysLinux.cpp (renamed from src/GlobalHotkeysLinux.cpp)39
-rw-r--r--src/GlobalHotkeys/GlobalHotkeysX11.cpp (renamed from src/GlobalHotkeysX11.cpp)2
-rw-r--r--src/GsrInfo.cpp5
-rw-r--r--src/Overlay.cpp1105
-rw-r--r--src/Process.cpp23
-rw-r--r--src/RegionSelector.cpp6
-rw-r--r--src/Theme.cpp54
-rw-r--r--src/Utils.cpp32
-rw-r--r--src/WindowSelector.cpp229
-rw-r--r--src/WindowUtils.cpp45
-rw-r--r--src/gui/Button.cpp2
-rw-r--r--src/gui/ComboBox.cpp2
-rw-r--r--src/gui/DropdownButton.cpp35
-rw-r--r--src/gui/GlobalSettingsPage.cpp48
-rw-r--r--src/gui/GsrPage.cpp5
-rw-r--r--src/gui/List.cpp30
-rw-r--r--src/gui/Page.cpp15
-rw-r--r--src/gui/RadioButton.cpp11
-rw-r--r--src/gui/ScreenshotSettingsPage.cpp23
-rw-r--r--src/gui/ScrollablePage.cpp13
-rw-r--r--src/gui/SettingsPage.cpp505
-rw-r--r--src/gui/StaticPage.cpp5
-rw-r--r--src/gui/Subsection.cpp17
-rw-r--r--src/gui/Widget.cpp16
-rw-r--r--src/main.cpp79
-rw-r--r--tools/gsr-global-hotkeys/README.md6
-rw-r--r--tools/gsr-global-hotkeys/keyboard_event.c100
-rw-r--r--tools/gsr-global-hotkeys/keyboard_event.h2
-rw-r--r--tools/gsr-global-hotkeys/main.c14
-rw-r--r--tools/gsr-ui-cli/main.c6
74 files changed, 3330 insertions, 756 deletions
diff --git a/.gitignore b/.gitignore
index fee7148..d9aeca8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,5 @@
sibs-build/
compile_commands.json
-gpu-screen-recorder-overlay-daemon/sibs-build/
-gpu-screen-recorder-overlay-daemon/compile_commands.json
+**/xdg-output-unstable-v1-client-protocol.h
+**/xdg-output-unstable-v1-protocol.c
diff --git a/README.md b/README.md
index 207091d..b4466ed 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ A program called `gsr-ui-cli` is also installed when installing this software. T
# Installation
If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\
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). This flatpak includes both this UI and gpu-screen-recorder so no need to install that first.
+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 UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI.
@@ -22,11 +22,13 @@ GPU Screen Recorder UI uses meson build system so you need to install `meson` to
## Build dependencies
These are the dependencies needed to build GPU Screen Recorder UI:
-* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxext, libxi)
-* libxcursor
+* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxext, libxi, libxcursor)
* libglvnd (which provides libgl, libglx and libegl)
* linux-api-headers
* libpulse (libpulse-simple)
+* libdrm
+* wayland (wayland-client, wayland-egl, wayland-scanner)
+* setcap (libcap)
## Runtime dependencies
There are also additional dependencies needed at runtime:
@@ -35,14 +37,17 @@ There are also additional dependencies needed at runtime:
* [GPU Screen Recorder Notification](https://git.dec05eba.com/gpu-screen-recorder-notification/)
## Program behavior notes
-This program has to grab all keyboards and create a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.
-This might cause issues for you if you use input remapping software. To workaround this you can go into settings and select "Only grab virtual devices"
+This program has to grab all keyboards and create a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.\
+This might cause issues for you if you use keyboard remapping software. To workaround this you can go into settings and select "Only grab virtual devices".\
+If you use keyboard remapping software such as keyd then make sure to make it ignore "gsr-ui virtual keyboard" (dec0:5eba device id), otherwise your keyboard can get locked
+as gpu screen recorder tries to grab keys and keyd grabs gpu screen recorder, leading to a lock.\
+If you are stuck in such a lock where you cant press and keyboard keys you can press (left) ctrl+shift+alt+esc to close gpu screen recorder and remove it from system startup.
# License
This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.\
`images/default.cur` it part of the [Adwaita icon theme](https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/tree/master) which is licensed under `CC BY-SA 3.0`.\
The controller buttons under `images/` were created by [Julio Cacko](https://juliocacko.itch.io/free-input-prompts) and they are licensed under `CC0 1.0 Universal`.\
-The PlayStation logo under `images/` was created by [ArksDigital](https://arks.itch.io/ps4-buttons) and it's are licensed under `CC BY 4.0`.
+The PlayStation logo under `images/` was created by [ArksDigital](https://arks.itch.io/ps4-buttons) and it's licensed under `CC BY 4.0`.
# Reporting bugs, contributing patches, questions or donation
See [https://git.dec05eba.com/?p=about](https://git.dec05eba.com/?p=about).\
@@ -56,9 +61,13 @@ I'm looking for somebody that can create sound effects for the notifications.
![](https://dec05eba.com/images/settings_page.jpg)
# Known issues
-* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
-* Opening the UI when a game is fullscreened can mess up the game window a bit on Hyprland. I believe this is an issue with Hyprland.
+* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay. Change your waybar dock mode to "dock" in its config to fix this.
+* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland. Change your waybar dock mode to "dock" in its config to fix this.
+* The background of the UI is black when opening the UI while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
+* Unable to close the region selection with escape key while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
# FAQ
## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service
If you have previously used the flatpak version of GPU Screen Recorder with the new UI then the non-flatpak version of the systemd service will conflict with that. Run `gsr-ui` to fix that.
+## I use a non-qwerty keyboard layout and I have an issue with incorrect keys registered in the software
+This is a KDE Plasma Wayland issue. Use `setxkbmap <language>` command, for example `setxkbmap se` to make sure X11 applications (such as this one) gets updated to use your languages keyboard layout.
diff --git a/TODO b/TODO
index cd43872..8f34501 100644
--- a/TODO
+++ b/TODO
@@ -12,10 +12,6 @@ Handle events in draw function because the render position of elements is availa
Add nvidia overclock option.
-Add support for window selection in capture.
-
-Add option to record the focused monitor. This works on wayland too when using kms capture since we can get cursor position without root and see which monitor (crtc) the cursor is on. Or use create_window_get_center_position.
-
Filechooser should have the option to select list view, search bar and common folders/mounted drives on the left side for quick navigation. Also a button to create a new directory.
Restart replay on system start if monitor resolution changes.
@@ -39,8 +35,6 @@ Fix first frame being black when running without a compositor.
Add support for systray.
-Add option to take screenshot.
-
Move event callbacks to a global list instead of std::function object in each widget. This reduces the size of widgets,
since most widgets wont have the event callback set.
This event callback would pass the widget as an argument.
@@ -74,8 +68,6 @@ Run `systemctl status --user gpu-screen-recorder` when starting recording and gi
Add option to select which gpu to record with, or list all monitors and automatically use the gpu associated with the monitor. Do the same in gtk application.
-Dont allow autostart of replay if capture option is window recording (when window recording is added).
-
Use global shortcuts desktop portal protocol on wayland when available.
Support CJK.
@@ -100,9 +92,6 @@ Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change Ex
When enabling X11 global hotkey again only grab lalt, not ralt.
-When adding window capture only add it to recording and streaming and do the window selection when recording starts, to make it more ergonomic with hotkeys.
- If hotkey for recording/streaming start is pressed on the button for start is clicked then hide the ui if it's visible and show the window selection option (cursor).
-
Show an error that prime run will be disabled when using desktop portal capture option. This can cause issues as the user may have selected a video codec option that isn't available on their iGPU but is available on the prime-run dGPU.
For keyboards that report supporting mice the keyboard grab will be delayed until any key has been pressed (and then released), see: https://github.com/dec05eba/gpu-screen-recorder-issues/issues/97
@@ -113,16 +102,13 @@ Check if "modprobe uinput" is needed on some systems (old fedora?).
Add recording timer to see duration of recording/streaming.
Make folder with window name work when using gamescope. Gamescope runs x11 itself so to get the window name inside that we have to connect to the gamescope X11 server (DISPLAY=:1 on x11 and DISPLAY=:2 on wayland, but not always).
+ Detect if the window is gamescope automatically (WM_CLASS = "gamescope") and get the x11 display automatically and connect to it to get the application its running.
+ This seems to only be an issue on wayland? the window title of the gamescope/steam bigpicture mode is the title of the game on x11, so it works automatically on x11.
When clicking on current directory in file manager show a dropdown menu where you can select common directories (HOME, Videos, Downloads and mounted drives) for quick navigation. Maybe even button to search.
Maybe change gsr-ui startup retry time in the systemd service, from 5 seconds to 2 seconds.
-Add support for window capture. This should not prompt for window selection directly but instead prompt for window selection when recording starts and hide the ui first.
- For screenshots window capture should exist but "follow focused" option should not exist.
-
-Improve audio design. It should have a button to add/remove audio tracks and button to add audio into each audio track separately and "record audio from all applications except the selected ones" for each audio track. Then also remove the "merge audio tracks" option.
-
Make it possible to take a screenshot through a button in the ui instead of having to use hotkey.
Handle failing to save a replay. gsr should output "failed to save replay, or something like that" to make it possible to detect that.
@@ -141,13 +127,71 @@ Do xi grab for keys as well. Otherwise the ui cant be used for keyboard input if
Make inactive buttons gray (in dropdown boxes and in the front page with save, etc when replay is not running).
-Implement focused monitor capture. On nvidia x11 just use the x11 monitor name. On wayland get monitor name from drm cursor. We can get x11 monitor by combining all drm monitors together (with either x11 or wayland monitor position info) and then calculating the x11 monitor at that position.
- To get the drm monitor from x11 cursor we can get the x11 monitor then get the CONNECTOR_ID property and get the monitor that matches that. Then copy the drm monitor name code from gsr and use that in gsr-ui to get the same name to use as monitor.
-
Add option to do screen-direct recording. But make it clear that it should not be used, except for gsync on x11 nvidia.
-Add window capture.
-
Add systray for recording status.
Add a desktop icon when gsr-ui has a window mode option (which should be the default launch option).
+
+Verify if cursor tracker monitor name is always correct. It uses the wayland monitor name for recording, but gpu screen recorder uses a custom name created from the drm connector name.
+
+Notification with the focused monitor (with CursorTrackerWayland) assumes that the x11 monitor name is the same as the drm monitor name. Same for find_monitor_by_name.
+
+If CursorTrackerWayland fails then fallback to getting focused monitor by window creation trick. Need to take into consideration prime laptop with dGPU that controls external monitors which cant be captured (different /dev/dri/card device).
+ Maybe automatically switch to recording with the device that controls the monitor.
+ In that case also add all monitors available to capture in the capture list and automatically choose the gpu that controls the monitor.
+
+Support cjk font. Use fc-match to find the location of the font. This also works in flatpak, in which case the fonts are in /run/host/..., where it lists system fonts.
+
+Keyboard layout is incorrect on wayland when using kde plasma keyboard settings to setup multiple keyboards, for example when changing to french.
+ Text input is correct, but hotkey is incorrect.
+ Need to use "setxkbmap fr" as well.
+ This happens only when grabbing keyboard (gsr-global-hotkeys). Same thing is seen with xev.
+
+Getting focused monitor on wayland doesn't work when vrr is enabled. This is because it uses software cursor instead (at least on kde plasma wayland).
+ Right now it falls back to create window & getting window position trick if there is no cursor visible (or a software cursor) and one monitor has vrr enabled.
+ Remove this when linux & wayland supports vrr with hardware cursor plane.
+ Find out another way to get cursor position on wayland.
+ This was fixed in linux 6.11 and in kde plasma in this commit: https://invent.kde.org/plasma/kwin/-/merge_requests/7582/diffs.
+
+Add option to start recording/replay/stream after the notification has disappeared. Show "Starting recording on this monitor in 3 seconds".
+ See if we can use hardware overlay plane instead somehow.
+
+When using wayland for mgl try using wlr-layer-shell and set layer to overlay and keyboard interactivity to exclusive. Do something similar for notifications.
+
+When starting gsr-ui remove any temporary replay disk data that has possibly remained from a crash, by looking for all folders that starts with gsr-replay and end with .gsr, in the replay directory.
+
+Add restart program button, in global settings. It should do almost the same thing as exit program, execept execv gsr-ui.
+
+When gpu screen recorder ui can run as a regular window (and supports tray icon and global shortcut portal) remove gpu screen recorder gtk. Then all error checking needs to be moved from that project to this project.
+ May need support for multi windows, or create a small project to display dialogs.
+
+Add a bug report page that automatically includes system info (make this clear to the user).
+ Do this by sending the report to a custom server that stores that data.
+ The server should limit reports per IP to prevent spam.
+
+Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers.
+
+Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland.
+
+Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing.
+
+Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
+
+Support freetype for text rendering. Maybe load freetype as runtime (with dlopen) and use that when available and fallback to stb_freetype if not available.
+
+Show .webm container option. It's currently chosen automatically if vp8/vp9 is chosen. The available containers should automatically switch depending on the video codec.
+
+In settings show audio levels for each audio. Maybe show audio level image beside the audio name in the dropdown box and switch to a different image (have 3-4 different images for each level) depending on the volume.
+
+Only use fake cursor on wayland if the focused x11 window is fullscreen.
+
+Create window as a real overlay window, using layer shell protocol, when possible. This will however minimize windows on floating wms. Check if this can be fixed somehow, or only use layer shell in tiling wms.
+
+Add timeout option for screenshots.
+
+Add a window that shows a warning for wayland users, that wayland doesn't support this software and if they experience issues then they should use x11 instead.
+
+Add a window that shows a warning if gpu video encoding isn't supported.
+
+Disable system notifications when recording. Does the notification dbus interface support pausing notifications?
diff --git a/depends/mglpp b/depends/mglpp
-Subproject 7d6e67668ba317d823f58f9bd8edbea2da9f4b6
+Subproject a784fdb62b1ddfc8c38733c3a16cd1f39e5d415
diff --git a/extra/gpu-screen-recorder-ui.service b/extra/gpu-screen-recorder-ui.service
index 8abb3de..e6120e6 100644
--- a/extra/gpu-screen-recorder-ui.service
+++ b/extra/gpu-screen-recorder-ui.service
@@ -2,7 +2,7 @@
Description=GPU Screen Recorder UI Service
[Service]
-ExecStart=gsr-ui
+ExecStart=gsr-ui launch-daemon
KillSignal=SIGINT
Restart=on-failure
RestartSec=5s
diff --git a/images/delete.png b/images/delete.png
new file mode 100644
index 0000000..f4ac335
--- /dev/null
+++ b/images/delete.png
Binary files differ
diff --git a/images/ps4_cross.png b/images/ps4_cross.png
new file mode 100644
index 0000000..fc14b2b
--- /dev/null
+++ b/images/ps4_cross.png
Binary files differ
diff --git a/images/ps4_triangle.png b/images/ps4_triangle.png
new file mode 100644
index 0000000..ff07fcd
--- /dev/null
+++ b/images/ps4_triangle.png
Binary files differ
diff --git a/images/screenshot.png b/images/screenshot.png
index e4e7dc7..d67acf6 100644
--- a/images/screenshot.png
+++ b/images/screenshot.png
Binary files differ
diff --git a/images/settings_extra_small.png b/images/settings_extra_small.png
new file mode 100644
index 0000000..71770fe
--- /dev/null
+++ b/images/settings_extra_small.png
Binary files differ
diff --git a/images/trash.png b/images/trash.png
new file mode 100644
index 0000000..9ae2191
--- /dev/null
+++ b/images/trash.png
Binary files differ
diff --git a/include/Config.hpp b/include/Config.hpp
index 0e8e4eb..7c2aeda 100644
--- a/include/Config.hpp
+++ b/include/Config.hpp
@@ -6,7 +6,7 @@
#include <vector>
#include <optional>
-#define GSR_CONFIG_FILE_VERSION 1
+#define GSR_CONFIG_FILE_VERSION 2
namespace gsr {
struct SupportedCaptureOptions;
@@ -30,6 +30,14 @@ namespace gsr {
std::string to_string(bool spaces = true, bool modifier_side = true) const;
};
+ struct AudioTrack {
+ std::vector<std::string> audio_inputs; // ids
+ bool application_audio_invert = false;
+
+ bool operator==(const AudioTrack &other) const;
+ bool operator!=(const AudioTrack &other) const;
+ };
+
struct RecordOptions {
std::string record_area_option = "screen";
int32_t record_area_width = 0;
@@ -37,16 +45,17 @@ namespace gsr {
int32_t video_width = 0;
int32_t video_height = 0;
int32_t fps = 60;
- int32_t video_bitrate = 15000;
- bool merge_audio_tracks = true; // Currently unused for streaming because all known streaming sites only support 1 audio track
- bool application_audio_invert = false;
+ int32_t video_bitrate = 8000;
+ bool merge_audio_tracks = true; // TODO: Remove in the future
+ bool application_audio_invert = false; // TODO: Remove in the future
bool change_video_resolution = false;
- std::vector<std::string> audio_tracks;
+ std::vector<std::string> audio_tracks; // ids, TODO: Remove in the future
+ std::vector<AudioTrack> audio_tracks_list;
std::string color_range = "limited";
std::string video_quality = "very_high";
std::string video_codec = "auto";
std::string audio_codec = "opus";
- std::string framerate_mode = "vfr";
+ std::string framerate_mode = "auto";
bool advanced_view = false;
bool overclock = false;
bool record_cursor = true;
@@ -70,6 +79,10 @@ namespace gsr {
std::string stream_key;
};
+ struct RumbleStreamConfig {
+ std::string stream_key;
+ };
+
struct CustomStreamConfig {
std::string url;
std::string container = "flv";
@@ -82,6 +95,7 @@ namespace gsr {
std::string streaming_service = "twitch";
YoutubeStreamConfig youtube;
TwitchStreamConfig twitch;
+ RumbleStreamConfig rumble;
CustomStreamConfig custom;
ConfigHotkey start_stop_hotkey;
};
@@ -91,6 +105,7 @@ namespace gsr {
bool save_video_in_game_folder = false;
bool show_recording_started_notifications = true;
bool show_video_saved_notifications = true;
+ bool show_video_paused_notifications = true;
std::string save_directory;
std::string container = "mp4";
ConfigHotkey start_stop_hotkey;
@@ -108,8 +123,11 @@ namespace gsr {
std::string save_directory;
std::string container = "mp4";
int32_t replay_time = 60;
+ std::string replay_storage = "ram";
ConfigHotkey start_stop_hotkey;
ConfigHotkey save_hotkey;
+ ConfigHotkey save_1_min_hotkey;
+ ConfigHotkey save_10_min_hotkey;
};
struct ScreenshotConfig {
diff --git a/include/CursorTracker/CursorTracker.hpp b/include/CursorTracker/CursorTracker.hpp
new file mode 100644
index 0000000..ff7374f
--- /dev/null
+++ b/include/CursorTracker/CursorTracker.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <optional>
+#include <string>
+#include <mglpp/system/vec.hpp>
+
+namespace gsr {
+ struct CursorInfo {
+ mgl::vec2i position;
+ std::string monitor_name;
+ };
+
+ class CursorTracker {
+ public:
+ CursorTracker() = default;
+ CursorTracker(const CursorTracker&) = delete;
+ CursorTracker& operator=(const CursorTracker&) = delete;
+ virtual ~CursorTracker() = default;
+
+ virtual void update() = 0;
+ virtual std::optional<CursorInfo> get_latest_cursor_info() = 0;
+ };
+} \ No newline at end of file
diff --git a/include/CursorTracker/CursorTrackerWayland.hpp b/include/CursorTracker/CursorTrackerWayland.hpp
new file mode 100644
index 0000000..1eeee83
--- /dev/null
+++ b/include/CursorTracker/CursorTrackerWayland.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "CursorTracker.hpp"
+#include <stdint.h>
+#include <vector>
+
+struct wl_display;
+struct wl_registry;
+struct wl_output;
+struct zxdg_output_manager_v1;
+struct zxdg_output_v1;
+
+namespace gsr {
+ struct WaylandOutput {
+ uint32_t wl_name;
+ struct wl_output *output;
+ struct zxdg_output_v1 *xdg_output;
+ mgl::vec2i pos;
+ mgl::vec2i size;
+ int32_t transform;
+ std::string name;
+ };
+
+ class CursorTrackerWayland : public CursorTracker {
+ public:
+ CursorTrackerWayland(const char *card_path);
+ CursorTrackerWayland(const CursorTrackerWayland&) = delete;
+ CursorTrackerWayland& operator=(const CursorTrackerWayland&) = delete;
+ ~CursorTrackerWayland();
+
+ void update() override;
+ std::optional<CursorInfo> get_latest_cursor_info() override;
+
+ std::vector<WaylandOutput> monitors;
+ struct zxdg_output_manager_v1 *xdg_output_manager = nullptr;
+ private:
+ void set_monitor_outputs_from_xdg_output(struct wl_display *dpy);
+ private:
+ int drm_fd = -1;
+ mgl::vec2i latest_cursor_position; // Position of the cursor within the monitor
+ int latest_crtc_id = -1;
+ };
+} \ No newline at end of file
diff --git a/include/CursorTracker/CursorTrackerX11.hpp b/include/CursorTracker/CursorTrackerX11.hpp
new file mode 100644
index 0000000..66618c4
--- /dev/null
+++ b/include/CursorTracker/CursorTrackerX11.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "CursorTracker.hpp"
+
+typedef struct _XDisplay Display;
+
+namespace gsr {
+ class CursorTrackerX11 : public CursorTracker {
+ public:
+ CursorTrackerX11(Display *dpy);
+ CursorTrackerX11(const CursorTrackerX11&) = delete;
+ CursorTrackerX11& operator=(const CursorTrackerX11&) = delete;
+ ~CursorTrackerX11() = default;
+
+ void update() override {}
+ std::optional<CursorInfo> get_latest_cursor_info() override;
+ private:
+ Display *dpy = nullptr;
+ };
+} \ No newline at end of file
diff --git a/include/GlobalHotkeys.hpp b/include/GlobalHotkeys/GlobalHotkeys.hpp
index 2927fa7..2927fa7 100644
--- a/include/GlobalHotkeys.hpp
+++ b/include/GlobalHotkeys/GlobalHotkeys.hpp
diff --git a/include/GlobalHotkeysJoystick.hpp b/include/GlobalHotkeys/GlobalHotkeysJoystick.hpp
index 30a7689..0177d29 100644
--- a/include/GlobalHotkeysJoystick.hpp
+++ b/include/GlobalHotkeys/GlobalHotkeysJoystick.hpp
@@ -1,7 +1,7 @@
#pragma once
#include "GlobalHotkeys.hpp"
-#include "Hotplug.hpp"
+#include "../Hotplug.hpp"
#include <unordered_map>
#include <thread>
#include <poll.h>
@@ -21,6 +21,8 @@ namespace gsr {
bool start();
// Currently valid ids:
// save_replay
+ // save_1_min_replay
+ // save_10_min_replay
// take_screenshot
// toggle_record
// toggle_replay
@@ -54,8 +56,12 @@ namespace gsr {
bool down_pressed = false;
bool left_pressed = false;
bool right_pressed = false;
+ bool l3_button_pressed = false;
+ bool r3_button_pressed = false;
bool save_replay = false;
+ bool save_1_min_replay = false;
+ bool save_10_min_replay = false;
bool take_screenshot = false;
bool toggle_record = false;
bool toggle_replay = false;
diff --git a/include/GlobalHotkeysLinux.hpp b/include/GlobalHotkeys/GlobalHotkeysLinux.hpp
index c9428de..959d095 100644
--- a/include/GlobalHotkeysLinux.hpp
+++ b/include/GlobalHotkeys/GlobalHotkeysLinux.hpp
@@ -22,6 +22,8 @@ namespace gsr {
void unbind_all_keys() override;
void poll_events() override;
private:
+ void close_fds();
+ private:
pid_t process_id = 0;
int read_pipes[2];
int write_pipes[2];
diff --git a/include/GlobalHotkeysX11.hpp b/include/GlobalHotkeys/GlobalHotkeysX11.hpp
index 610399a..610399a 100644
--- a/include/GlobalHotkeysX11.hpp
+++ b/include/GlobalHotkeys/GlobalHotkeysX11.hpp
diff --git a/include/Overlay.hpp b/include/Overlay.hpp
index d7b8af1..3de89c2 100644
--- a/include/Overlay.hpp
+++ b/include/Overlay.hpp
@@ -6,9 +6,11 @@
#include "Config.hpp"
#include "window_texture.h"
#include "WindowUtils.hpp"
-#include "GlobalHotkeysJoystick.hpp"
+#include "GlobalHotkeys/GlobalHotkeysJoystick.hpp"
#include "AudioPlayer.hpp"
#include "RegionSelector.hpp"
+#include "WindowSelector.hpp"
+#include "CursorTracker/CursorTracker.hpp"
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
@@ -58,12 +60,15 @@ namespace gsr {
void toggle_stream();
void toggle_replay();
void save_replay();
+ void save_replay_1_min();
+ void save_replay_10_min();
void take_screenshot();
void take_screenshot_region();
- void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type);
+ void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target = nullptr);
bool is_open() const;
bool should_exit(std::string &reason) const;
void exit();
+ void go_back_to_old_ui();
const Config& get_config() const;
@@ -85,7 +90,8 @@ namespace gsr {
void update_notification_process_status();
void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type);
void on_replay_saved(const char *replay_saved_filepath);
- void update_gsr_replay_save();
+ void process_gsr_output();
+ void on_gsr_process_error(int exit_code, NotificationType notification_type);
void update_gsr_process_status();
void update_gsr_screenshot_process_status();
@@ -94,7 +100,7 @@ namespace gsr {
void update_power_supply_status();
void update_system_startup_status();
- void on_stop_recording(int exit_code);
+ void on_stop_recording(int exit_code, const std::string &video_filepath);
void update_ui_recording_paused();
void update_ui_recording_unpaused();
@@ -108,13 +114,18 @@ namespace gsr {
void update_ui_replay_started();
void update_ui_replay_stopped();
+ void prepare_gsr_output_for_reading();
void on_press_save_replay();
- bool on_press_start_replay(bool disable_notification, bool finished_region_selection);
- void on_press_start_record(bool finished_region_selection);
- void on_press_start_stream(bool finished_region_selection);
- void on_press_take_screenshot(bool finished_region_selection, bool force_region_capture);
+ void on_press_save_replay_1_min_replay();
+ void on_press_save_replay_10_min_replay();
+ bool on_press_start_replay(bool disable_notification, bool finished_selection);
+ void on_press_start_record(bool finished_selection);
+ void on_press_start_stream(bool finished_selection);
+ void on_press_take_screenshot(bool finished_selection, bool force_region_capture);
bool update_compositor_texture(const Monitor &monitor);
+ std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);
+
void force_window_on_top();
private:
using KeyBindingCallback = std::function<void()>;
@@ -200,10 +211,23 @@ namespace gsr {
bool replay_save_show_notification = false;
ReplayStartupMode replay_startup_mode = ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
bool try_replay_startup = true;
+ bool replay_recording = false;
+ int replay_save_duration_min = 0;
AudioPlayer audio_player;
+
RegionSelector region_selector;
bool start_region_capture = false;
std::function<void()> on_region_selected;
+
+ WindowSelector window_selector;
+ bool start_window_capture = false;
+ std::function<void()> on_window_selected;
+
+ std::string recording_capture_target;
+ std::string screenshot_capture_target;
+
+ std::unique_ptr<CursorTracker> cursor_tracker;
+ mgl::Clock cursor_tracker_update_clock;
};
} \ No newline at end of file
diff --git a/include/RegionSelector.hpp b/include/RegionSelector.hpp
index 0465302..ef0bc0e 100644
--- a/include/RegionSelector.hpp
+++ b/include/RegionSelector.hpp
@@ -26,7 +26,6 @@ namespace gsr {
bool failed() const;
bool poll_events();
- bool is_selected() const;
bool take_selection();
bool take_canceled();
Region get_selection() const;
diff --git a/include/SafeVector.hpp b/include/SafeVector.hpp
index 6cb4725..63125ad 100644
--- a/include/SafeVector.hpp
+++ b/include/SafeVector.hpp
@@ -1,8 +1,15 @@
#pragma once
#include <vector>
-#include <memory>
#include <functional>
+#include <assert.h>
+#include "gui/Widget.hpp"
+
+template <typename T>
+struct SafeVectorItem {
+ T item;
+ bool alive = false;
+};
// A vector that can be modified while iterating
template <typename T>
@@ -10,64 +17,84 @@ class SafeVector {
public:
using PointerType = typename std::pointer_traits<T>::element_type*;
+ SafeVector() = default;
+ SafeVector(const SafeVector&) = delete;
+ SafeVector& operator=(const SafeVector&) = delete;
+ ~SafeVector() {
+ clear();
+ }
+
void push_back(T item) {
- data.push_back(std::move(item));
+ data.push_back({std::move(item), true});
+ ++num_items_alive;
}
// Safe to call when vector is empty
// TODO: Make this iterator safe
void pop_back() {
- if(!data.empty())
+ if(!data.empty()) {
+ gsr::add_widget_to_remove(std::move(data.back().item));
data.pop_back();
+ --num_items_alive;
+ }
}
// Might not remove the data immediately if inside for_each loop.
// In that case the item is removed at the end of the loop.
void remove(PointerType item_to_remove) {
- if(for_each_depth == 0)
+ if(for_each_depth == 0) {
remove_item(item_to_remove);
- else
- remove_queue.push_back(item_to_remove);
+ return;
+ }
+
+ SafeVectorItem<T> *item = get_item(item_to_remove);
+ if(item && item->alive) {
+ item->alive = false;
+ --num_items_alive;
+ has_items_to_remove = true;
+ }
}
// Safe to call when vector is empty, in which case it returns nullptr
T* back() {
- if(data.empty())
- return nullptr;
- else
- return &data.back();
+ for(auto it = data.rbegin(), end = data.rend(); it != end; ++it) {
+ if(it->alive)
+ return &it->item;
+ }
+ return nullptr;
}
// TODO: Make this iterator safe
void clear() {
+ for(auto &item : data) {
+ gsr::add_widget_to_remove(std::move(item.item));
+ }
data.clear();
- remove_queue.clear();
+ num_items_alive = 0;
}
// Return true from |callback| to continue. This function returns false if |callback| returned false
- bool for_each(std::function<bool(T &t)> callback) {
+ bool for_each(std::function<bool(T &t)> callback, bool include_dead = false) {
bool result = true;
++for_each_depth;
for(size_t i = 0; i < data.size(); ++i) {
- result = callback(data[i]);
- if(!result)
- break;
+ if(data[i].alive || include_dead) {
+ result = callback(data[i].item);
+ if(!result)
+ break;
+ }
}
--for_each_depth;
- if(for_each_depth == 0) {
- for(PointerType item_to_remove : remove_queue) {
- remove_item(item_to_remove);
- }
- remove_queue.clear();
- }
+ if(for_each_depth == 0)
+ remove_dead_items();
return result;
}
// Return true from |callback| to continue. This function returns false if |callback| returned false
- bool for_each_reverse(std::function<bool(T &t)> callback) {
+ bool for_each_reverse(std::function<bool(T &t)> callback, bool include_dead = false) {
bool result = true;
++for_each_depth;
@@ -80,50 +107,84 @@ public:
if(i < 0)
break;
- result = callback(data[i]);
- if(!result)
- break;
+ if(data[i].alive || include_dead) {
+ result = callback(data[i].item);
+ if(!result)
+ break;
+ }
--i;
}
--for_each_depth;
- if(for_each_depth == 0) {
- for(PointerType item_to_remove : remove_queue) {
- remove_item(item_to_remove);
- }
- remove_queue.clear();
- }
+ if(for_each_depth == 0)
+ remove_dead_items();
return result;
}
T& operator[](size_t index) {
- return data[index];
+ assert(index < data.size());
+ return data[index].item;
}
const T& operator[](size_t index) const {
- return data[index];
+ assert(index < data.size());
+ return data[index].item;
}
size_t size() const {
- return data.size();
+ return (size_t)num_items_alive;
}
bool empty() const {
- return data.empty();
+ return num_items_alive == 0;
+ }
+
+ void replace_item(PointerType item_to_replace, T new_item) {
+ SafeVectorItem<T> *item = get_item(item_to_replace);
+ if(item->alive) {
+ gsr::add_widget_to_remove(std::move(item->item));
+ item->item = std::move(new_item);
+ }
}
private:
void remove_item(PointerType item_to_remove) {
for(auto it = data.begin(), end = data.end(); it != end; ++it) {
- if(&*(*it) == item_to_remove) {
+ if(&*(it->item) == item_to_remove) {
+ gsr::add_widget_to_remove(std::move(it->item));
data.erase(it);
+ --num_items_alive;
return;
}
}
}
+
+ SafeVectorItem<T>* get_item(PointerType item_to_remove) {
+ for(auto &item : data) {
+ if(&*(item.item) == item_to_remove)
+ return &item;
+ }
+ return nullptr;
+ }
+
+ void remove_dead_items() {
+ if(!has_items_to_remove)
+ return;
+
+ for(auto it = data.begin(); it != data.end();) {
+ if(it->alive) {
+ ++it;
+ } else {
+ gsr::add_widget_to_remove(std::move(it->item));
+ it = data.erase(it);
+ }
+ }
+ has_items_to_remove = false;
+ }
private:
- std::vector<T> data;
- std::vector<PointerType> remove_queue;
+ std::vector<SafeVectorItem<T>> data;
int for_each_depth = 0;
+ int num_items_alive = 0;
+ bool has_items_to_remove = false;
}; \ No newline at end of file
diff --git a/include/Theme.hpp b/include/Theme.hpp
index 4305072..1390182 100644
--- a/include/Theme.hpp
+++ b/include/Theme.hpp
@@ -28,6 +28,7 @@ namespace gsr {
mgl::Texture combobox_arrow_texture;
mgl::Texture settings_texture;
mgl::Texture settings_small_texture;
+ mgl::Texture settings_extra_small_texture;
mgl::Texture folder_texture;
mgl::Texture up_arrow_texture;
mgl::Texture replay_button_texture;
@@ -42,6 +43,7 @@ namespace gsr {
mgl::Texture pause_texture;
mgl::Texture save_texture;
mgl::Texture screenshot_texture;
+ mgl::Texture trash_texture;
mgl::Texture ps4_home_texture;
mgl::Texture ps4_options_texture;
@@ -49,6 +51,8 @@ namespace gsr {
mgl::Texture ps4_dpad_down_texture;
mgl::Texture ps4_dpad_left_texture;
mgl::Texture ps4_dpad_right_texture;
+ mgl::Texture ps4_cross_texture;
+ mgl::Texture ps4_triangle_texture;
double double_click_timeout_seconds = 0.4;
diff --git a/include/Utils.hpp b/include/Utils.hpp
index 19700df..3d3c029 100644
--- a/include/Utils.hpp
+++ b/include/Utils.hpp
@@ -14,6 +14,9 @@ namespace gsr {
using StringSplitCallback = std::function<bool(std::string_view line)>;
void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func);
+ bool starts_with(std::string_view str, const char *substr);
+ bool ends_with(std::string_view str, const char *substr);
+ std::string strip(const std::string &str);
std::string get_home_dir();
std::string get_config_dir();
diff --git a/include/WindowSelector.hpp b/include/WindowSelector.hpp
new file mode 100644
index 0000000..ab4a85d
--- /dev/null
+++ b/include/WindowSelector.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <X11/Xlib.h>
+
+#include <mglpp/graphics/Color.hpp>
+
+namespace gsr {
+ class WindowSelector {
+ public:
+ WindowSelector();
+ WindowSelector(const WindowSelector&) = delete;
+ WindowSelector& operator=(const WindowSelector&) = delete;
+ ~WindowSelector();
+
+ bool start(mgl::Color border_color);
+ void stop();
+ bool is_started() const;
+
+ bool failed() const;
+ bool poll_events();
+ bool take_selection();
+ bool take_canceled();
+ Window get_selection() const;
+ private:
+ Display *dpy = nullptr;
+ Cursor crosshair_cursor = None;
+ Colormap border_window_colormap = None;
+ Window border_window = None;
+ Window selected_window = None;
+ bool selected = false;
+ bool canceled = false;
+ };
+} \ No newline at end of file
diff --git a/include/WindowUtils.hpp b/include/WindowUtils.hpp
index e31eeb2..5c4d39a 100644
--- a/include/WindowUtils.hpp
+++ b/include/WindowUtils.hpp
@@ -15,6 +15,7 @@ namespace gsr {
struct Monitor {
mgl::vec2i position;
mgl::vec2i size;
+ std::string name;
};
std::optional<std::string> get_window_title(Display *dpy, Window window);
@@ -23,6 +24,7 @@ namespace gsr {
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window);
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window);
void set_window_size_not_resizable(Display *dpy, Window window, int width, int height);
+ Window window_get_target_window_child(Display *display, Window window);
mgl::vec2i get_cursor_position(Display *dpy, Window *window);
mgl::vec2i create_window_get_center_position(Display *display);
std::string get_window_manager_name(Display *display);
diff --git a/include/gui/DropdownButton.hpp b/include/gui/DropdownButton.hpp
index 486e811..f613d86 100644
--- a/include/gui/DropdownButton.hpp
+++ b/include/gui/DropdownButton.hpp
@@ -21,6 +21,7 @@ namespace gsr {
void set_item_label(const std::string &id, const std::string &new_label);
void set_item_icon(const std::string &id, mgl::Texture *texture);
void set_item_description(const std::string &id, const std::string &new_description);
+ void set_item_enabled(const std::string &id, bool enabled);
void set_description(std::string description_text);
void set_activated(bool activated);
@@ -36,6 +37,7 @@ namespace gsr {
mgl::Text description_text;
mgl::Texture *icon_texture = nullptr;
std::string id;
+ bool enabled = true;
};
std::vector<Item> items;
diff --git a/include/gui/GlobalSettingsPage.hpp b/include/gui/GlobalSettingsPage.hpp
index 5df5b9c..d96397d 100644
--- a/include/gui/GlobalSettingsPage.hpp
+++ b/include/gui/GlobalSettingsPage.hpp
@@ -22,6 +22,8 @@ namespace gsr {
NONE,
REPLAY_START_STOP,
REPLAY_SAVE,
+ REPLAY_SAVE_1_MIN,
+ REPLAY_SAVE_10_MIN,
RECORD_START_STOP,
RECORD_PAUSE_UNPAUSE,
STREAM_START_STOP,
@@ -56,6 +58,7 @@ namespace gsr {
std::unique_ptr<RadioButton> create_enable_joystick_hotkeys_button();
std::unique_ptr<List> create_show_hide_hotkey_options();
std::unique_ptr<List> create_replay_hotkey_options();
+ std::unique_ptr<List> create_replay_partial_save_hotkey_options();
std::unique_ptr<List> create_record_hotkey_options();
std::unique_ptr<List> create_stream_hotkey_options();
std::unique_ptr<List> create_screenshot_hotkey_options();
@@ -89,6 +92,8 @@ namespace gsr {
Button *turn_replay_on_off_button_ptr = nullptr;
Button *save_replay_button_ptr = nullptr;
+ Button *save_replay_1_min_button_ptr = nullptr;
+ Button *save_replay_10_min_button_ptr = nullptr;
Button *start_stop_recording_button_ptr = nullptr;
Button *pause_unpause_recording_button_ptr = nullptr;
Button *start_stop_streaming_button_ptr = nullptr;
diff --git a/include/gui/List.hpp b/include/gui/List.hpp
index 72c5353..f79d165 100644
--- a/include/gui/List.hpp
+++ b/include/gui/List.hpp
@@ -21,19 +21,20 @@ namespace gsr {
List(Orientation orientation, Alignment content_alignment = Alignment::START);
List(const List&) = delete;
List& operator=(const List&) = delete;
+ virtual ~List() override;
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
void draw(mgl::Window &window, mgl::vec2f offset) override;
- //void remove_child_widget(Widget *widget) override;
-
void add_widget(std::unique_ptr<Widget> widget);
void remove_widget(Widget *widget);
+ void replace_widget(Widget *widget, std::unique_ptr<Widget> new_widget);
void clear();
// Return true from |callback| to continue
void for_each_child_widget(std::function<bool(std::unique_ptr<Widget> &widget)> callback);
// Returns nullptr if index is invalid
Widget* get_child_widget_by_index(size_t index) const;
+ size_t get_num_children() const;
void set_spacing(float spacing);
diff --git a/include/gui/Page.hpp b/include/gui/Page.hpp
index 0d8536a..00a53c6 100644
--- a/include/gui/Page.hpp
+++ b/include/gui/Page.hpp
@@ -10,13 +10,11 @@ namespace gsr {
Page() = default;
Page(const Page&) = delete;
Page& operator=(const Page&) = delete;
- virtual ~Page() = default;
+ virtual ~Page() override;
virtual void on_navigate_to_page() {}
virtual void on_navigate_away_from_page() {}
- //void remove_child_widget(Widget *widget) override;
-
virtual void add_widget(std::unique_ptr<Widget> widget);
protected:
SafeVector<std::unique_ptr<Widget>> widgets;
diff --git a/include/gui/RadioButton.hpp b/include/gui/RadioButton.hpp
index 16d638e..e319aa0 100644
--- a/include/gui/RadioButton.hpp
+++ b/include/gui/RadioButton.hpp
@@ -23,7 +23,8 @@ namespace gsr {
void add_item(const std::string &text, const std::string &id);
void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true);
- const std::string get_selected_id() const;
+ const std::string& get_selected_id() const;
+ const std::string& get_selected_text() const;
mgl::vec2f get_size() override;
diff --git a/include/gui/ScreenshotSettingsPage.hpp b/include/gui/ScreenshotSettingsPage.hpp
index 1cfbf00..db66d66 100644
--- a/include/gui/ScreenshotSettingsPage.hpp
+++ b/include/gui/ScreenshotSettingsPage.hpp
@@ -26,7 +26,6 @@ namespace gsr {
private:
std::unique_ptr<ComboBox> create_record_area_box();
std::unique_ptr<Widget> create_record_area();
- std::unique_ptr<List> create_select_window();
std::unique_ptr<Entry> create_image_width_entry();
std::unique_ptr<Entry> create_image_height_entry();
std::unique_ptr<List> create_image_resolution();
@@ -56,7 +55,6 @@ namespace gsr {
GsrPage *content_page_ptr = nullptr;
ScrollablePage *settings_scrollable_page_ptr = nullptr;
- List *select_window_list_ptr = nullptr;
List *image_resolution_list_ptr = nullptr;
List *restore_portal_session_list_ptr = nullptr;
List *color_range_list_ptr = nullptr;
diff --git a/include/gui/ScrollablePage.hpp b/include/gui/ScrollablePage.hpp
index 452d0e9..54ec2cb 100644
--- a/include/gui/ScrollablePage.hpp
+++ b/include/gui/ScrollablePage.hpp
@@ -12,6 +12,7 @@ namespace gsr {
ScrollablePage(mgl::vec2f size);
ScrollablePage(const ScrollablePage&) = delete;
ScrollablePage& operator=(const ScrollablePage&) = delete;
+ virtual ~ScrollablePage() override;
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
void draw(mgl::Window &window, mgl::vec2f offset) override;
diff --git a/include/gui/SettingsPage.hpp b/include/gui/SettingsPage.hpp
index b9f5cde..1810de5 100644
--- a/include/gui/SettingsPage.hpp
+++ b/include/gui/SettingsPage.hpp
@@ -18,6 +18,12 @@ namespace gsr {
class ScrollablePage;
class Label;
class LineSeparator;
+ class Subsection;
+
+ enum class AudioDeviceType {
+ OUTPUT,
+ INPUT
+ };
class SettingsPage : public StaticPage {
public:
@@ -40,7 +46,6 @@ namespace gsr {
std::unique_ptr<RadioButton> create_view_radio_button();
std::unique_ptr<ComboBox> create_record_area_box();
std::unique_ptr<Widget> create_record_area();
- std::unique_ptr<List> create_select_window();
std::unique_ptr<Entry> create_area_width_entry();
std::unique_ptr<Entry> create_area_height_entry();
std::unique_ptr<List> create_area_size();
@@ -53,20 +58,22 @@ namespace gsr {
std::unique_ptr<List> create_restore_portal_session_section();
std::unique_ptr<Widget> create_change_video_resolution_section();
std::unique_ptr<Widget> create_capture_target_section();
- std::unique_ptr<ComboBox> create_audio_device_selection_combobox();
- std::unique_ptr<Button> create_remove_audio_device_button(List *audio_device_list_ptr);
- std::unique_ptr<List> create_audio_device();
- std::unique_ptr<Button> create_add_audio_device_button();
- std::unique_ptr<ComboBox> create_application_audio_selection_combobox();
- std::unique_ptr<List> create_application_audio();
- std::unique_ptr<List> create_custom_application_audio();
- std::unique_ptr<Button> create_add_application_audio_button();
- std::unique_ptr<Button> create_add_custom_application_audio_button();
- std::unique_ptr<List> create_add_audio_buttons();
- std::unique_ptr<List> create_audio_track_track_section();
- std::unique_ptr<CheckBox> create_split_audio_checkbox();
+ std::unique_ptr<ComboBox> create_audio_device_selection_combobox(AudioDeviceType device_type);
+ std::unique_ptr<Button> create_remove_audio_device_button(List *audio_input_list_ptr, List *audio_device_list_ptr);
+ std::unique_ptr<List> create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr);
+ std::unique_ptr<Button> create_add_audio_track_button();
+ std::unique_ptr<Button> create_add_audio_output_device_button(List *audio_input_list_ptr);
+ std::unique_ptr<Button> create_add_audio_input_device_button(List *audio_input_list_ptr);
+ std::unique_ptr<ComboBox> create_application_audio_selection_combobox(List *application_audio_row);
+ std::unique_ptr<List> create_application_audio(List *audio_input_list_ptr);
+ std::unique_ptr<List> create_custom_application_audio(List *audio_input_list_ptr);
+ std::unique_ptr<Button> create_add_application_audio_button(List *audio_input_list_ptr);
+ std::unique_ptr<List> create_add_audio_buttons(List *audio_input_list_ptr);
+ std::unique_ptr<List> create_audio_input_section();
std::unique_ptr<CheckBox> create_application_audio_invert_checkbox();
- std::unique_ptr<Widget> create_audio_track_section();
+ std::unique_ptr<List> create_audio_track_title_and_remove(Subsection *audio_track_subsection, const char *title);
+ std::unique_ptr<Subsection> create_audio_track_section(Widget *parent_widget);
+ std::unique_ptr<List> create_audio_track_section_list();
std::unique_ptr<Widget> create_audio_section();
std::unique_ptr<List> create_video_quality_box();
std::unique_ptr<List> create_video_bitrate_entry();
@@ -95,11 +102,12 @@ namespace gsr {
std::unique_ptr<List> create_container_section();
std::unique_ptr<List> create_replay_time_entry();
std::unique_ptr<List> create_replay_time();
+ std::unique_ptr<List> create_replay_storage();
std::unique_ptr<RadioButton> create_start_replay_automatically();
std::unique_ptr<CheckBox> create_save_replay_in_game_folder();
std::unique_ptr<CheckBox> create_restart_replay_on_save();
std::unique_ptr<Label> create_estimated_replay_file_size();
- void update_estimated_replay_file_size();
+ void update_estimated_replay_file_size(const std::string &replay_storage_type);
void update_replay_time_text();
std::unique_ptr<CheckBox> create_save_recording_in_game_folder();
std::unique_ptr<Label> create_estimated_record_file_size();
@@ -125,6 +133,8 @@ namespace gsr {
void save_replay();
void save_record();
void save_stream();
+
+ void view_changed(bool advanced_view, Subsection *notifications_subsection_ptr);
private:
Type type;
Config &config;
@@ -136,7 +146,6 @@ namespace gsr {
GsrPage *content_page_ptr = nullptr;
ScrollablePage *settings_scrollable_page_ptr = nullptr;
List *settings_list_ptr = nullptr;
- List *select_window_list_ptr = nullptr;
List *area_size_list_ptr = nullptr;
List *video_resolution_list_ptr = nullptr;
List *restore_portal_session_list_ptr = nullptr;
@@ -152,11 +161,6 @@ namespace gsr {
Entry *framerate_entry_ptr = nullptr;
Entry *video_bitrate_entry_ptr = nullptr;
List *video_bitrate_list_ptr = nullptr;
- List *audio_track_list_ptr = nullptr;
- Button *add_application_audio_button_ptr = nullptr;
- Button *add_custom_application_audio_button_ptr = nullptr;
- CheckBox *split_audio_checkbox_ptr = nullptr;
- CheckBox *application_audio_invert_checkbox_ptr = nullptr;
CheckBox *change_video_resolution_checkbox_ptr = nullptr;
ComboBox *color_range_box_ptr = nullptr;
ComboBox *video_quality_box_ptr = nullptr;
@@ -180,15 +184,20 @@ namespace gsr {
CheckBox *save_recording_in_game_folder_ptr = nullptr;
CheckBox *show_recording_started_notification_checkbox_ptr = nullptr;
CheckBox *show_video_saved_notification_checkbox_ptr = nullptr;
+ CheckBox *show_video_paused_notification_checkbox_ptr = nullptr;
CheckBox *show_streaming_started_notification_checkbox_ptr = nullptr;
CheckBox *show_streaming_stopped_notification_checkbox_ptr = nullptr;
Button *save_directory_button_ptr = nullptr;
Entry *twitch_stream_key_entry_ptr = nullptr;
Entry *youtube_stream_key_entry_ptr = nullptr;
+ Entry *rumble_stream_key_entry_ptr = nullptr;
Entry *stream_url_entry_ptr = nullptr;
Entry *replay_time_entry_ptr = nullptr;
+ RadioButton *replay_storage_button_ptr = nullptr;
Label *replay_time_label_ptr = nullptr;
RadioButton *turn_on_replay_automatically_mode_ptr = nullptr;
+ Subsection *audio_section_ptr = nullptr;
+ List *audio_track_section_list_ptr = nullptr;
PageStack *page_stack = nullptr;
};
diff --git a/include/gui/Subsection.hpp b/include/gui/Subsection.hpp
index 4da6baf..88953d2 100644
--- a/include/gui/Subsection.hpp
+++ b/include/gui/Subsection.hpp
@@ -11,15 +11,20 @@ namespace gsr {
Subsection(const char *title, std::unique_ptr<Widget> inner_widget, mgl::vec2f size);
Subsection(const Subsection&) = delete;
Subsection& operator=(const Subsection&) = delete;
+ virtual ~Subsection() override;
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
void draw(mgl::Window &window, mgl::vec2f offset) override;
mgl::vec2f get_size() override;
mgl::vec2f get_inner_size() override;
+
+ Widget* get_inner_widget();
+ void set_bg_color(mgl::Color color);
private:
Label label;
std::unique_ptr<Widget> inner_widget;
mgl::vec2f size;
+ mgl::Color bg_color{25, 30, 34};
};
} \ No newline at end of file
diff --git a/include/gui/Widget.hpp b/include/gui/Widget.hpp
index 57424cd..131e756 100644
--- a/include/gui/Widget.hpp
+++ b/include/gui/Widget.hpp
@@ -1,6 +1,7 @@
#pragma once
#include <mglpp/system/vec.hpp>
+#include <memory>
namespace mgl {
class Event;
@@ -31,8 +32,6 @@ namespace gsr {
virtual void draw(mgl::Window &window, mgl::vec2f offset) = 0;
virtual void set_position(mgl::vec2f position);
- //virtual void remove_child_widget(Widget *widget) { (void)widget; }
-
virtual mgl::vec2f get_position() const;
virtual mgl::vec2f get_size() = 0;
// This can be different from get_size, for example with ScrollablePage this excludes the margins
@@ -61,4 +60,7 @@ namespace gsr {
bool visible = true;
};
+
+ void add_widget_to_remove(std::unique_ptr<Widget> widget);
+ void remove_widgets_to_be_removed();
} \ No newline at end of file
diff --git a/meson.build b/meson.build
index 3b19092..002c8b1 100644
--- a/meson.build
+++ b/meson.build
@@ -1,4 +1,4 @@
-project('gsr-ui', ['c', 'cpp'], version : '1.3.0', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
+project('gsr-ui', ['c', 'cpp'], version : '1.6.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -32,22 +32,28 @@ src = [
'src/gui/GlobalSettingsPage.cpp',
'src/gui/GsrPage.cpp',
'src/gui/Subsection.cpp',
+ 'src/GlobalHotkeys/GlobalHotkeysX11.cpp',
+ 'src/GlobalHotkeys/GlobalHotkeysLinux.cpp',
+ 'src/GlobalHotkeys/GlobalHotkeysJoystick.cpp',
+ 'src/CursorTracker/CursorTrackerX11.cpp',
+ 'src/CursorTracker/CursorTrackerWayland.cpp',
'src/Utils.cpp',
'src/WindowUtils.cpp',
'src/RegionSelector.cpp',
+ 'src/WindowSelector.cpp',
'src/Config.cpp',
'src/GsrInfo.cpp',
'src/Process.cpp',
'src/Overlay.cpp',
- 'src/GlobalHotkeysX11.cpp',
- 'src/GlobalHotkeysLinux.cpp',
- 'src/GlobalHotkeysJoystick.cpp',
'src/AudioPlayer.cpp',
'src/Hotplug.cpp',
'src/Rpc.cpp',
'src/main.cpp',
]
+subdir('protocol')
+src += protocol_src
+
mglpp_proj = subproject('mglpp')
mglpp_dep = mglpp_proj.get_variable('mglpp_dep')
@@ -56,7 +62,7 @@ datadir = get_option('datadir')
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
-add_project_arguments('-DGSR_FLATPAK_VERSION="5.2.0"', language: ['c', 'cpp'])
+add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.3"', language: ['c', 'cpp'])
executable(
meson.project_name(),
@@ -70,7 +76,10 @@ executable(
dependency('xext'),
dependency('xi'),
dependency('xcursor'),
+ dependency('xrandr'),
dependency('libpulse-simple'),
+ dependency('libdrm'),
+ dependency('wayland-client'),
],
cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"',
)
diff --git a/project.conf b/project.conf
index 03308e6..54f9172 100644
--- a/project.conf
+++ b/project.conf
@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
-version = "1.3.0"
+version = "1.6.7"
platforms = ["posix"]
[lang.cpp]
@@ -16,4 +16,7 @@ xfixes = ">=0"
xext = ">=0"
xi = ">=0"
xcursor = ">=1"
+xrandr = ">=0.5"
libpulse-simple = ">=0"
+libdrm = ">=2"
+wayland-client = ">=1"
diff --git a/protocol/meson.build b/protocol/meson.build
new file mode 100644
index 0000000..bbdccba
--- /dev/null
+++ b/protocol/meson.build
@@ -0,0 +1,25 @@
+wayland_scanner = dependency('wayland-scanner', native: true)
+wayland_scanner_path = wayland_scanner.get_variable(pkgconfig: 'wayland_scanner')
+wayland_scanner_prog = find_program(wayland_scanner_path, native: true)
+
+wayland_scanner_code = generator(
+ wayland_scanner_prog,
+ output: '@BASENAME@-protocol.c',
+ arguments: ['private-code', '@INPUT@', '@OUTPUT@'],
+)
+
+wayland_scanner_client = generator(
+ wayland_scanner_prog,
+ output: '@BASENAME@-client-protocol.h',
+ arguments: ['client-header', '@INPUT@', '@OUTPUT@'],
+)
+
+protocols = [
+ 'xdg-output-unstable-v1.xml',
+]
+
+protocol_src = []
+foreach xml : protocols
+ protocol_src += wayland_scanner_code.process(xml)
+ protocol_src += wayland_scanner_client.process(xml)
+endforeach
diff --git a/protocol/xdg-output-unstable-v1.xml b/protocol/xdg-output-unstable-v1.xml
new file mode 100644
index 0000000..a7306e4
--- /dev/null
+++ b/protocol/xdg-output-unstable-v1.xml
@@ -0,0 +1,222 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="xdg_output_unstable_v1">
+
+ <copyright>
+ Copyright © 2017 Red Hat Inc.
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+ </copyright>
+
+ <description summary="Protocol to describe output regions">
+ This protocol aims at describing outputs in a way which is more in line
+ with the concept of an output on desktop oriented systems.
+
+ Some information are more specific to the concept of an output for
+ a desktop oriented system and may not make sense in other applications,
+ such as IVI systems for example.
+
+ Typically, the global compositor space on a desktop system is made of
+ a contiguous or overlapping set of rectangular regions.
+
+ The logical_position and logical_size events defined in this protocol
+ might provide information identical to their counterparts already
+ available from wl_output, in which case the information provided by this
+ protocol should be preferred to their equivalent in wl_output. The goal is
+ to move the desktop specific concepts (such as output location within the
+ global compositor space, etc.) out of the core wl_output protocol.
+
+ Warning! The protocol described in this file is experimental and
+ backward incompatible changes may be made. Backward compatible
+ changes may be added together with the corresponding interface
+ version bump.
+ Backward incompatible changes are done by bumping the version
+ number in the protocol and interface names and resetting the
+ interface version. Once the protocol is to be declared stable,
+ the 'z' prefix and the version number in the protocol and
+ interface names are removed and the interface version number is
+ reset.
+ </description>
+
+ <interface name="zxdg_output_manager_v1" version="3">
+ <description summary="manage xdg_output objects">
+ A global factory interface for xdg_output objects.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the xdg_output_manager object">
+ Using this request a client can tell the server that it is not
+ going to use the xdg_output_manager object anymore.
+
+ Any objects already created through this instance are not affected.
+ </description>
+ </request>
+
+ <request name="get_xdg_output">
+ <description summary="create an xdg output from a wl_output">
+ This creates a new xdg_output object for the given wl_output.
+ </description>
+ <arg name="id" type="new_id" interface="zxdg_output_v1"/>
+ <arg name="output" type="object" interface="wl_output"/>
+ </request>
+ </interface>
+
+ <interface name="zxdg_output_v1" version="3">
+ <description summary="compositor logical output region">
+ An xdg_output describes part of the compositor geometry.
+
+ This typically corresponds to a monitor that displays part of the
+ compositor space.
+
+ For objects version 3 onwards, after all xdg_output properties have been
+ sent (when the object is created and when properties are updated), a
+ wl_output.done event is sent. This allows changes to the output
+ properties to be seen as atomic, even if they happen via multiple events.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the xdg_output object">
+ Using this request a client can tell the server that it is not
+ going to use the xdg_output object anymore.
+ </description>
+ </request>
+
+ <event name="logical_position">
+ <description summary="position of the output within the global compositor space">
+ The position event describes the location of the wl_output within
+ the global compositor space.
+
+ The logical_position event is sent after creating an xdg_output
+ (see xdg_output_manager.get_xdg_output) and whenever the location
+ of the output changes within the global compositor space.
+ </description>
+ <arg name="x" type="int"
+ summary="x position within the global compositor space"/>
+ <arg name="y" type="int"
+ summary="y position within the global compositor space"/>
+ </event>
+
+ <event name="logical_size">
+ <description summary="size of the output in the global compositor space">
+ The logical_size event describes the size of the output in the
+ global compositor space.
+
+ Most regular Wayland clients should not pay attention to the
+ logical size and would rather rely on xdg_shell interfaces.
+
+ Some clients such as Xwayland, however, need this to configure
+ their surfaces in the global compositor space as the compositor
+ may apply a different scale from what is advertised by the output
+ scaling property (to achieve fractional scaling, for example).
+
+ For example, for a wl_output mode 3840×2160 and a scale factor 2:
+
+ - A compositor not scaling the monitor viewport in its compositing space
+ will advertise a logical size of 3840×2160,
+
+ - A compositor scaling the monitor viewport with scale factor 2 will
+ advertise a logical size of 1920×1080,
+
+ - A compositor scaling the monitor viewport using a fractional scale of
+ 1.5 will advertise a logical size of 2560×1440.
+
+ For example, for a wl_output mode 1920×1080 and a 90 degree rotation,
+ the compositor will advertise a logical size of 1080x1920.
+
+ The logical_size event is sent after creating an xdg_output
+ (see xdg_output_manager.get_xdg_output) and whenever the logical
+ size of the output changes, either as a result of a change in the
+ applied scale or because of a change in the corresponding output
+ mode(see wl_output.mode) or transform (see wl_output.transform).
+ </description>
+ <arg name="width" type="int"
+ summary="width in global compositor space"/>
+ <arg name="height" type="int"
+ summary="height in global compositor space"/>
+ </event>
+
+ <event name="done" deprecated-since="3">
+ <description summary="all information about the output have been sent">
+ This event is sent after all other properties of an xdg_output
+ have been sent.
+
+ This allows changes to the xdg_output properties to be seen as
+ atomic, even if they happen via multiple events.
+
+ For objects version 3 onwards, this event is deprecated. Compositors
+ are not required to send it anymore and must send wl_output.done
+ instead.
+ </description>
+ </event>
+
+ <!-- Version 2 additions -->
+
+ <event name="name" since="2">
+ <description summary="name of this output">
+ Many compositors will assign names to their outputs, show them to the
+ user, allow them to be configured by name, etc. The client may wish to
+ know this name as well to offer the user similar behaviors.
+
+ The naming convention is compositor defined, but limited to
+ alphanumeric characters and dashes (-). Each name is unique among all
+ wl_output globals, but if a wl_output global is destroyed the same name
+ may be reused later. The names will also remain consistent across
+ sessions with the same hardware and software configuration.
+
+ Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do
+ not assume that the name is a reflection of an underlying DRM
+ connector, X11 connection, etc.
+
+ The name event is sent after creating an xdg_output (see
+ xdg_output_manager.get_xdg_output). This event is only sent once per
+ xdg_output, and the name does not change over the lifetime of the
+ wl_output global.
+
+ This event is deprecated, instead clients should use wl_output.name.
+ Compositors must still support this event.
+ </description>
+ <arg name="name" type="string" summary="output name"/>
+ </event>
+
+ <event name="description" since="2">
+ <description summary="human-readable description of this output">
+ Many compositors can produce human-readable descriptions of their
+ outputs. The client may wish to know this description as well, to
+ communicate the user for various purposes.
+
+ The description is a UTF-8 string with no convention defined for its
+ contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11
+ output via :1'.
+
+ The description event is sent after creating an xdg_output (see
+ xdg_output_manager.get_xdg_output) and whenever the description
+ changes. The description is optional, and may not be sent at all.
+
+ For objects of version 2 and lower, this event is only sent once per
+ xdg_output, and the description does not change over the lifetime of
+ the wl_output global.
+
+ This event is deprecated, instead clients should use
+ wl_output.description. Compositors must still support this event.
+ </description>
+ <arg name="description" type="string" summary="output description"/>
+ </event>
+
+ </interface>
+</protocol>
diff --git a/src/Config.cpp b/src/Config.cpp
index fdb5e4a..313cd38 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -1,7 +1,7 @@
#include "../include/Config.hpp"
#include "../include/Utils.hpp"
#include "../include/GsrInfo.hpp"
-#include "../include/GlobalHotkeys.hpp"
+#include "../include/GlobalHotkeys/GlobalHotkeys.hpp"
#include <variant>
#include <limits.h>
#include <inttypes.h>
@@ -15,6 +15,8 @@
#define FORMAT_U32 "%" PRIu32
namespace gsr {
+ static const std::string_view add_audio_track_tag = "[add_audio_track]";
+
static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) {
std::vector<mgl::Keyboard::Key> result;
if(modifiers & HOTKEY_MOD_LCTRL)
@@ -82,8 +84,8 @@ namespace gsr {
modifier_str = mgl::Keyboard::key_to_string(modifier_key);
if(!modifier_side) {
- string_remove_all(modifier_str, "Left");
- string_remove_all(modifier_str, "Right");
+ string_remove_all(modifier_str, "Left ");
+ string_remove_all(modifier_str, "Right ");
}
result += modifier_str;
}
@@ -101,6 +103,14 @@ namespace gsr {
return result;
}
+ bool AudioTrack::operator==(const AudioTrack &other) const {
+ return audio_inputs == other.audio_inputs && application_audio_invert == other.application_audio_invert;
+ }
+
+ bool AudioTrack::operator!=(const AudioTrack &other) const {
+ return !operator==(other);
+ }
+
Config::Config(const SupportedCaptureOptions &capture_options) {
const std::string default_videos_save_directory = get_videos_dir();
const std::string default_pictures_save_directory = get_pictures_dir();
@@ -108,25 +118,25 @@ namespace gsr {
set_hotkeys_to_default();
streaming_config.record_options.video_quality = "custom";
- streaming_config.record_options.audio_tracks.push_back("default_output");
- streaming_config.record_options.video_bitrate = 15000;
+ streaming_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
+ streaming_config.record_options.video_bitrate = 8000;
record_config.save_directory = default_videos_save_directory;
- record_config.record_options.audio_tracks.push_back("default_output");
- record_config.record_options.video_bitrate = 45000;
+ record_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
+ record_config.record_options.video_bitrate = 40000;
replay_config.record_options.video_quality = "custom";
replay_config.save_directory = default_videos_save_directory;
- replay_config.record_options.audio_tracks.push_back("default_output");
- replay_config.record_options.video_bitrate = 45000;
+ replay_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
+ replay_config.record_options.video_bitrate = 40000;
screenshot_config.save_directory = default_pictures_save_directory;
if(!capture_options.monitors.empty()) {
- streaming_config.record_options.record_area_option = capture_options.monitors.front().name;
- record_config.record_options.record_area_option = capture_options.monitors.front().name;
- replay_config.record_options.record_area_option = capture_options.monitors.front().name;
- screenshot_config.record_area_option = capture_options.monitors.front().name;
+ streaming_config.record_options.record_area_option = "focused_monitor";
+ record_config.record_options.record_area_option = "focused_monitor";
+ replay_config.record_options.record_area_option = "focused_monitor";
+ screenshot_config.record_area_option = "focused_monitor";
}
}
@@ -138,6 +148,8 @@ namespace gsr {
replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
+ replay_config.save_1_min_hotkey = {mgl::Keyboard::F11, HOTKEY_MOD_LALT};
+ replay_config.save_10_min_hotkey = {mgl::Keyboard::F12, HOTKEY_MOD_LALT};
screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Printscreen, 0};
screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LCTRL};
@@ -152,7 +164,7 @@ namespace gsr {
return KeyValue{line.substr(0, space_index), line.substr(space_index + 1)};
}
- using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*>;
+ using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*, std::vector<AudioTrack>*>;
static std::map<std::string_view, ConfigValue> get_config_options(Config &config) {
return {
@@ -174,6 +186,7 @@ namespace gsr {
{"streaming.record_options.application_audio_invert", &config.streaming_config.record_options.application_audio_invert},
{"streaming.record_options.change_video_resolution", &config.streaming_config.record_options.change_video_resolution},
{"streaming.record_options.audio_track", &config.streaming_config.record_options.audio_tracks},
+ {"streaming.record_options.audio_track_item", &config.streaming_config.record_options.audio_tracks_list},
{"streaming.record_options.color_range", &config.streaming_config.record_options.color_range},
{"streaming.record_options.video_quality", &config.streaming_config.record_options.video_quality},
{"streaming.record_options.codec", &config.streaming_config.record_options.video_codec},
@@ -188,6 +201,7 @@ namespace gsr {
{"streaming.service", &config.streaming_config.streaming_service},
{"streaming.youtube.key", &config.streaming_config.youtube.stream_key},
{"streaming.twitch.key", &config.streaming_config.twitch.stream_key},
+ {"streaming.rumble.key", &config.streaming_config.rumble.stream_key},
{"streaming.custom.url", &config.streaming_config.custom.url},
{"streaming.custom.container", &config.streaming_config.custom.container},
{"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey},
@@ -203,6 +217,7 @@ namespace gsr {
{"record.record_options.application_audio_invert", &config.record_config.record_options.application_audio_invert},
{"record.record_options.change_video_resolution", &config.record_config.record_options.change_video_resolution},
{"record.record_options.audio_track", &config.record_config.record_options.audio_tracks},
+ {"record.record_options.audio_track_item", &config.record_config.record_options.audio_tracks_list},
{"record.record_options.color_range", &config.record_config.record_options.color_range},
{"record.record_options.video_quality", &config.record_config.record_options.video_quality},
{"record.record_options.codec", &config.record_config.record_options.video_codec},
@@ -215,6 +230,7 @@ namespace gsr {
{"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder},
{"record.show_recording_started_notifications", &config.record_config.show_recording_started_notifications},
{"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications},
+ {"record.show_video_paused_notifications", &config.record_config.show_video_paused_notifications},
{"record.save_directory", &config.record_config.save_directory},
{"record.container", &config.record_config.container},
{"record.start_stop_hotkey", &config.record_config.start_stop_hotkey},
@@ -231,6 +247,7 @@ namespace gsr {
{"replay.record_options.application_audio_invert", &config.replay_config.record_options.application_audio_invert},
{"replay.record_options.change_video_resolution", &config.replay_config.record_options.change_video_resolution},
{"replay.record_options.audio_track", &config.replay_config.record_options.audio_tracks},
+ {"replay.record_options.audio_track_item", &config.replay_config.record_options.audio_tracks_list},
{"replay.record_options.color_range", &config.replay_config.record_options.color_range},
{"replay.record_options.video_quality", &config.replay_config.record_options.video_quality},
{"replay.record_options.codec", &config.replay_config.record_options.video_codec},
@@ -249,8 +266,11 @@ namespace gsr {
{"replay.save_directory", &config.replay_config.save_directory},
{"replay.container", &config.replay_config.container},
{"replay.time", &config.replay_config.replay_time},
+ {"replay.replay_storage", &config.replay_config.replay_storage},
{"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey},
{"replay.save_hotkey", &config.replay_config.save_hotkey},
+ {"replay.save_1_min_hotkey", &config.replay_config.save_1_min_hotkey},
+ {"replay.save_10_min_hotkey", &config.replay_config.save_10_min_hotkey},
{"screenshot.record_area_option", &config.screenshot_config.record_area_option},
{"screenshot.image_width", &config.screenshot_config.image_width},
@@ -291,6 +311,9 @@ namespace gsr {
} else if(std::holds_alternative<std::vector<std::string>*>(it.second)) {
if(*std::get<std::vector<std::string>*>(it.second) != *std::get<std::vector<std::string>*>(it_other->second))
return false;
+ } else if(std::holds_alternative<std::vector<AudioTrack>*>(it.second)) {
+ if(*std::get<std::vector<AudioTrack>*>(it.second) != *std::get<std::vector<AudioTrack>*>(it_other->second))
+ return false;
} else {
assert(false);
}
@@ -302,6 +325,17 @@ namespace gsr {
return !operator==(other);
}
+ static void populate_new_audio_track_from_old(RecordOptions &record_options) {
+ if(record_options.merge_audio_tracks) {
+ record_options.audio_tracks_list.push_back({std::move(record_options.audio_tracks), record_options.application_audio_invert});
+ } else {
+ for(const std::string &audio_input : record_options.audio_tracks) {
+ record_options.audio_tracks_list.push_back({std::vector<std::string>{audio_input}, record_options.application_audio_invert});
+ }
+ }
+ record_options.audio_tracks.clear();
+ }
+
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options) {
std::optional<Config> config;
@@ -313,10 +347,15 @@ namespace gsr {
}
config = Config(capture_options);
+
config->streaming_config.record_options.audio_tracks.clear();
config->record_config.record_options.audio_tracks.clear();
config->replay_config.record_options.audio_tracks.clear();
+ config->streaming_config.record_options.audio_tracks_list.clear();
+ config->record_config.record_options.audio_tracks_list.clear();
+ config->replay_config.record_options.audio_tracks_list.clear();
+
auto config_options = get_config_options(config.value());
string_split_char(file_content, '\n', [&](std::string_view line) {
@@ -355,6 +394,23 @@ namespace gsr {
} else if(std::holds_alternative<std::vector<std::string>*>(it->second)) {
std::string array_value(key_value->value);
std::get<std::vector<std::string>*>(it->second)->push_back(std::move(array_value));
+ } else if(std::holds_alternative<std::vector<AudioTrack>*>(it->second)) {
+ const size_t space_index = key_value->value.find(' ');
+ if(space_index == std::string_view::npos) {
+ fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data());
+ return true;
+ }
+
+ const bool application_audio_invert = key_value->value.substr(0, space_index) == "true";
+ const std::string_view audio_input = key_value->value.substr(space_index + 1);
+ std::vector<AudioTrack> &audio_tracks = *std::get<std::vector<AudioTrack>*>(it->second);
+
+ if(audio_input == add_audio_track_tag) {
+ audio_tracks.push_back({std::vector<std::string>{}, application_audio_invert});
+ } else if(!audio_tracks.empty()) {
+ audio_tracks.back().application_audio_invert = application_audio_invert;
+ audio_tracks.back().audio_inputs.emplace_back(audio_input);
+ }
} else {
assert(false);
}
@@ -362,11 +418,16 @@ namespace gsr {
return true;
});
- if(config->main_config.config_file_version != GSR_CONFIG_FILE_VERSION) {
- fprintf(stderr, "Info: the config file is outdated, resetting it\n");
- config = std::nullopt;
+ if(config->main_config.config_file_version == 1) {
+ populate_new_audio_track_from_old(config->streaming_config.record_options);
+ populate_new_audio_track_from_old(config->record_config.record_options);
+ populate_new_audio_track_from_old(config->replay_config.record_options);
}
+ config->streaming_config.record_options.audio_tracks.clear();
+ config->record_config.record_options.audio_tracks.clear();
+ config->replay_config.record_options.audio_tracks.clear();
+
return config;
}
@@ -402,9 +463,17 @@ namespace gsr {
const ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it.second);
fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->key, config_hotkey->modifiers);
} else if(std::holds_alternative<std::vector<std::string>*>(it.second)) {
- std::vector<std::string> *array = std::get<std::vector<std::string>*>(it.second);
- for(const std::string &value : *array) {
- fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), value.c_str());
+ std::vector<std::string> *audio_inputs = std::get<std::vector<std::string>*>(it.second);
+ for(const std::string &audio_input : *audio_inputs) {
+ fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), audio_input.c_str());
+ }
+ } else if(std::holds_alternative<std::vector<AudioTrack>*>(it.second)) {
+ std::vector<AudioTrack> *audio_tracks = std::get<std::vector<AudioTrack>*>(it.second);
+ for(const AudioTrack &audio_track : *audio_tracks) {
+ fprintf(file, "%.*s %s %.*s\n", (int)it.first.size(), it.first.data(), audio_track.application_audio_invert ? "true" : "false", (int)add_audio_track_tag.size(), add_audio_track_tag.data());
+ for(const std::string &audio_input : audio_track.audio_inputs) {
+ fprintf(file, "%.*s %s %s\n", (int)it.first.size(), it.first.data(), audio_track.application_audio_invert ? "true" : "false", audio_input.c_str());
+ }
}
} else {
assert(false);
diff --git a/src/CursorTracker/CursorTrackerWayland.cpp b/src/CursorTracker/CursorTrackerWayland.cpp
new file mode 100644
index 0000000..7af86b4
--- /dev/null
+++ b/src/CursorTracker/CursorTrackerWayland.cpp
@@ -0,0 +1,538 @@
+#include "../../include/CursorTracker/CursorTrackerWayland.hpp"
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+#include <wayland-client.h>
+#include "xdg-output-unstable-v1-client-protocol.h"
+
+namespace gsr {
+ static const int MAX_CONNECTORS = 32;
+ static const uint32_t plane_property_all = 0xF;
+
+ typedef enum {
+ PLANE_PROPERTY_CRTC_X = 1 << 0,
+ PLANE_PROPERTY_CRTC_Y = 1 << 1,
+ PLANE_PROPERTY_CRTC_ID = 1 << 2,
+ PLANE_PROPERTY_TYPE_CURSOR = 1 << 3,
+ } plane_property_mask;
+
+ typedef struct {
+ uint64_t crtc_id;
+ mgl::vec2i size;
+ bool vrr_enabled;
+ } drm_connector;
+
+ typedef struct {
+ drm_connector connectors[MAX_CONNECTORS];
+ int num_connectors;
+ bool has_any_crtc_with_vrr_enabled;
+ } drm_connectors;
+
+ /* Returns plane_property_mask */
+ static uint32_t plane_get_properties(int drm_fd, uint32_t plane_id, int *crtc_x, int *crtc_y, int *crtc_id) {
+ *crtc_x = 0;
+ *crtc_y = 0;
+ *crtc_id = 0;
+
+ uint32_t property_mask = 0;
+
+ drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(drm_fd, plane_id, DRM_MODE_OBJECT_PLANE);
+ if(!props)
+ return property_mask;
+
+ for(uint32_t i = 0; i < props->count_props; ++i) {
+ drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]);
+ if(!prop)
+ continue;
+
+ // SRC_* values are fixed 16.16 points
+ const uint32_t type = prop->flags & (DRM_MODE_PROP_LEGACY_TYPE | DRM_MODE_PROP_EXTENDED_TYPE);
+ if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_X") == 0) {
+ *crtc_x = (int)props->prop_values[i];
+ property_mask |= PLANE_PROPERTY_CRTC_X;
+ } else if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_Y") == 0) {
+ *crtc_y = (int)props->prop_values[i];
+ property_mask |= PLANE_PROPERTY_CRTC_Y;
+ } else if((type & DRM_MODE_PROP_OBJECT) && strcmp(prop->name, "CRTC_ID") == 0) {
+ *crtc_id = (int)props->prop_values[i];
+ property_mask |= PLANE_PROPERTY_CRTC_ID;
+ } else if((type & DRM_MODE_PROP_ENUM) && strcmp(prop->name, "type") == 0) {
+ const uint64_t current_enum_value = props->prop_values[i];
+ for(int j = 0; j < prop->count_enums; ++j) {
+ if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Cursor") == 0) {
+ property_mask |= PLANE_PROPERTY_TYPE_CURSOR;
+ break;
+ }
+ }
+ }
+
+ drmModeFreeProperty(prop);
+ }
+
+ drmModeFreeObjectProperties(props);
+ return property_mask;
+ }
+
+ static bool get_drm_property_by_name(int drm_fd, drmModeObjectPropertiesPtr props, const char *name, uint64_t *result) {
+ for(uint32_t i = 0; i < props->count_props; ++i) {
+ drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]);
+ if(!prop)
+ continue;
+
+ if(strcmp(name, prop->name) == 0) {
+ *result = props->prop_values[i];
+ drmModeFreeProperty(prop);
+ return true;
+ }
+ drmModeFreeProperty(prop);
+ }
+ return false;
+ }
+
+ static bool connector_get_property_by_name(int drm_fd, drmModeConnectorPtr props, const char *name, uint64_t *result) {
+ drmModeObjectProperties properties;
+ properties.count_props = (uint32_t)props->count_props;
+ properties.props = props->props;
+ properties.prop_values = props->prop_values;
+ return get_drm_property_by_name(drm_fd, &properties, name, result);
+ }
+
+ // Note: this monitor name logic is kept in sync with gpu screen recorder
+ static std::string get_monitor_name_from_crtc_id(int drm_fd, uint32_t crtc_id) {
+ std::string result;
+ drmModeResPtr resources = drmModeGetResources(drm_fd);
+ if(!resources)
+ return result;
+
+ for(int i = 0; i < resources->count_connectors; ++i) {
+ uint64_t connector_crtc_id = 0;
+ drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]);
+ if(!connector)
+ continue;
+
+ const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type);
+ if(!connection_name)
+ goto next;
+
+ if(connector->connection != DRM_MODE_CONNECTED)
+ goto next;
+
+ if(connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
+ result = connection_name;
+ result += "-";
+ result += std::to_string(connector->connector_type_id);
+ drmModeFreeConnector(connector);
+ break;
+ }
+
+ next:
+ drmModeFreeConnector(connector);
+ }
+
+ drmModeFreeResources(resources);
+ return result;
+ }
+
+ // Name is the crtc name. TODO: verify if this works on all wayland compositors
+ static const WaylandOutput* get_wayland_monitor_by_name(const std::vector<WaylandOutput> &monitors, const std::string &name) {
+ for(const WaylandOutput &monitor : monitors) {
+ if(monitor.name == name)
+ return &monitor;
+ }
+ return nullptr;
+ }
+
+ static WaylandOutput* get_wayland_monitor_by_output(CursorTrackerWayland &cursor_tracker_wayland, struct wl_output *output) {
+ for(WaylandOutput &monitor : cursor_tracker_wayland.monitors) {
+ if(monitor.output == output)
+ return &monitor;
+ }
+ return nullptr;
+ }
+
+ 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;
+ CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
+ WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
+ if(!monitor)
+ return;
+
+ monitor->pos.x = x;
+ monitor->pos.y = y;
+ monitor->transform = transform;
+ }
+
+ 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;
+ CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
+ WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
+ if(!monitor)
+ return;
+
+ monitor->size.x = width;
+ monitor->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;
+ CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
+ WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
+ if(!monitor)
+ return;
+
+ monitor->name = 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 = {
+ output_handle_geometry,
+ output_handle_mode,
+ output_handle_done,
+ output_handle_scale,
+ output_handle_name,
+ 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;
+ CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
+ if(strcmp(interface, wl_output_interface.name) == 0) {
+ if(version < 4) {
+ fprintf(stderr, "Warning: wl output interface version is < 4, expected >= 4\n");
+ return;
+ }
+
+ struct wl_output *output = (struct wl_output*)wl_registry_bind(registry, name, &wl_output_interface, 4);
+ cursor_tracker_wayland->monitors.push_back(
+ WaylandOutput{
+ name,
+ output,
+ nullptr,
+ mgl::vec2i{0, 0},
+ mgl::vec2i{0, 0},
+ 0,
+ ""
+ });
+ wl_output_add_listener(output, &output_listener, cursor_tracker_wayland);
+ } else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
+ if(version < 1) {
+ fprintf(stderr, "Warning: xdg output interface version is < 1, expected >= 1\n");
+ return;
+ }
+
+ if(cursor_tracker_wayland->xdg_output_manager) {
+ zxdg_output_manager_v1_destroy(cursor_tracker_wayland->xdg_output_manager);
+ cursor_tracker_wayland->xdg_output_manager = NULL;
+ }
+ cursor_tracker_wayland->xdg_output_manager = (struct zxdg_output_manager_v1*)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1);
+ }
+ }
+
+ static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) {
+ (void)data;
+ (void)registry;
+ (void)name;
+ // TODO: Remove output
+ }
+
+ static struct wl_registry_listener registry_listener = {
+ registry_add_object,
+ registry_remove_object,
+ };
+
+ static void xdg_output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y) {
+ (void)zxdg_output_v1;
+ WaylandOutput *monitor = (WaylandOutput*)data;
+ monitor->pos.x = x;
+ monitor->pos.y = y;
+ }
+
+ static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) {
+ (void)data;
+ (void)xdg_output;
+ (void)width;
+ (void)height;
+ }
+
+ static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) {
+ (void)data;
+ (void)xdg_output;
+ }
+
+ static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) {
+ (void)data;
+ (void)xdg_output;
+ (void)name;
+ }
+
+ static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) {
+ (void)data;
+ (void)xdg_output;
+ (void)description;
+ }
+
+ static const struct zxdg_output_v1_listener xdg_output_listener = {
+ xdg_output_logical_position,
+ xdg_output_handle_logical_size,
+ xdg_output_handle_done,
+ xdg_output_handle_name,
+ xdg_output_handle_description,
+ };
+
+ /* Returns nullptr if not found */
+ static drm_connector* get_drm_connector_by_crtc_id(drm_connectors *connectors, uint32_t crtc_id) {
+ for(int i = 0; i < connectors->num_connectors; ++i) {
+ if(connectors->connectors[i].crtc_id == crtc_id)
+ return &connectors->connectors[i];
+ }
+ return nullptr;
+ }
+
+ static void get_drm_connectors(int drm_fd, drm_connectors *drm_connectors) {
+ drm_connectors->num_connectors = 0;
+ drm_connectors->has_any_crtc_with_vrr_enabled = false;
+
+ drmModeResPtr resources = drmModeGetResources(drm_fd);
+ if(!resources)
+ return;
+
+ for(int i = 0; i < resources->count_connectors && drm_connectors->num_connectors < MAX_CONNECTORS; ++i) {
+ drmModeConnectorPtr connector = nullptr;
+ drmModeCrtcPtr crtc = nullptr;
+
+ connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]);
+ if(!connector)
+ continue;
+
+ uint64_t crtc_id = 0;
+ connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &crtc_id);
+ if(crtc_id == 0)
+ goto next_connector;
+
+ crtc = drmModeGetCrtc(drm_fd, crtc_id);
+ if(!crtc)
+ goto next_connector;
+
+ drm_connectors->connectors[drm_connectors->num_connectors].crtc_id = crtc_id;
+ drm_connectors->connectors[drm_connectors->num_connectors].size = mgl::vec2i{(int)crtc->width, (int)crtc->height};
+ drm_connectors->connectors[drm_connectors->num_connectors].vrr_enabled = false;
+ ++drm_connectors->num_connectors;
+
+ next_connector:
+ if(crtc)
+ drmModeFreeCrtc(crtc);
+
+ if(connector)
+ drmModeFreeConnector(connector);
+ }
+
+ for(int i = 0; i < resources->count_crtcs; ++i) {
+ drmModeCrtcPtr crtc = nullptr;
+ drmModeObjectPropertiesPtr properties = nullptr;
+ uint64_t vrr_enabled = 0;
+ drm_connector *connector = nullptr;
+
+ crtc = drmModeGetCrtc(drm_fd, resources->crtcs[i]);
+ if(!crtc)
+ continue;
+
+ properties = drmModeObjectGetProperties(drm_fd, crtc->crtc_id, DRM_MODE_OBJECT_CRTC);
+ if(!properties)
+ goto next_crtc;
+
+ if(!get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled))
+ goto next_crtc;
+
+ connector = get_drm_connector_by_crtc_id(drm_connectors, crtc->crtc_id);
+ if(!connector)
+ goto next_crtc;
+
+ if(vrr_enabled) {
+ connector->vrr_enabled = true;
+ drm_connectors->has_any_crtc_with_vrr_enabled = true;
+ }
+
+ next_crtc:
+ if(properties)
+ drmModeFreeObjectProperties(properties);
+
+ if(crtc)
+ drmModeFreeCrtc(crtc);
+ }
+
+ drmModeFreeResources(resources);
+ }
+
+ CursorTrackerWayland::CursorTrackerWayland(const char *card_path) {
+ drm_fd = open(card_path, O_RDONLY);
+ if(drm_fd <= 0) {
+ fprintf(stderr, "Error: CursorTrackerWayland: failed to open %s\n", card_path);
+ return;
+ }
+
+ drmSetClientCap(drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
+ drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1);
+ }
+
+ CursorTrackerWayland::~CursorTrackerWayland() {
+ if(drm_fd > 0)
+ close(drm_fd);
+ }
+
+ void CursorTrackerWayland::update() {
+ if(drm_fd <= 0)
+ return;
+
+ drm_connectors connectors;
+ connectors.num_connectors = 0;
+ connectors.has_any_crtc_with_vrr_enabled = false;
+ get_drm_connectors(drm_fd, &connectors);
+
+ drmModePlaneResPtr planes = drmModeGetPlaneResources(drm_fd);
+ if(!planes)
+ return;
+
+ bool found_cursor = false;
+ for(uint32_t i = 0; i < planes->count_planes; ++i) {
+ drmModePlanePtr plane = nullptr;
+ const drm_connector *connector = nullptr;
+ int crtc_x = 0;
+ int crtc_y = 0;
+ int crtc_id = 0;
+ uint32_t property_mask = 0;
+
+ plane = drmModeGetPlane(drm_fd, planes->planes[i]);
+ if(!plane)
+ goto next;
+
+ if(!plane->fb_id)
+ goto next;
+
+ property_mask = plane_get_properties(drm_fd, planes->planes[i], &crtc_x, &crtc_y, &crtc_id);
+ if(property_mask != plane_property_all || crtc_id <= 0)
+ goto next;
+
+ connector = get_drm_connector_by_crtc_id(&connectors, crtc_id);
+ if(!connector)
+ goto next;
+
+ if(crtc_x >= 0 && crtc_x <= connector->size.x && crtc_y >= 0 && crtc_y <= connector->size.y) {
+ latest_cursor_position.x = crtc_x;
+ latest_cursor_position.y = crtc_y;
+ latest_crtc_id = crtc_id;
+ found_cursor = true;
+ drmModeFreePlane(plane);
+ break;
+ }
+
+ next:
+ drmModeFreePlane(plane);
+ }
+
+ // On kde plasma wayland (and possibly other wayland compositors) it uses a software cursor only for the monitors with vrr enabled.
+ // In that case we cant know the cursor location and we instead want to fallback to getting focused monitor by using the hack of creating a window and getting the position.
+ if(!found_cursor && latest_crtc_id > 0 && connectors.has_any_crtc_with_vrr_enabled)
+ latest_crtc_id = -1;
+
+ drmModeFreePlaneResources(planes);
+ }
+
+ void CursorTrackerWayland::set_monitor_outputs_from_xdg_output(struct wl_display *dpy) {
+ if(!xdg_output_manager) {
+ fprintf(stderr, "Warning: CursorTrackerWayland::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n");
+ return;
+ }
+
+ for(WaylandOutput &monitor : monitors) {
+ monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, monitor.output);
+ zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor);
+ }
+
+ // Fetch xdg_output
+ wl_display_roundtrip(dpy);
+ }
+
+ std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() {
+ if(drm_fd <= 0 || latest_crtc_id == -1)
+ return std::nullopt;
+
+ std::string monitor_name = get_monitor_name_from_crtc_id(drm_fd, latest_crtc_id);
+ if(monitor_name.empty())
+ return std::nullopt;
+
+ struct wl_display *dpy = wl_display_connect(nullptr);
+ if(!dpy) {
+ fprintf(stderr, "Error: CursorTrackerWayland::get_latest_cursor_info: failed to connect to the wayland server\n");
+ return std::nullopt;
+ }
+
+ monitors.clear();
+ struct wl_registry *registry = wl_display_get_registry(dpy);
+ wl_registry_add_listener(registry, &registry_listener, this);
+
+ // Fetch globals
+ wl_display_roundtrip(dpy);
+
+ // Fetch wl_output
+ wl_display_roundtrip(dpy);
+
+ set_monitor_outputs_from_xdg_output(dpy);
+
+ mgl::vec2i cursor_position = latest_cursor_position;
+ const WaylandOutput *wayland_monitor = get_wayland_monitor_by_name(monitors, monitor_name);
+ if(!wayland_monitor)
+ return std::nullopt;
+
+ cursor_position = wayland_monitor->pos + latest_cursor_position;
+ for(WaylandOutput &monitor : monitors) {
+ if(monitor.output) {
+ wl_output_destroy(monitor.output);
+ monitor.output = nullptr;
+ }
+
+ if(monitor.xdg_output) {
+ zxdg_output_v1_destroy(monitor.xdg_output);
+ monitor.xdg_output = nullptr;
+ }
+ }
+ monitors.clear();
+
+ if(xdg_output_manager) {
+ zxdg_output_manager_v1_destroy(xdg_output_manager);
+ xdg_output_manager = nullptr;
+ }
+
+ wl_registry_destroy(registry);
+ wl_display_disconnect(dpy);
+
+ return CursorInfo{ cursor_position, std::move(monitor_name) };
+ }
+} \ No newline at end of file
diff --git a/src/CursorTracker/CursorTrackerX11.cpp b/src/CursorTracker/CursorTrackerX11.cpp
new file mode 100644
index 0000000..7c98f4d
--- /dev/null
+++ b/src/CursorTracker/CursorTrackerX11.cpp
@@ -0,0 +1,29 @@
+#include "../../include/CursorTracker/CursorTrackerX11.hpp"
+#include "../../include/WindowUtils.hpp"
+
+namespace gsr {
+ CursorTrackerX11::CursorTrackerX11(Display *dpy) : dpy(dpy) {
+
+ }
+
+ std::optional<CursorInfo> CursorTrackerX11::get_latest_cursor_info() {
+ Window window = None;
+ const auto cursor_pos = get_cursor_position(dpy, &window);
+ const auto monitors = get_monitors(dpy);
+ std::string monitor_name;
+
+ for(const auto &monitor : monitors) {
+ if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x
+ && cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y)
+ {
+ monitor_name = monitor.name;
+ break;
+ }
+ }
+
+ if(monitor_name.empty())
+ return std::nullopt;
+
+ return CursorInfo{ cursor_pos, std::move(monitor_name) };
+ }
+} \ No newline at end of file
diff --git a/src/GlobalHotkeysJoystick.cpp b/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp
index d005aa9..5969438 100644
--- a/src/GlobalHotkeysJoystick.cpp
+++ b/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp
@@ -1,4 +1,4 @@
-#include "../include/GlobalHotkeysJoystick.hpp"
+#include "../../include/GlobalHotkeys/GlobalHotkeysJoystick.hpp"
#include <string.h>
#include <errno.h>
#include <fcntl.h>
@@ -7,11 +7,77 @@
namespace gsr {
static constexpr int button_pressed = 1;
+ static constexpr int cross_button = 0;
+ static constexpr int triangle_button = 2;
static constexpr int options_button = 9;
static constexpr int playstation_button = 10;
+ static constexpr int l3_button = 11;
+ static constexpr int r3_button = 12;
static constexpr int axis_up_down = 7;
static constexpr int axis_left_right = 6;
+ struct DeviceId {
+ uint16_t vendor;
+ uint16_t product;
+ };
+
+ static bool read_file_hex_number(const char *path, unsigned int *value) {
+ *value = 0;
+ FILE *f = fopen(path, "rb");
+ if(!f)
+ return false;
+
+ fscanf(f, "%x", value);
+ fclose(f);
+ return true;
+ }
+
+ static DeviceId joystick_get_device_id(const char *path) {
+ DeviceId device_id;
+ device_id.vendor = 0;
+ device_id.product = 0;
+
+ const char *js_path_id = nullptr;
+ const int len = strlen(path);
+ for(int i = len - 1; i >= 0; --i) {
+ if(path[i] == '/') {
+ js_path_id = path + i + 1;
+ break;
+ }
+ }
+
+ if(!js_path_id)
+ return device_id;
+
+ unsigned int vendor = 0;
+ unsigned int product = 0;
+ char path_buf[1024];
+
+ snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id);
+ if(!read_file_hex_number(path_buf, &vendor))
+ return device_id;
+
+ snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id);
+ if(!read_file_hex_number(path_buf, &product))
+ return device_id;
+
+ device_id.vendor = vendor;
+ device_id.product = product;
+ return device_id;
+ }
+
+ static bool is_ps4_controller(DeviceId device_id) {
+ return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4);
+ }
+
+ static bool is_ps5_controller(DeviceId device_id) {
+ return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6);
+ }
+
+ static bool is_stadia_controller(DeviceId device_id) {
+ return device_id.vendor == 0x18D1 && (device_id.product == 0x9400);
+ }
+
// Returns -1 on error
static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) {
if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0)
@@ -104,6 +170,20 @@ namespace gsr {
it->second("save_replay");
}
+ if(save_1_min_replay) {
+ save_1_min_replay = false;
+ auto it = bound_actions_by_id.find("save_1_min_replay");
+ if(it != bound_actions_by_id.end())
+ it->second("save_1_min_replay");
+ }
+
+ if(save_10_min_replay) {
+ save_10_min_replay = false;
+ auto it = bound_actions_by_id.find("save_10_min_replay");
+ if(it != bound_actions_by_id.end())
+ it->second("save_10_min_replay");
+ }
+
if(take_screenshot) {
take_screenshot = false;
auto it = bound_actions_by_id.find("take_screenshot");
@@ -186,10 +266,36 @@ namespace gsr {
return;
if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) {
- if(event.number == playstation_button)
- playstation_button_pressed = event.value == button_pressed;
- else if(playstation_button_pressed && event.number == options_button && event.value == button_pressed)
- toggle_show = true;
+ switch(event.number) {
+ case playstation_button: {
+ // Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time
+ playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed;
+ break;
+ }
+ case options_button: {
+ if(playstation_button_pressed && event.value == button_pressed)
+ toggle_show = true;
+ break;
+ }
+ case cross_button: {
+ if(playstation_button_pressed && event.value == button_pressed)
+ save_1_min_replay = true;
+ break;
+ }
+ case triangle_button: {
+ if(playstation_button_pressed && event.value == button_pressed)
+ save_10_min_replay = true;
+ break;
+ }
+ case l3_button: {
+ l3_button_pressed = event.value == button_pressed;
+ break;
+ }
+ case r3_button: {
+ r3_button_pressed = event.value == button_pressed;
+ break;
+ }
+ }
} else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) {
const int trigger_threshold = 16383;
const bool prev_up_pressed = up_pressed;
@@ -243,6 +349,8 @@ namespace gsr {
dev_input_id
};
+ //const DeviceId device_id = joystick_get_device_id(dev_input_filepath);
+
++num_poll_fd;
fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath);
return true;
diff --git a/src/GlobalHotkeysLinux.cpp b/src/GlobalHotkeys/GlobalHotkeysLinux.cpp
index fbba0ea..a56bbc6 100644
--- a/src/GlobalHotkeysLinux.cpp
+++ b/src/GlobalHotkeys/GlobalHotkeysLinux.cpp
@@ -1,5 +1,4 @@
-#include "../include/GlobalHotkeysLinux.hpp"
-#include <signal.h>
+#include "../../include/GlobalHotkeys/GlobalHotkeysLinux.hpp"
#include <sys/wait.h>
#include <fcntl.h>
#include <limits.h>
@@ -71,21 +70,41 @@ namespace gsr {
}
GlobalHotkeysLinux::~GlobalHotkeysLinux() {
+ if(write_pipes[PIPE_WRITE] > 0) {
+ char command[32];
+ const int command_size = snprintf(command, sizeof(command), "exit\n");
+ if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) {
+ fprintf(stderr, "Error: GlobalHotkeysLinux::~GlobalHotkeysLinux: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno));
+ close_fds();
+ }
+ } else {
+ close_fds();
+ }
+
+ if(process_id > 0) {
+ int status;
+ waitpid(process_id, &status, 0);
+ }
+
+ close_fds();
+ }
+
+ void GlobalHotkeysLinux::close_fds() {
for(int i = 0; i < 2; ++i) {
- if(read_pipes[i] > 0)
+ if(read_pipes[i] > 0) {
close(read_pipes[i]);
+ read_pipes[i] = -1;
+ }
- if(write_pipes[i] > 0)
+ if(write_pipes[i] > 0) {
close(write_pipes[i]);
+ write_pipes[i] = -1;
+ }
}
- if(read_file)
+ if(read_file) {
fclose(read_file);
-
- if(process_id > 0) {
- kill(process_id, SIGKILL);
- int status;
- waitpid(process_id, &status, 0);
+ read_file = nullptr;
}
}
diff --git a/src/GlobalHotkeysX11.cpp b/src/GlobalHotkeys/GlobalHotkeysX11.cpp
index 9af2607..bc79ce8 100644
--- a/src/GlobalHotkeysX11.cpp
+++ b/src/GlobalHotkeys/GlobalHotkeysX11.cpp
@@ -1,4 +1,4 @@
-#include "../include/GlobalHotkeysX11.hpp"
+#include "../../include/GlobalHotkeys/GlobalHotkeysX11.hpp"
#include <X11/keysym.h>
#include <mglpp/window/Event.hpp>
#include <assert.h>
diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp
index 5af6397..d7212d7 100644
--- a/src/GsrInfo.cpp
+++ b/src/GsrInfo.cpp
@@ -175,11 +175,6 @@ namespace gsr {
CAPTURE_OPTIONS
};
- static bool starts_with(std::string_view str, const char *substr) {
- size_t len = strlen(substr);
- return str.size() >= len && memcmp(str.data(), substr, len) == 0;
- }
-
GsrInfoExitStatus get_gpu_screen_recorder_info(GsrInfo *gsr_info) {
*gsr_info = GsrInfo{};
diff --git a/src/Overlay.cpp b/src/Overlay.cpp
index 4eb8844..794ef92 100644
--- a/src/Overlay.cpp
+++ b/src/Overlay.cpp
@@ -12,8 +12,10 @@
#include "../include/gui/Utils.hpp"
#include "../include/gui/PageStack.hpp"
#include "../include/WindowUtils.hpp"
-#include "../include/GlobalHotkeys.hpp"
-#include "../include/GlobalHotkeysLinux.hpp"
+#include "../include/GlobalHotkeys/GlobalHotkeys.hpp"
+#include "../include/GlobalHotkeys/GlobalHotkeysLinux.hpp"
+#include "../include/CursorTracker/CursorTrackerX11.hpp"
+#include "../include/CursorTracker/CursorTrackerWayland.hpp"
#include <string.h>
#include <assert.h>
@@ -24,6 +26,7 @@
#include <malloc.h>
#include <stdexcept>
#include <algorithm>
+#include <inttypes.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
@@ -35,6 +38,7 @@
#include <X11/Xcursor/Xcursor.h>
#include <mglpp/system/Rect.hpp>
#include <mglpp/window/Event.hpp>
+#include <mglpp/system/Utf8.hpp>
extern "C" {
#include <mgl/mgl.h>
@@ -45,8 +49,9 @@ namespace gsr {
static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.5;
static const double replay_saving_notification_timeout_seconds = 0.5;
- static const double notification_timeout_seconds = 2.0;
+ static const double notification_timeout_seconds = 2.5;
static const double notification_error_timeout_seconds = 5.0;
+ static const double cursor_tracker_update_timeout_sec = 0.1;
static mgl::Texture texture_from_ximage(XImage *img) {
uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3);
@@ -204,14 +209,20 @@ namespace gsr {
return false;
}*/
- // Returns the first monitor if not found. Assumes there is at least one monitor connected.
static const Monitor* find_monitor_at_position(const std::vector<Monitor> &monitors, mgl::vec2i pos) {
- assert(!monitors.empty());
for(const Monitor &monitor : monitors) {
if(mgl::IntRect(monitor.position, monitor.size).contains(pos))
return &monitor;
}
- return &monitors.front();
+ return nullptr;
+ }
+
+ static const Monitor* find_monitor_by_name(const std::vector<Monitor> &monitors, const std::string &name) {
+ for(const Monitor &monitor : monitors) {
+ if(monitor.name == name)
+ return &monitor;
+ }
+ return nullptr;
}
static std::string get_power_supply_online_filepath() {
@@ -262,6 +273,33 @@ namespace gsr {
return true;
}
+ static bool is_hyprland_waybar_running_as_dock() {
+ const char *args[] = { "hyprctl", "layers", nullptr };
+ std::string stdout_str;
+ if(exec_program_on_host_get_stdout(args, stdout_str) != 0)
+ return false;
+
+ int waybar_layer_level = -1;
+ int current_layer_level = 0;
+ string_split_char(stdout_str, '\n', [&](const std::string_view line) {
+ if(line.find("Layer level 0") != std::string_view::npos)
+ current_layer_level = 0;
+ else if(line.find("Layer level 1") != std::string_view::npos)
+ current_layer_level = 1;
+ else if(line.find("Layer level 2") != std::string_view::npos)
+ current_layer_level = 2;
+ else if(line.find("Layer level 3") != std::string_view::npos)
+ current_layer_level = 3;
+ else if(line.find("namespace: waybar") != std::string_view::npos) {
+ waybar_layer_level = current_layer_level;
+ return false;
+ }
+ return true;
+ });
+
+ return waybar_layer_level >= 0 && waybar_layer_level <= 1;
+ }
+
static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) {
return {
(uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key),
@@ -313,6 +351,20 @@ namespace gsr {
});
global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().replay_config.save_1_min_hotkey),
+ "replay_save_1_min", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->save_replay_1_min();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().replay_config.save_10_min_hotkey),
+ "replay_save_10_min", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->save_replay_10_min();
+ });
+
+ global_hotkeys->bind_key_press(
config_hotkey_to_hotkey(overlay->get_config().screenshot_config.take_screenshot_hotkey),
"take_screenshot", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
@@ -325,6 +377,13 @@ namespace gsr {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->take_screenshot_region();
});
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(ConfigHotkey{ mgl::Keyboard::Key::Escape, HOTKEY_MOD_LCTRL | HOTKEY_MOD_LSHIFT | HOTKEY_MOD_LALT }),
+ "exit", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->go_back_to_old_ui();
+ });
}
static std::unique_ptr<GlobalHotkeysLinux> register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) {
@@ -351,6 +410,16 @@ namespace gsr {
overlay->save_replay();
});
+ global_hotkeys_js->bind_action("save_1_min_replay", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->save_replay_1_min();
+ });
+
+ global_hotkeys_js->bind_action("save_10_min_replay", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->save_replay_10_min();
+ });
+
global_hotkeys_js->bind_action("take_screenshot", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->take_screenshot();
@@ -411,6 +480,11 @@ namespace gsr {
XKeysymToKeycode(x11_mapping_display, XK_F1); // If we dont call we will never get a MappingNotify
else
fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n");
+
+ if(this->gsr_info.system_info.display_server == DisplayServer::X11)
+ cursor_tracker = std::make_unique<CursorTrackerX11>((Display*)mgl_get_context()->connection);
+ else if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND && !this->gsr_info.gpu_info.card_path.empty())
+ cursor_tracker = std::make_unique<CursorTrackerWayland>(this->gsr_info.gpu_info.card_path.c_str());
}
Overlay::~Overlay() {
@@ -531,8 +605,8 @@ namespace gsr {
memset(xi_output_xev, 0, sizeof(*xi_output_xev));
xi_output_xev->type = MotionNotify;
xi_output_xev->xmotion.display = display;
- xi_output_xev->xmotion.window = window->get_system_handle();
- xi_output_xev->xmotion.subwindow = window->get_system_handle();
+ xi_output_xev->xmotion.window = (Window)window->get_system_handle();
+ xi_output_xev->xmotion.subwindow = (Window)window->get_system_handle();
xi_output_xev->xmotion.x = de->root_x - window_pos.x;
xi_output_xev->xmotion.y = de->root_y - window_pos.y;
xi_output_xev->xmotion.x_root = de->root_x;
@@ -544,8 +618,8 @@ namespace gsr {
memset(xi_output_xev, 0, sizeof(*xi_output_xev));
xi_output_xev->type = cookie->evtype == XI_ButtonPress ? ButtonPress : ButtonRelease;
xi_output_xev->xbutton.display = display;
- xi_output_xev->xbutton.window = window->get_system_handle();
- xi_output_xev->xbutton.subwindow = window->get_system_handle();
+ xi_output_xev->xbutton.window = (Window)window->get_system_handle();
+ xi_output_xev->xbutton.subwindow = (Window)window->get_system_handle();
xi_output_xev->xbutton.x = de->root_x - window_pos.x;
xi_output_xev->xbutton.y = de->root_y - window_pos.y;
xi_output_xev->xbutton.x_root = de->root_x;
@@ -558,8 +632,8 @@ namespace gsr {
memset(xi_output_xev, 0, sizeof(*xi_output_xev));
xi_output_xev->type = cookie->evtype == XI_KeyPress ? KeyPress : KeyRelease;
xi_output_xev->xkey.display = display;
- xi_output_xev->xkey.window = window->get_system_handle();
- xi_output_xev->xkey.subwindow = window->get_system_handle();
+ xi_output_xev->xkey.window = (Window)window->get_system_handle();
+ xi_output_xev->xkey.subwindow = (Window)window->get_system_handle();
xi_output_xev->xkey.x = de->root_x - window_pos.x;
xi_output_xev->xkey.y = de->root_y - window_pos.y;
xi_output_xev->xkey.x_root = de->root_x;
@@ -617,6 +691,12 @@ namespace gsr {
if(global_hotkeys_js)
global_hotkeys_js->poll_events();
+ if(cursor_tracker_update_clock.get_elapsed_time_seconds() >= cursor_tracker_update_timeout_sec) {
+ cursor_tracker_update_clock.restart();
+ if(cursor_tracker)
+ cursor_tracker->update();
+ }
+
handle_keyboard_mapping_event();
region_selector.poll_events();
if(region_selector.take_canceled()) {
@@ -626,6 +706,22 @@ namespace gsr {
on_region_selected = nullptr;
}
+ window_selector.poll_events();
+ if(window_selector.take_canceled()) {
+ on_window_selected = nullptr;
+ } else if(window_selector.take_selection() && on_window_selected) {
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ const Window selected_window = window_selector.get_selection();
+ if(selected_window && selected_window != DefaultRootWindow(display)) {
+ on_window_selected();
+ } else {
+ show_notification("No window selected", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
+ }
+ on_window_selected = nullptr;
+ }
+
if(!visible || !window)
return;
@@ -654,8 +750,10 @@ namespace gsr {
}
bool Overlay::draw() {
+ remove_widgets_to_be_removed();
+
update_notification_process_status();
- update_gsr_replay_save();
+ process_gsr_output();
update_gsr_process_status();
update_gsr_screenshot_process_status();
replay_status_update_status();
@@ -664,12 +762,21 @@ namespace gsr {
start_region_capture = false;
hide();
if(!region_selector.start(get_color_theme().tint_color)) {
- show_notification("Failed to start region capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::NONE);
+ show_notification("Failed to start region capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
on_region_selected = nullptr;
}
}
- if(region_selector.is_started()) {
+ if(start_window_capture) {
+ start_window_capture = false;
+ hide();
+ if(!window_selector.start(get_color_theme().tint_color)) {
+ show_notification("Failed to start window capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
+ on_window_selected = nullptr;
+ }
+ }
+
+ if(region_selector.is_started() || window_selector.is_started()) {
usleep(5 * 1000); // 5 ms
return true;
}
@@ -734,13 +841,13 @@ namespace gsr {
// There should be a debug mode to not use these
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
- XGrabPointer(display, window->get_system_handle(), True,
+ XGrabPointer(display, (Window)window->get_system_handle(), True,
ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask |
ButtonMotionMask,
GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime);
// TODO: This breaks global hotkeys (when using x11 global hotkeys)
- XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime);
+ XGrabKeyboard(display, (Window)window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime);
XFlush(display);
}
@@ -803,7 +910,7 @@ namespace gsr {
if(visible)
return;
- if(region_selector.is_started())
+ if(region_selector.is_started() || window_selector.is_started())
return;
drawn_first_frame = false;
@@ -824,18 +931,36 @@ namespace gsr {
const std::string wm_name = get_window_manager_name(display);
const bool is_kwin = wm_name == "KWin";
const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
+ const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
+ const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock();
+
+ std::optional<CursorInfo> cursor_info;
+ if(cursor_tracker) {
+ cursor_tracker->update();
+ cursor_info = cursor_tracker->get_latest_cursor_info();
+ }
// The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it
Window x11_cursor_window = None;
- const mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window);
- const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display);
-
- const Monitor *focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value);
+ mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window);
+ const Monitor *focused_monitor = nullptr;
+ if(cursor_info) {
+ focused_monitor = find_monitor_by_name(monitors, cursor_info->monitor_name);
+ if(!focused_monitor)
+ focused_monitor = &monitors.front();
+ cursor_position = cursor_info->position;
+ } else {
+ const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display);
+ focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value);
+ if(!focused_monitor)
+ focused_monitor = &monitors.front();
+ }
// Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused.
// If the focused window is a wayland application then don't use override redirect and instead create
// a fullscreen window for the ui.
- const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots;
+ // TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor))
+ const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland;
if(prevent_game_minimizing) {
window_pos = focused_monitor->position;
@@ -863,18 +988,21 @@ namespace gsr {
// Nvidia + Wayland + Egl doesn't work on some systems properly and it instead falls back to software rendering.
// Use Glx on Wayland to workaround this issue. This is fine since Egl is only needed for x11 to reliably get the texture of the fullscreen window on Nvidia
// when a compositor isn't running.
- window_create_params.render_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_RENDER_API_GLX : MGL_RENDER_API_EGL;
+ window_create_params.graphics_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_GRAPHICS_API_GLX : MGL_GRAPHICS_API_EGL;
- if(!window->create("gsr ui", window_create_params))
+ if(!window->create("gsr ui", window_create_params)) {
fprintf(stderr, "error: failed to create window\n");
+ window.reset();
+ return;
+ }
//window->set_low_latency(true);
unsigned char data = 2; // Prefer being composed to allow transparency
- XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
+ XChangeProperty(display, (Window)window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
data = 1;
- XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
+ XChangeProperty(display, (Window)window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
const auto original_window_size = window_size;
window_pos = focused_monitor->position;
@@ -908,12 +1036,12 @@ namespace gsr {
//window->set_fullscreen(true);
if(gsr_info.system_info.display_server == DisplayServer::X11)
- make_window_click_through(display, window->get_system_handle());
+ make_window_click_through(display, (Window)window->get_system_handle());
window->set_visible(true);
- make_window_sticky(display, window->get_system_handle());
- hide_window_from_taskbar(display, window->get_system_handle());
+ make_window_sticky(display, (Window)window->get_system_handle());
+ hide_window_from_taskbar(display, (Window)window->get_system_handle());
if(default_cursor) {
XFreeCursor(display, default_cursor);
@@ -925,13 +1053,18 @@ namespace gsr {
grab_mouse_and_keyboard();
// The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed
+ cursor_hotspot = {0, 0};
xi_setup_fake_cursor();
+ if(cursor_info && gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
+ win->cursor_position.x += cursor_hotspot.x;
+ win->cursor_position.y += cursor_hotspot.y;
+ }
// We want to grab all devices to prevent any other application below the UI from receiving events.
// Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
xi_grab_all_mouse_devices(xi_display);
- if(!is_wlroots)
+ if(!is_wlroots && !hyprland_waybar_is_dock)
window->set_fullscreen(true);
visible = true;
@@ -955,6 +1088,9 @@ namespace gsr {
if(paused)
update_ui_recording_paused();
+ if(replay_recording)
+ update_ui_recording_started();
+
// Wayland compositors have retarded fullscreen animations that we cant disable in a proper way
// without messing up window position.
show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15;
@@ -1002,10 +1138,16 @@ namespace gsr {
replay_dropdown_button_ptr = button.get();
button->add_item("Turn on", "start", config.replay_config.start_stop_hotkey.to_string(false, false));
button->add_item("Save", "save", config.replay_config.save_hotkey.to_string(false, false));
+ if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
+ button->add_item("Save 1 min", "save_1_min", config.replay_config.save_1_min_hotkey.to_string(false, false));
+ button->add_item("Save 10 min", "save_10_min", config.replay_config.save_10_min_hotkey.to_string(false, false));
+ }
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("save", &get_theme().save_texture);
- button->set_item_icon("settings", &get_theme().settings_small_texture);
+ button->set_item_icon("save_1_min", &get_theme().save_texture);
+ button->set_item_icon("save_10_min", &get_theme().save_texture);
+ button->set_item_icon("settings", &get_theme().settings_extra_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack);
@@ -1017,10 +1159,17 @@ namespace gsr {
page_stack.push(std::move(replay_settings_page));
} else if(id == "save") {
on_press_save_replay();
+ } else if(id == "save_1_min") {
+ on_press_save_replay_1_min_replay();
+ } else if(id == "save_10_min") {
+ on_press_save_replay_10_min_replay();
} else if(id == "start") {
on_press_start_replay(false, false);
}
};
+ button->set_item_enabled("save", false);
+ button->set_item_enabled("save_1_min", false);
+ button->set_item_enabled("save_10_min", false);
main_buttons_list->add_widget(std::move(button));
}
{
@@ -1032,7 +1181,7 @@ namespace gsr {
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("pause", &get_theme().pause_texture);
- button->set_item_icon("settings", &get_theme().settings_small_texture);
+ button->set_item_icon("settings", &get_theme().settings_extra_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack);
@@ -1047,6 +1196,7 @@ namespace gsr {
on_press_start_record(false);
}
};
+ button->set_item_enabled("pause", false);
main_buttons_list->add_widget(std::move(button));
}
{
@@ -1056,7 +1206,7 @@ namespace gsr {
button->add_item("Start", "start", config.streaming_config.start_stop_hotkey.to_string(false, false));
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
- button->set_item_icon("settings", &get_theme().settings_small_texture);
+ button->set_item_icon("settings", &get_theme().settings_extra_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack);
@@ -1125,23 +1275,15 @@ namespace gsr {
};
settings_page->on_page_closed = [this]() {
- if(global_hotkeys) {
- replay_dropdown_button_ptr->set_item_description("start", config.replay_config.start_stop_hotkey.to_string(false, false));
- replay_dropdown_button_ptr->set_item_description("save", config.replay_config.save_hotkey.to_string(false, false));
+ replay_dropdown_button_ptr->set_item_description("start", config.replay_config.start_stop_hotkey.to_string(false, false));
+ replay_dropdown_button_ptr->set_item_description("save", config.replay_config.save_hotkey.to_string(false, false));
+ replay_dropdown_button_ptr->set_item_description("save_1_min", config.replay_config.save_1_min_hotkey.to_string(false, false));
+ replay_dropdown_button_ptr->set_item_description("save_10_min", config.replay_config.save_10_min_hotkey.to_string(false, false));
- record_dropdown_button_ptr->set_item_description("start", config.record_config.start_stop_hotkey.to_string(false, false));
- record_dropdown_button_ptr->set_item_description("pause", config.record_config.pause_unpause_hotkey.to_string(false, false));
+ record_dropdown_button_ptr->set_item_description("start", config.record_config.start_stop_hotkey.to_string(false, false));
+ record_dropdown_button_ptr->set_item_description("pause", config.record_config.pause_unpause_hotkey.to_string(false, false));
- stream_dropdown_button_ptr->set_item_description("start", config.streaming_config.start_stop_hotkey.to_string(false, false));
- } else {
- replay_dropdown_button_ptr->set_item_description("start", "");
- replay_dropdown_button_ptr->set_item_description("save", "");
-
- record_dropdown_button_ptr->set_item_description("start", "");
- record_dropdown_button_ptr->set_item_description("pause", "");
-
- stream_dropdown_button_ptr->set_item_description("start", "");
- }
+ stream_dropdown_button_ptr->set_item_description("start", config.streaming_config.start_stop_hotkey.to_string(false, false));
};
page_stack.push(std::move(settings_page));
@@ -1202,6 +1344,7 @@ namespace gsr {
while(!page_stack.empty()) {
page_stack.pop();
}
+ remove_widgets_to_be_removed();
if(default_cursor) {
XFreeCursor(display, default_cursor);
@@ -1225,6 +1368,7 @@ namespace gsr {
visible = false;
drawn_first_frame = false;
start_region_capture = false;
+ start_window_capture = false;
if(xi_input_xev) {
free(xi_input_xev);
@@ -1296,10 +1440,12 @@ namespace gsr {
if(paused) {
update_ui_recording_unpaused();
- show_notification("Recording has been unpaused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ if(config.record_config.show_video_paused_notifications)
+ show_notification("Recording has been unpaused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
} else {
update_ui_recording_paused();
- show_notification("Recording has been paused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ if(config.record_config.show_video_paused_notifications)
+ show_notification("Recording has been paused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
}
kill(gpu_screen_recorder_process, SIGUSR2);
@@ -1318,6 +1464,14 @@ namespace gsr {
on_press_save_replay();
}
+ void Overlay::save_replay_1_min() {
+ on_press_save_replay_1_min_replay();
+ }
+
+ void Overlay::save_replay_10_min() {
+ on_press_save_replay_10_min_replay();
+ }
+
void Overlay::take_screenshot() {
on_press_take_screenshot(false, false);
}
@@ -1337,26 +1491,177 @@ namespace gsr {
return nullptr;
}
- void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type) {
+ static void truncate_string(std::string &str, int max_length) {
+ int index = 0;
+ size_t byte_index = 0;
+
+ while(index < max_length && byte_index < str.size()) {
+ uint32_t codepoint = 0;
+ size_t codepoint_length = 0;
+ mgl::utf8_decode((const unsigned char*)str.c_str() + byte_index, str.size() - byte_index, &codepoint, &codepoint_length);
+ if(codepoint_length == 0)
+ codepoint_length = 1;
+
+ index += 1;
+ byte_index += codepoint_length;
+ }
+
+ if(byte_index < str.size()) {
+ str.erase(byte_index);
+ str += "...";
+ }
+ }
+
+ static bool is_hex_num(char c) {
+ return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
+ }
+
+ static bool contains_non_hex_number(const char *str) {
+ bool hex_start = false;
+ size_t len = strlen(str);
+ if(len >= 2 && memcmp(str, "0x", 2) == 0) {
+ str += 2;
+ len -= 2;
+ hex_start = true;
+ }
+
+ bool is_hex = false;
+ for(size_t i = 0; i < len; ++i) {
+ char c = str[i];
+ if(c == '\0')
+ return false;
+ if(!is_hex_num(c))
+ return true;
+ if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
+ is_hex = true;
+ }
+
+ return is_hex && !hex_start;
+ }
+
+ static bool is_number(const char *str) {
+ const char *p = str;
+ while(*p) {
+ char c = *p;
+ if(c < '0' || c > '9')
+ return false;
+ ++p;
+ }
+ return true;
+ }
+
+ static bool is_capture_target_monitor(const char *capture_target) {
+ return strcmp(capture_target, "window") != 0 && strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target);
+ }
+
+ static std::string capture_target_get_notification_name(const char *capture_target) {
+ std::string result;
+ if(is_capture_target_monitor(capture_target)) {
+ result = "this monitor";
+ } else if(is_number(capture_target)) {
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ int64_t window_id = None;
+ sscanf(capture_target, "%" PRIi64, &window_id);
+
+ const std::optional<std::string> window_title = get_window_title(display, window_id);
+ if(window_title) {
+ result = strip(window_title.value());
+ truncate_string(result, 20);
+ result = "window \"" + result + "\"";
+ } else {
+ result = std::string("window ") + capture_target;
+ }
+ } else {
+ result = capture_target;
+ }
+ return result;
+ }
+
+ static std::string get_valid_monitor_x11(const std::string &target_monitor_name, const std::vector<Monitor> &monitors) {
+ std::string target_monitor_name_clean = target_monitor_name;
+ if(starts_with(target_monitor_name_clean, "HDMI-A"))
+ target_monitor_name_clean.replace(0, 6, "HDMI");
+
+ for(const Monitor &monitor : monitors) {
+ std::string monitor_name_clean = monitor.name;
+ if(starts_with(monitor_name_clean, "HDMI-A"))
+ monitor_name_clean.replace(0, 6, "HDMI");
+
+ if(target_monitor_name_clean == monitor_name_clean)
+ return monitor.name;
+ }
+
+ return "";
+ }
+
+ static std::string get_focused_monitor_by_cursor(CursorTracker *cursor_tracker, const GsrInfo &gsr_info, const std::vector<Monitor> &x11_monitors) {
+ std::optional<CursorInfo> cursor_info;
+ if(cursor_tracker) {
+ cursor_tracker->update();
+ cursor_info = cursor_tracker->get_latest_cursor_info();
+ }
+
+ std::string focused_monitor_name;
+ if(cursor_info) {
+ focused_monitor_name = std::move(cursor_info->monitor_name);
+ } else {
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ Window x11_cursor_window = None;
+ mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window);
+
+ const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display);
+ const Monitor *focused_monitor = find_monitor_at_position(x11_monitors, monitor_position_query_value);
+ if(focused_monitor)
+ focused_monitor_name = focused_monitor->name;
+ }
+
+ return focused_monitor_name;
+ }
+
+ void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) {
char timeout_seconds_str[32];
snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds);
const std::string icon_color_str = color_to_hex_str(icon_color);
const std::string bg_color_str = color_to_hex_str(bg_color);
- const char *notification_args[12] = {
+ const char *notification_args[14] = {
"gsr-notify", "--text", str, "--timeout", timeout_seconds_str,
"--icon-color", icon_color_str.c_str(), "--bg-color", bg_color_str.c_str(),
};
+ int arg_index = 9;
const char *notification_type_str = notification_type_to_string(notification_type);
if(notification_type_str) {
- notification_args[9] = "--icon";
- notification_args[10] = notification_type_str;
- notification_args[11] = nullptr;
- } else {
- notification_args[9] = nullptr;
+ notification_args[arg_index++] = "--icon";
+ notification_args[arg_index++] = notification_type_str;
}
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ std::string monitor_name;
+ const auto monitors = get_monitors(display);
+
+ if(capture_target && is_capture_target_monitor(capture_target))
+ monitor_name = capture_target;
+ else
+ monitor_name = get_focused_monitor_by_cursor(cursor_tracker.get(), gsr_info, monitors);
+
+ monitor_name = get_valid_monitor_x11(monitor_name, monitors);
+ if(!monitor_name.empty()) {
+ notification_args[arg_index++] = "--monitor";
+ notification_args[arg_index++] = monitor_name.c_str();
+ } else if(!monitors.empty()) {
+ notification_args[arg_index++] = "--monitor";
+ notification_args[arg_index++] = monitors.front().name.c_str();
+ }
+
+ notification_args[arg_index++] = nullptr;
+
if(notification_process > 0) {
kill(notification_process, SIGKILL);
int status = 0;
@@ -1381,6 +1686,19 @@ namespace gsr {
do_exit = true;
}
+ void Overlay::go_back_to_old_ui() {
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ if(inside_flatpak)
+ exit_reason = "back-to-old-ui";
+ else
+ exit_reason = "exit";
+
+ const char *args[] = { "systemctl", "disable", "--user", "gpu-screen-recorder-ui", nullptr };
+ std::string stdout_str;
+ exec_program_on_host_get_stdout(args, stdout_str);
+ exit();
+ }
+
const Config& Overlay::get_config() const {
return config;
}
@@ -1437,17 +1755,12 @@ namespace gsr {
return result;
}
- static void truncate_string(std::string &str, int max_length) {
- if((int)str.size() > max_length)
- str.replace(str.begin() + max_length, str.end(), "...");
- }
-
void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const std::string video_filename = filepath_get_filename(video_filepath);
- const Window gsr_ui_window = window ? window->get_system_handle() : None;
+ const Window gsr_ui_window = window ? (Window)window->get_system_handle() : None;
std::string focused_window_name = get_window_name_at_cursor_position(display, gsr_ui_window);
if(focused_window_name.empty())
focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED);
@@ -1463,44 +1776,75 @@ namespace gsr {
rename(video_filepath, new_video_filepath.c_str());
truncate_string(focused_window_name, 20);
- std::string text;
+ const char *capture_target = nullptr;
+ char msg[512];
+
switch(notification_type) {
case NotificationType::RECORD: {
if(!config.record_config.show_video_saved_notifications)
return;
- text = "Saved recording to '" + focused_window_name + "/" + video_filename + "'";
+
+ snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str());
+ capture_target = recording_capture_target.c_str();
break;
}
case NotificationType::REPLAY: {
if(!config.replay_config.show_replay_saved_notifications)
return;
- text = "Saved replay to '" + focused_window_name + "/" + video_filename + "'";
+
+ char duration[32];
+ if(replay_save_duration_min > 0)
+ snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min);
+ else
+ snprintf(duration, sizeof(duration), " ");
+
+ snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str());
+ capture_target = recording_capture_target.c_str();
break;
}
case NotificationType::SCREENSHOT: {
if(!config.screenshot_config.show_screenshot_saved_notifications)
return;
- text = "Saved screenshot to '" + focused_window_name + "/" + video_filename + "'";
+
+ snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str(), focused_window_name.c_str());
+ capture_target = screenshot_capture_target.c_str();
break;
}
case NotificationType::NONE:
case NotificationType::STREAM:
break;
}
- show_notification(text.c_str(), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type);
+ show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type, capture_target);
+ }
+
+ static NotificationType recording_status_to_notification_type(RecordingStatus recording_status) {
+ switch(recording_status) {
+ case RecordingStatus::NONE: return NotificationType::NONE;
+ case RecordingStatus::REPLAY: return NotificationType::REPLAY;
+ case RecordingStatus::RECORD: return NotificationType::RECORD;
+ case RecordingStatus::STREAM: return NotificationType::STREAM;
+ }
+ return NotificationType::NONE;
}
void Overlay::on_replay_saved(const char *replay_saved_filepath) {
replay_save_show_notification = false;
if(config.replay_config.save_video_in_game_folder) {
save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY);
- } else {
- const std::string text = "Saved replay to '" + filepath_get_filename(replay_saved_filepath) + "'";
- show_notification(text.c_str(), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ } else if(config.replay_config.show_replay_saved_notifications) {
+ char duration[32];
+ if(replay_save_duration_min > 0)
+ snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min);
+ else
+ snprintf(duration, sizeof(duration), " ");
+
+ char msg[512];
+ snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
+ show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
}
}
- void Overlay::update_gsr_replay_save() {
+ void Overlay::process_gsr_output() {
if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) {
replay_save_show_notification = false;
show_notification("Saving replay, this might take some time", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
@@ -1508,21 +1852,71 @@ namespace gsr {
if(gpu_screen_recorder_process_output_file) {
char buffer[1024];
- char *replay_saved_filepath = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file);
- if(!replay_saved_filepath || replay_saved_filepath[0] == '\0')
+ char *line = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file);
+ if(!line || line[0] == '\0')
+ return;
+
+ const int line_len = strlen(line);
+ if(line[line_len - 1] == '\n')
+ line[line_len - 1] = '\0';
+
+ if(starts_with({line, (size_t)line_len}, "Error: ")) {
+ show_notification(line + 7, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), recording_status_to_notification_type(recording_status));
return;
+ }
- const int line_len = strlen(replay_saved_filepath);
- if(replay_saved_filepath[line_len - 1] == '\n')
- replay_saved_filepath[line_len - 1] = '\0';
+ const std::string video_filepath = filepath_get_filename(line);
+ if(starts_with(video_filepath, "Video_")) {
+ on_stop_recording(0, line);
+ return;
+ }
- on_replay_saved(replay_saved_filepath);
+ switch(recording_status) {
+ case RecordingStatus::NONE:
+ break;
+ case RecordingStatus::REPLAY:
+ on_replay_saved(line);
+ break;
+ case RecordingStatus::RECORD:
+ break;
+ case RecordingStatus::STREAM:
+ break;
+ }
} else if(gpu_screen_recorder_process_output_fd > 0) {
char buffer[1024];
read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer));
}
}
+ void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) {
+ fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
+ if(exit_code == 50) {
+ show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
+ } else if(exit_code == 60) {
+ show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
+ } else {
+ const char *prefix = "";
+ switch(notification_type) {
+ case NotificationType::NONE:
+ case NotificationType::SCREENSHOT:
+ break;
+ case NotificationType::RECORD:
+ prefix = "Failed to start/save recording";
+ break;
+ case NotificationType::REPLAY:
+ prefix = "Replay stopped because of an error";
+ break;
+ case NotificationType::STREAM:
+ prefix = "Streaming stopped because of an error";
+ break;
+ }
+
+ char msg[256];
+ snprintf(msg, sizeof(msg), "%s. Verify if settings are correct", prefix);
+ show_notification(msg, notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
+ }
+ }
+
void Overlay::update_gsr_process_status() {
if(gpu_screen_recorder_process <= 0)
return;
@@ -1543,19 +1937,19 @@ namespace gsr {
case RecordingStatus::NONE:
break;
case RecordingStatus::REPLAY: {
+ replay_save_duration_min = 0;
update_ui_replay_stopped();
if(exit_code == 0) {
if(config.replay_config.show_replay_stopped_notifications)
show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
} else {
- fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Replay stopped because of an error. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
+ on_gsr_process_error(exit_code, NotificationType::REPLAY);
}
break;
}
case RecordingStatus::RECORD: {
update_ui_recording_stopped();
- on_stop_recording(exit_code);
+ on_stop_recording(exit_code, record_filepath);
break;
}
case RecordingStatus::STREAM: {
@@ -1564,8 +1958,7 @@ namespace gsr {
if(config.streaming_config.show_streaming_stopped_notifications)
show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
} else {
- fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Streaming stopped because of an error. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
+ on_gsr_process_error(exit_code, NotificationType::STREAM);
}
break;
}
@@ -1592,9 +1985,10 @@ namespace gsr {
if(exit_code == 0) {
if(config.screenshot_config.save_screenshot_in_game_folder) {
save_video_in_current_game_directory(screenshot_filepath.c_str(), NotificationType::SCREENSHOT);
- } else {
- const std::string text = "Saved screenshot to '" + filepath_get_filename(screenshot_filepath.c_str()) + "'";
- show_notification(text.c_str(), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT);
+ } else if(config.screenshot_config.show_screenshot_saved_notifications) {
+ char msg[512];
+ snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str());
+ show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
}
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code);
@@ -1604,28 +1998,25 @@ namespace gsr {
gpu_screen_recorder_screenshot_process = -1;
}
- static bool starts_with(std::string_view str, const char *substr) {
- size_t len = strlen(substr);
- return str.size() >= len && memcmp(str.data(), substr, len) == 0;
- }
-
- static bool are_all_audio_tracks_available_to_capture(const std::vector<std::string> &audio_tracks) {
+ static bool are_all_audio_tracks_available_to_capture(const std::vector<AudioTrack> &audio_tracks) {
const auto audio_devices = get_audio_devices();
- for(const std::string &audio_track : audio_tracks) {
- std::string_view audio_track_name(audio_track.c_str());
- const bool is_app_audio = starts_with(audio_track_name, "app:");
- if(is_app_audio)
- continue;
+ for(const AudioTrack &audio_track : audio_tracks) {
+ for(const std::string &audio_input : audio_track.audio_inputs) {
+ std::string_view audio_track_name(audio_input.c_str());
+ const bool is_app_audio = starts_with(audio_track_name, "app:");
+ if(is_app_audio)
+ continue;
- if(starts_with(audio_track_name, "device:"))
- audio_track_name.remove_prefix(7);
+ if(starts_with(audio_track_name, "device:"))
+ audio_track_name.remove_prefix(7);
- auto it = std::find_if(audio_devices.begin(), audio_devices.end(), [&](const auto &audio_device) {
- return audio_device.name == audio_track_name;
- });
- if(it == audio_devices.end()) {
- //fprintf(stderr, "Audio not ready\n");
- return false;
+ auto it = std::find_if(audio_devices.begin(), audio_devices.end(), [&](const auto &audio_device) {
+ return audio_device.name == audio_track_name;
+ });
+ if(it == audio_devices.end()) {
+ //fprintf(stderr, "Audio not ready\n");
+ return false;
+ }
}
}
return true;
@@ -1649,14 +2040,14 @@ namespace gsr {
Display *display = (Display*)context->connection;
const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED);
- if(window && focused_window == window->get_system_handle())
+ if(window && focused_window == (Window)window->get_system_handle())
return;
const bool prev_focused_window_is_fullscreen = focused_window_is_fullscreen;
focused_window_is_fullscreen = focused_window != 0 && window_is_fullscreen(display, focused_window);
if(focused_window_is_fullscreen != prev_focused_window_is_fullscreen) {
if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) {
- if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks))
+ if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list))
on_press_start_replay(false, false);
} else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) {
on_press_start_replay(true, false);
@@ -1673,7 +2064,7 @@ namespace gsr {
power_supply_connected = power_supply_online_filepath.empty() || power_supply_is_connected(power_supply_online_filepath.c_str());
if(power_supply_connected != prev_power_supply_status) {
if(recording_status == RecordingStatus::NONE && power_supply_connected) {
- if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks))
+ if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list))
on_press_start_replay(false, false);
} else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) {
on_press_start_replay(false, false);
@@ -1685,22 +2076,24 @@ namespace gsr {
if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP || recording_status != RecordingStatus::NONE || !try_replay_startup)
return;
- if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks))
+ if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list))
on_press_start_replay(true, false);
}
- void Overlay::on_stop_recording(int exit_code) {
+ void Overlay::on_stop_recording(int exit_code, const std::string &video_filepath) {
if(exit_code == 0) {
if(config.record_config.save_video_in_game_folder) {
- save_video_in_current_game_directory(record_filepath.c_str(), NotificationType::RECORD);
- } else {
- const std::string text = "Saved recording to '" + filepath_get_filename(record_filepath.c_str()) + "'";
- show_notification(text.c_str(), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ save_video_in_current_game_directory(video_filepath.c_str(), NotificationType::RECORD);
+ } else if(config.record_config.show_video_saved_notifications) {
+ char msg[512];
+ snprintf(msg, sizeof(msg), "Saved a recording of %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
+ show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
}
} else {
- fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Failed to start/save recording. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
+ on_gsr_process_error(exit_code, NotificationType::RECORD);
}
+ update_ui_recording_stopped();
+ replay_recording = false;
}
void Overlay::update_ui_recording_paused() {
@@ -1729,6 +2122,7 @@ namespace gsr {
record_dropdown_button_ptr->set_activated(true);
record_dropdown_button_ptr->set_description("Recording");
record_dropdown_button_ptr->set_item_icon("start", &get_theme().stop_texture);
+ record_dropdown_button_ptr->set_item_enabled("pause", recording_status == RecordingStatus::RECORD);
}
void Overlay::update_ui_recording_stopped() {
@@ -1742,7 +2136,9 @@ namespace gsr {
record_dropdown_button_ptr->set_item_label("pause", "Pause");
record_dropdown_button_ptr->set_item_icon("pause", &get_theme().pause_texture);
+ record_dropdown_button_ptr->set_item_enabled("pause", false);
paused = false;
+ replay_recording = false;
}
void Overlay::update_ui_streaming_started() {
@@ -1763,6 +2159,7 @@ namespace gsr {
stream_dropdown_button_ptr->set_activated(false);
stream_dropdown_button_ptr->set_description("Not streaming");
stream_dropdown_button_ptr->set_item_icon("start", &get_theme().play_texture);
+ update_ui_recording_stopped();
}
void Overlay::update_ui_replay_started() {
@@ -1773,6 +2170,9 @@ namespace gsr {
replay_dropdown_button_ptr->set_activated(true);
replay_dropdown_button_ptr->set_description("On");
replay_dropdown_button_ptr->set_item_icon("start", &get_theme().stop_texture);
+ replay_dropdown_button_ptr->set_item_enabled("save", true);
+ replay_dropdown_button_ptr->set_item_enabled("save_1_min", true);
+ replay_dropdown_button_ptr->set_item_enabled("save_10_min", true);
}
void Overlay::update_ui_replay_stopped() {
@@ -1783,6 +2183,10 @@ namespace gsr {
replay_dropdown_button_ptr->set_activated(false);
replay_dropdown_button_ptr->set_description("Off");
replay_dropdown_button_ptr->set_item_icon("start", &get_theme().play_texture);
+ replay_dropdown_button_ptr->set_item_enabled("save", false);
+ replay_dropdown_button_ptr->set_item_enabled("save_1_min", false);
+ replay_dropdown_button_ptr->set_item_enabled("save_10_min", false);
+ update_ui_recording_stopped();
}
static std::string get_date_str() {
@@ -1804,29 +2208,43 @@ namespace gsr {
return container;
}
- static std::vector<std::string> create_audio_tracks_real_names(const std::vector<std::string> &audio_tracks, bool application_audio_invert, const GsrInfo &gsr_info) {
+ static std::vector<std::string> create_audio_tracks_cli_args(const std::vector<AudioTrack> &audio_tracks, const GsrInfo &gsr_info) {
std::vector<std::string> result;
- for(const std::string &audio_track : audio_tracks) {
- std::string audio_track_name = audio_track;
- const bool is_app_audio = starts_with(audio_track_name, "app:");
- if(is_app_audio && !gsr_info.system_info.supports_app_audio)
- continue;
+ result.reserve(audio_tracks.size());
- if(is_app_audio && application_audio_invert)
- audio_track_name.replace(0, 4, "app-inverse:");
+ for(const AudioTrack &audio_track : audio_tracks) {
+ std::string audio_track_merged;
+ int num_app_audio = 0;
- result.push_back(std::move(audio_track_name));
- }
- return result;
- }
+ for(const std::string &audio_input_name : audio_track.audio_inputs) {
+ std::string new_audio_input_name = audio_input_name;
+ const bool is_app_audio = starts_with(new_audio_input_name, "app:");
+ if(is_app_audio && !gsr_info.system_info.supports_app_audio)
+ continue;
- static std::string merge_audio_tracks(const std::vector<std::string> &audio_tracks) {
- std::string result;
- for(size_t i = 0; i < audio_tracks.size(); ++i) {
- if(i > 0)
- result += "|";
- result += audio_tracks[i];
+ if(is_app_audio && audio_track.application_audio_invert)
+ new_audio_input_name.replace(0, 4, "app-inverse:");
+
+ if(is_app_audio)
+ ++num_app_audio;
+
+ if(!audio_track_merged.empty())
+ audio_track_merged += "|";
+
+ audio_track_merged += new_audio_input_name;
+ }
+
+ if(num_app_audio == 0 && audio_track.application_audio_invert) {
+ if(!audio_track_merged.empty())
+ audio_track_merged += "|";
+
+ audio_track_merged += "app-inverse:";
+ }
+
+ if(!audio_track_merged.empty())
+ result.push_back(std::move(audio_track_merged));
}
+
return result;
}
@@ -1841,7 +2259,7 @@ namespace gsr {
args.push_back(region_str);
}
- static void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, const std::string &audio_devices_merged, char *region_str, int region_str_size, const RegionSelector &region_selector) {
+ static void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const RegionSelector &region_selector) {
if(record_options.video_quality == "custom") {
args.push_back("-bm");
args.push_back("cbr");
@@ -1857,16 +2275,9 @@ namespace gsr {
args.push_back(region);
}
- if(record_options.merge_audio_tracks) {
- if(!audio_devices_merged.empty()) {
- args.push_back("-a");
- args.push_back(audio_devices_merged.c_str());
- }
- } else {
- for(const std::string &audio_track : audio_tracks) {
- args.push_back("-a");
- args.push_back(audio_track.c_str());
- }
+ for(const std::string &audio_track : audio_tracks) {
+ args.push_back("-a");
+ args.push_back(audio_track.c_str());
}
if(record_options.restore_portal_session) {
@@ -1878,15 +2289,17 @@ namespace gsr {
add_region_command(args, region_str, region_str_size, region_selector);
}
- static bool validate_capture_target(const GsrInfo &gsr_info, const std::string &capture_target) {
- const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
- // TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number)
- if(capture_target == "region") {
- return capture_options.region;
+ static bool validate_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
+ if(capture_target == "window") {
+ return capture_options.window;
} else if(capture_target == "focused") {
return capture_options.focused;
+ } else if(capture_target == "region") {
+ return capture_options.region;
} else if(capture_target == "portal") {
return capture_options.portal;
+ } else if(capture_target == "focused_monitor") {
+ return !capture_options.monitors.empty();
} else {
for(const GsrMonitor &monitor : capture_options.monitors) {
if(capture_target == monitor.name)
@@ -1896,17 +2309,139 @@ namespace gsr {
}
}
+ static std::string get_valid_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
+ std::string capture_target_clean = capture_target;
+ if(starts_with(capture_target_clean, "HDMI-A"))
+ capture_target_clean.replace(0, 6, "HDMI");
+
+ for(const GsrMonitor &monitor : capture_options.monitors) {
+ std::string monitor_name_clean = monitor.name;
+ if(starts_with(monitor_name_clean, "HDMI-A"))
+ monitor_name_clean.replace(0, 6, "HDMI");
+
+ if(capture_target_clean == monitor_name_clean)
+ return monitor.name;
+ }
+
+ return "";
+ }
+
+ std::string Overlay::get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
+ if(capture_target == "window") {
+ return std::to_string(window_selector.get_selection());
+ } else if(capture_target == "focused_monitor") {
+ std::optional<CursorInfo> cursor_info;
+ if(cursor_tracker) {
+ cursor_tracker->update();
+ cursor_info = cursor_tracker->get_latest_cursor_info();
+ }
+
+ std::string focused_monitor_name;
+ if(cursor_info) {
+ focused_monitor_name = std::move(cursor_info->monitor_name);
+ } else {
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+ focused_monitor_name = get_focused_monitor_by_cursor(cursor_tracker.get(), gsr_info, get_monitors(display));
+ }
+
+ focused_monitor_name = get_valid_capture_target(focused_monitor_name, capture_options);
+ if(!focused_monitor_name.empty())
+ return focused_monitor_name;
+ else if(!capture_options.monitors.empty())
+ return capture_options.monitors.front().name;
+ else
+ return "";
+ } else {
+ return capture_target;
+ }
+ }
+
+ void Overlay::prepare_gsr_output_for_reading() {
+ if(gpu_screen_recorder_process_output_fd <= 0)
+ return;
+
+ const int fdl = fcntl(gpu_screen_recorder_process_output_fd, F_GETFL);
+ fcntl(gpu_screen_recorder_process_output_fd, F_SETFL, fdl | O_NONBLOCK);
+ gpu_screen_recorder_process_output_file = fdopen(gpu_screen_recorder_process_output_fd, "r");
+ if(gpu_screen_recorder_process_output_file)
+ gpu_screen_recorder_process_output_fd = -1;
+ }
+
void Overlay::on_press_save_replay() {
if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0)
return;
+ replay_save_duration_min = 0;
replay_save_show_notification = true;
replay_save_clock.restart();
kill(gpu_screen_recorder_process, SIGUSR1);
}
- bool Overlay::on_press_start_replay(bool disable_notification, bool finished_region_selection) {
- if(region_selector.is_started())
+ void Overlay::on_press_save_replay_1_min_replay() {
+ if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0)
+ return;
+
+ replay_save_duration_min = 1;
+ replay_save_show_notification = true;
+ replay_save_clock.restart();
+ kill(gpu_screen_recorder_process, SIGRTMIN+3);
+ }
+
+ void Overlay::on_press_save_replay_10_min_replay() {
+ if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0)
+ return;
+
+ replay_save_duration_min = 10;
+ replay_save_show_notification = true;
+ replay_save_clock.restart();
+ kill(gpu_screen_recorder_process, SIGRTMIN+5);
+ }
+
+ static const char* switch_video_codec_to_usable_hardware_encoder(const GsrInfo &gsr_info) {
+ if(gsr_info.supported_video_codecs.h264)
+ return "h264";
+ else if(gsr_info.supported_video_codecs.hevc)
+ return "hevc";
+ else if(gsr_info.supported_video_codecs.av1)
+ return "av1";
+ else if(gsr_info.supported_video_codecs.vp8)
+ return "vp8";
+ else if(gsr_info.supported_video_codecs.vp9)
+ return "vp9";
+ return nullptr;
+ }
+
+ static const char* change_container_if_codec_not_supported(const char *video_codec, const char *container) {
+ if(strcmp(video_codec, "vp8") == 0 || strcmp(video_codec, "vp9") == 0) {
+ if(strcmp(container, "webm") != 0 && strcmp(container, "matroska") != 0) {
+ fprintf(stderr, "Warning: container '%s' is not compatible with video codec '%s', using webm container instead\n", container, video_codec);
+ return "webm";
+ }
+ } else if(strcmp(container, "webm") == 0) {
+ fprintf(stderr, "Warning: container webm is not compatible with video codec '%s', using mp4 container instead\n", video_codec);
+ return "mp4";
+ }
+ return container;
+ }
+
+ static void choose_video_codec_and_container_with_fallback(const GsrInfo &gsr_info, const char **video_codec, const char **container, const char **encoder) {
+ *encoder = "gpu";
+ if(strcmp(*video_codec, "h264_software") == 0) {
+ *video_codec = "h264";
+ *encoder = "cpu";
+ } else if(strcmp(*video_codec, "auto") == 0) {
+ *video_codec = switch_video_codec_to_usable_hardware_encoder(gsr_info);
+ if(!*video_codec) {
+ *video_codec = "h264";
+ *encoder = "cpu";
+ }
+ }
+ *container = change_container_if_codec_not_supported(*video_codec, *container);
+ }
+
+ bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
+ if(region_selector.is_started() || window_selector.is_started())
return false;
switch(recording_status) {
@@ -1914,10 +2449,10 @@ namespace gsr {
case RecordingStatus::REPLAY:
break;
case RecordingStatus::RECORD:
- show_notification("Unable to start replay when recording.\nStop recording before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ show_notification("Unable to start replay when recording.\nStop recording before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
return false;
case RecordingStatus::STREAM:
- show_notification("Unable to start replay when streaming.\nStop streaming before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
+ show_notification("Unable to start replay when streaming.\nStop streaming before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
return false;
}
@@ -1925,9 +2460,6 @@ namespace gsr {
replay_save_show_notification = false;
try_replay_startup = false;
- // window->close();
- // usleep(1000 * 50); // 50 milliseconds
-
close_gpu_screen_recorder_output();
if(gpu_screen_recorder_process > 0) {
@@ -1940,6 +2472,7 @@ namespace gsr {
gpu_screen_recorder_process = -1;
recording_status = RecordingStatus::NONE;
+ replay_save_duration_min = 0;
update_ui_replay_stopped();
// TODO: Show this with a slight delay to make sure it doesn't show up in the video
@@ -1949,14 +2482,16 @@ namespace gsr {
return true;
}
- if(!validate_capture_target(gsr_info, config.replay_config.record_options.record_area_option)) {
+ const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
+ recording_capture_target = get_capture_target(config.replay_config.record_options.record_area_option, capture_options);
+ if(!validate_capture_target(recording_capture_target, capture_options)) {
char err_msg[256];
- snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid. Please change capture target in settings", config.replay_config.record_options.record_area_option.c_str());
- show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::REPLAY);
+ snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str());
+ show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
return false;
}
- if(config.replay_config.record_options.record_area_option == "region" && !finished_region_selection) {
+ if(config.replay_config.record_options.record_area_option == "region" && !finished_selection) {
start_region_capture = true;
on_region_selected = [disable_notification, this]() {
on_press_start_replay(disable_notification, true);
@@ -1964,20 +2499,25 @@ namespace gsr {
return false;
}
+ if(config.replay_config.record_options.record_area_option == "window" && !finished_selection) {
+ start_window_capture = true;
+ on_window_selected = [disable_notification, this]() {
+ on_press_start_replay(disable_notification, true);
+ };
+ return false;
+ }
+
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.replay_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate);
const std::string output_directory = config.replay_config.save_directory;
- const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.replay_config.record_options.audio_tracks, config.replay_config.record_options.application_audio_invert, gsr_info);
- const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks);
+ const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode;
const std::string replay_time = std::to_string(config.replay_config.replay_time);
+ const char *container = config.replay_config.container.c_str();
const char *video_codec = config.replay_config.record_options.video_codec.c_str();
const char *encoder = "gpu";
- if(strcmp(video_codec, "h264_software") == 0) {
- video_codec = "h264";
- encoder = "cpu";
- }
+ choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
char size[64];
size[0] = '\0';
@@ -1988,8 +2528,8 @@ namespace gsr {
snprintf(size, sizeof(size), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height);
std::vector<const char*> args = {
- "gpu-screen-recorder", "-w", config.replay_config.record_options.record_area_option.c_str(),
- "-c", config.replay_config.container.c_str(),
+ "gpu-screen-recorder", "-w", recording_capture_target.c_str(),
+ "-c", container,
"-ac", config.replay_config.record_options.audio_codec.c_str(),
"-cursor", config.replay_config.record_options.record_cursor ? "yes" : "no",
"-cr", config.replay_config.record_options.color_range.c_str(),
@@ -2007,24 +2547,31 @@ namespace gsr {
args.push_back("yes");
}
+ if(gsr_info.system_info.gsr_version >= GsrVersion{5, 5, 0}) {
+ args.push_back("-replay-storage");
+ args.push_back(config.replay_config.replay_storage.c_str());
+ }
+
char region_str[128];
- add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, size, audio_tracks_merged, region_str, sizeof(region_str), region_selector);
+ add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector);
+
+ if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
+ args.push_back("-ro");
+ args.push_back(config.record_config.save_directory.c_str());
+ }
args.push_back(nullptr);
gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd);
if(gpu_screen_recorder_process == -1) {
- // TODO: Show notification failed to start
+ show_notification("Failed to launch gpu-screen-recorder to start replay", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
+ return false;
} else {
recording_status = RecordingStatus::REPLAY;
update_ui_replay_started();
}
- const int fdl = fcntl(gpu_screen_recorder_process_output_fd, F_GETFL);
- fcntl(gpu_screen_recorder_process_output_fd, F_SETFL, fdl | O_NONBLOCK);
- gpu_screen_recorder_process_output_file = fdopen(gpu_screen_recorder_process_output_fd, "r");
- if(gpu_screen_recorder_process_output_file)
- gpu_screen_recorder_process_output_fd = -1;
+ prepare_gsr_output_for_reading();
// TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video.
// Make clear to the user that the recording starts after the notification is gone.
@@ -2035,32 +2582,62 @@ namespace gsr {
// TODO: Do not run this is a daemon. Instead get the pid and when launching another notification close the current notification
// program and start another one. This can also be used to check when the notification has finished by checking with waitpid NOWAIT
// to see when the program has exit.
- if(!disable_notification && config.replay_config.show_replay_started_notifications)
- show_notification("Replay has started", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY);
+ if(!disable_notification && config.replay_config.show_replay_started_notifications) {
+ char msg[256];
+ snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
+ show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
+ }
return true;
}
- void Overlay::on_press_start_record(bool finished_region_selection) {
- if(region_selector.is_started())
+ void Overlay::on_press_start_record(bool finished_selection) {
+ if(region_selector.is_started() || window_selector.is_started())
return;
switch(recording_status) {
case RecordingStatus::NONE:
case RecordingStatus::RECORD:
break;
- case RecordingStatus::REPLAY:
- show_notification("Unable to start recording when replay is turned on.\nTurn off replay before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ case RecordingStatus::REPLAY: {
+ if(gpu_screen_recorder_process <= 0)
+ return;
+
+ if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
+ if(!replay_recording) {
+ if(config.record_config.show_recording_started_notifications)
+ show_notification("Started recording in the replay session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
+ update_ui_recording_started();
+ }
+ replay_recording = true;
+ kill(gpu_screen_recorder_process, SIGRTMIN);
+ } else {
+ show_notification("Unable to start recording when replay is turned on.\nTurn off replay before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), get_color_theme().tint_color, NotificationType::REPLAY);
+ }
return;
- case RecordingStatus::STREAM:
- show_notification("Unable to start recording when streaming.\nStop streaming before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
+ }
+ case RecordingStatus::STREAM: {
+ if(gpu_screen_recorder_process <= 0)
+ return;
+
+ if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
+ if(!replay_recording) {
+ if(config.record_config.show_recording_started_notifications)
+ show_notification("Started recording in the streaming session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
+ update_ui_recording_started();
+ }
+ replay_recording = true;
+ kill(gpu_screen_recorder_process, SIGRTMIN);
+ } else {
+ show_notification("Unable to start recording when streaming.\nStop streaming before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), get_color_theme().tint_color, NotificationType::STREAM);
+ }
return;
+ }
}
paused = false;
- // window->close();
- // usleep(1000 * 50); // 50 milliseconds
+ close_gpu_screen_recorder_output();
if(gpu_screen_recorder_process > 0) {
kill(gpu_screen_recorder_process, SIGINT);
@@ -2072,7 +2649,7 @@ namespace gsr {
int exit_code = -1;
if(WIFEXITED(status))
exit_code = WEXITSTATUS(status);
- on_stop_recording(exit_code);
+ on_stop_recording(exit_code, record_filepath);
}
gpu_screen_recorder_process = -1;
@@ -2082,14 +2659,16 @@ namespace gsr {
return;
}
- if(!validate_capture_target(gsr_info, config.record_config.record_options.record_area_option)) {
+ const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
+ recording_capture_target = get_capture_target(config.record_config.record_options.record_area_option, capture_options);
+ if(!validate_capture_target(config.record_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
- snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid. Please change capture target in settings", config.record_config.record_options.record_area_option.c_str());
- show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD);
+ snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str());
+ show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
return;
}
- if(config.record_config.record_options.record_area_option == "region" && !finished_region_selection) {
+ if(config.record_config.record_options.record_area_option == "region" && !finished_selection) {
start_region_capture = true;
on_region_selected = [this]() {
on_press_start_record(true);
@@ -2097,21 +2676,26 @@ namespace gsr {
return;
}
+ if(config.record_config.record_options.record_area_option == "window" && !finished_selection) {
+ start_window_capture = true;
+ on_window_selected = [this]() {
+ on_press_start_record(true);
+ };
+ return;
+ }
+
record_filepath.clear();
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.record_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate);
const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str());
- const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.record_config.record_options.audio_tracks, config.record_config.record_options.application_audio_invert, gsr_info);
- const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks);
+ const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode;
+ const char *container = config.record_config.container.c_str();
const char *video_codec = config.record_config.record_options.video_codec.c_str();
const char *encoder = "gpu";
- if(strcmp(video_codec, "h264_software") == 0) {
- video_codec = "h264";
- encoder = "cpu";
- }
+ choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
char size[64];
size[0] = '\0';
@@ -2122,8 +2706,8 @@ namespace gsr {
snprintf(size, sizeof(size), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height);
std::vector<const char*> args = {
- "gpu-screen-recorder", "-w", config.record_config.record_options.record_area_option.c_str(),
- "-c", config.record_config.container.c_str(),
+ "gpu-screen-recorder", "-w", recording_capture_target.c_str(),
+ "-c", container,
"-ac", config.record_config.record_options.audio_codec.c_str(),
"-cursor", config.record_config.record_options.record_cursor ? "yes" : "no",
"-cr", config.record_config.record_options.color_range.c_str(),
@@ -2136,27 +2720,33 @@ namespace gsr {
};
char region_str[128];
- add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, size, audio_tracks_merged, region_str, sizeof(region_str), region_selector);
+ add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector);
args.push_back(nullptr);
record_filepath = output_file;
- gpu_screen_recorder_process = exec_program(args.data(), nullptr);
+ gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd);
if(gpu_screen_recorder_process == -1) {
- // TODO: Show notification failed to start
+ show_notification("Failed to launch gpu-screen-recorder to start recording", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
+ return;
} else {
recording_status = RecordingStatus::RECORD;
update_ui_recording_started();
}
+ prepare_gsr_output_for_reading();
+
// TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video.
// Make clear to the user that the recording starts after the notification is gone.
// Maybe have the option in notification to show timer until its getting hidden, then the notification can say:
// Starting recording in 3...
// 2...
// 1...
- if(config.record_config.show_recording_started_notifications)
- show_notification("Recording has started", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
+ if(config.record_config.show_recording_started_notifications) {
+ char msg[256];
+ snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
+ show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
+ }
}
static std::string streaming_get_url(const Config &config) {
@@ -2167,6 +2757,9 @@ namespace gsr {
} else if(config.streaming_config.streaming_service == "youtube") {
url += "rtmp://a.rtmp.youtube.com/live2/";
url += config.streaming_config.youtube.stream_key;
+ } else if(config.streaming_config.streaming_service == "rumble") {
+ url += "rtmp://rtmp.rumble.com/live/";
+ url += config.streaming_config.rumble.stream_key;
} else if(config.streaming_config.streaming_service == "custom") {
url = config.streaming_config.custom.url;
if(url.size() >= 7 && strncmp(url.c_str(), "rtmp://", 7) == 0)
@@ -2191,8 +2784,8 @@ namespace gsr {
return url;
}
- void Overlay::on_press_start_stream(bool finished_region_selection) {
- if(region_selector.is_started())
+ void Overlay::on_press_start_stream(bool finished_selection) {
+ if(region_selector.is_started() || window_selector.is_started())
return;
switch(recording_status) {
@@ -2200,17 +2793,16 @@ namespace gsr {
case RecordingStatus::STREAM:
break;
case RecordingStatus::REPLAY:
- show_notification("Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ show_notification("Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
return;
case RecordingStatus::RECORD:
- show_notification("Unable to start streaming when recording.\nStop recording before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ show_notification("Unable to start streaming when recording.\nStop recording before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
return;
}
paused = false;
- // window->close();
- // usleep(1000 * 50); // 50 milliseconds
+ close_gpu_screen_recorder_output();
if(gpu_screen_recorder_process > 0) {
kill(gpu_screen_recorder_process, SIGINT);
@@ -2230,14 +2822,16 @@ namespace gsr {
return;
}
- if(!validate_capture_target(gsr_info, config.streaming_config.record_options.record_area_option)) {
+ const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
+ recording_capture_target = get_capture_target(config.streaming_config.record_options.record_area_option, capture_options);
+ if(!validate_capture_target(config.streaming_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
- snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid. Please change capture target in settings", config.streaming_config.record_options.record_area_option.c_str());
- show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::STREAM);
+ snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str());
+ show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
return;
}
- if(config.streaming_config.record_options.record_area_option == "region" && !finished_region_selection) {
+ if(config.streaming_config.record_options.record_area_option == "region" && !finished_selection) {
start_region_capture = true;
on_region_selected = [this]() {
on_press_start_stream(true);
@@ -2245,22 +2839,29 @@ namespace gsr {
return;
}
+ if(config.streaming_config.record_options.record_area_option == "window" && !finished_selection) {
+ start_window_capture = true;
+ on_window_selected = [this]() {
+ on_press_start_stream(true);
+ };
+ return;
+ }
+
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.streaming_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.streaming_config.record_options.video_bitrate);
- const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.streaming_config.record_options.audio_tracks, config.streaming_config.record_options.application_audio_invert, gsr_info);
- const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks);
+ std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.streaming_config.record_options.audio_tracks_list, gsr_info);
+ // This isn't possible unless the user modified the config file manually,
+ // But we check it anyways as streaming on some sites can fail if there is more than one audio track
+ if(audio_tracks.size() > 1)
+ audio_tracks.resize(1);
const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode;
+ const char *container = "flv";
+ if(config.streaming_config.streaming_service == "custom")
+ container = config.streaming_config.custom.container.c_str();
const char *video_codec = config.streaming_config.record_options.video_codec.c_str();
const char *encoder = "gpu";
- if(strcmp(video_codec, "h264_software") == 0) {
- video_codec = "h264";
- encoder = "cpu";
- }
-
- std::string container = "flv";
- if(config.streaming_config.streaming_service == "custom")
- container = config.streaming_config.custom.container;
+ choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
const std::string url = streaming_get_url(config);
@@ -2273,8 +2874,8 @@ namespace gsr {
snprintf(size, sizeof(size), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height);
std::vector<const char*> args = {
- "gpu-screen-recorder", "-w", config.streaming_config.record_options.record_area_option.c_str(),
- "-c", container.c_str(),
+ "gpu-screen-recorder", "-w", recording_capture_target.c_str(),
+ "-c", container,
"-ac", config.streaming_config.record_options.audio_codec.c_str(),
"-cursor", config.streaming_config.record_options.record_cursor ? "yes" : "no",
"-cr", config.streaming_config.record_options.color_range.c_str(),
@@ -2285,20 +2886,27 @@ namespace gsr {
"-o", url.c_str()
};
- config.streaming_config.record_options.merge_audio_tracks = true;
char region_str[128];
- add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, size, audio_tracks_merged, region_str, sizeof(region_str), region_selector);
+ add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector);
+
+ if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
+ args.push_back("-ro");
+ args.push_back(config.record_config.save_directory.c_str());
+ }
args.push_back(nullptr);
- gpu_screen_recorder_process = exec_program(args.data(), nullptr);
+ gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd);
if(gpu_screen_recorder_process == -1) {
- // TODO: Show notification failed to start
+ show_notification("Failed to launch gpu-screen-recorder to start streaming", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
+ return;
} else {
recording_status = RecordingStatus::STREAM;
update_ui_streaming_started();
}
+ prepare_gsr_output_for_reading();
+
// TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video.
// Make clear to the user that the recording starts after the notification is gone.
// Maybe have the option in notification to show timer until its getting hidden, then the notification can say:
@@ -2308,12 +2916,15 @@ namespace gsr {
// TODO: Do not run this is a daemon. Instead get the pid and when launching another notification close the current notification
// program and start another one. This can also be used to check when the notification has finished by checking with waitpid NOWAIT
// to see when the program has exit.
- if(config.streaming_config.show_streaming_started_notifications)
- show_notification("Streaming has started", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM);
+ if(config.streaming_config.show_streaming_started_notifications) {
+ char msg[256];
+ snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
+ show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str());
+ }
}
- void Overlay::on_press_take_screenshot(bool finished_region_selection, bool force_region_capture) {
- if(region_selector.is_started())
+ void Overlay::on_press_take_screenshot(bool finished_selection, bool force_region_capture) {
+ if(region_selector.is_started() || window_selector.is_started())
return;
if(gpu_screen_recorder_screenshot_process > 0) {
@@ -2323,14 +2934,16 @@ namespace gsr {
const bool region_capture = config.screenshot_config.record_area_option == "region" || force_region_capture;
const char *record_area_option = region_capture ? "region" : config.screenshot_config.record_area_option.c_str();
- if(!validate_capture_target(gsr_info, record_area_option)) {
+ const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
+ screenshot_capture_target = get_capture_target(record_area_option, capture_options);
+ if(!validate_capture_target(record_area_option, capture_options)) {
char err_msg[256];
- snprintf(err_msg, sizeof(err_msg), "Failed to take a screenshot, capture target \"%s\" is invalid. Please change capture target in settings", record_area_option);
- show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::SCREENSHOT);
+ snprintf(err_msg, sizeof(err_msg), "Failed to take a screenshot, capture target \"%s\" is invalid.\nPlease change capture target in settings", screenshot_capture_target.c_str());
+ show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT);
return;
}
- if(region_capture && !finished_region_selection) {
+ if(region_capture && !finished_selection) {
start_region_capture = true;
on_region_selected = [this, force_region_capture]() {
usleep(200 * 1000); // Hack: wait 0.2 seconds before taking a screenshot to allow user to move cursor away. TODO: Remove this
@@ -2339,11 +2952,19 @@ namespace gsr {
return;
}
+ if(config.screenshot_config.record_area_option == "window" && !finished_selection) {
+ start_window_capture = true;
+ on_window_selected = [this, force_region_capture]() {
+ on_press_take_screenshot(true, force_region_capture);
+ };
+ return;
+ }
+
// TODO: Validate input, fallback to valid values
const std::string output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format
std::vector<const char*> args = {
- "gpu-screen-recorder", "-w", record_area_option,
+ "gpu-screen-recorder", "-w", screenshot_capture_target.c_str(),
"-cursor", config.screenshot_config.record_cursor ? "yes" : "no",
"-v", "no",
"-q", config.screenshot_config.image_quality.c_str(),
@@ -2372,7 +2993,7 @@ namespace gsr {
screenshot_filepath = output_file;
gpu_screen_recorder_screenshot_process = exec_program(args.data(), nullptr);
if(gpu_screen_recorder_screenshot_process == -1) {
- // TODO: Show notification failed to start
+ show_notification("Failed to launch gpu-screen-recorder to take a screenshot", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT);
}
}
@@ -2421,7 +3042,7 @@ namespace gsr {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
- XRaiseWindow(display, window->get_system_handle());
+ XRaiseWindow(display, (Window)window->get_system_handle());
XFlush(display);
}
}
diff --git a/src/Process.cpp b/src/Process.cpp
index 0a62986..c02753a 100644
--- a/src/Process.cpp
+++ b/src/Process.cpp
@@ -130,8 +130,6 @@ namespace gsr {
exit_status = -1;
break;
}
-
- buffer[bytes_read] = '\0';
result.append(buffer, bytes_read);
}
@@ -178,11 +176,21 @@ namespace gsr {
}
}
+ static const char *get_basename(const char *path, int size) {
+ for(int i = size - 1; i >= 0; --i) {
+ if(path[i] == '/')
+ return path + i + 1;
+ }
+ return path;
+ }
+
// |output_buffer| should be at least PATH_MAX in size
bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) {
output_buffer[0] = '\0';
+ const char *arg0_start = NULL;
const char *arg0_end = NULL;
+ int arg0_size = 0;
int fd = open(filepath, O_RDONLY);
if(fd == -1)
return false;
@@ -192,13 +200,16 @@ namespace gsr {
if(bytes_read == -1)
goto err;
- arg0_end = (const char*)memchr(buffer, '\0', bytes_read);
+ arg0_start = buffer;
+ arg0_end = (const char*)memchr(arg0_start, '\0', bytes_read);
if(!arg0_end)
goto err;
- if((arg0_end - buffer) + 1 <= output_buffer_size) {
- memcpy(output_buffer, buffer, arg0_end - buffer);
- output_buffer[arg0_end - buffer] = '\0';
+ arg0_start = get_basename(arg0_start, arg0_end - arg0_start);
+ arg0_size = arg0_end - arg0_start;
+ if(arg0_size + 1 <= output_buffer_size) {
+ memcpy(output_buffer, arg0_start, arg0_size);
+ output_buffer[arg0_size] = '\0';
close(fd);
return true;
}
diff --git a/src/RegionSelector.cpp b/src/RegionSelector.cpp
index 5b7243b..89a0209 100644
--- a/src/RegionSelector.cpp
+++ b/src/RegionSelector.cpp
@@ -208,7 +208,7 @@ namespace gsr {
window_attr.background_pixel = is_wayland ? 0 : border_color_x11;
window_attr.border_pixel = 0;
window_attr.override_redirect = true;
- window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
+ window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
window_attr.colormap = region_window_colormap;
Screen *screen = XDefaultScreenOfDisplay(dpy);
@@ -366,10 +366,6 @@ namespace gsr {
return true;
}
- bool RegionSelector::is_selected() const {
- return selected;
- }
-
bool RegionSelector::take_selection() {
const bool result = selected;
selected = false;
diff --git a/src/Theme.cpp b/src/Theme.cpp
index 2001f7d..2bef3c8 100644
--- a/src/Theme.cpp
+++ b/src/Theme.cpp
@@ -63,73 +63,85 @@ namespace gsr {
if(!theme->title_font_file.load((resources_path + "fonts/NotoSans-Bold.ttf").c_str(), mgl::MemoryMappedFile::LoadOptions{true, false}))
goto error;
- if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str()))
+ if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->settings_texture.load_from_file((resources_path + "images/settings.png").c_str()))
goto error;
- if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str()))
+ if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str()))
+ if(!theme->settings_extra_small_texture.load_from_file((resources_path + "images/settings_extra_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str()))
+ if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str()))
+ if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str()))
+ if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str()))
+ if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str()))
+ if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
+ goto error;
+
+ if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->logo_texture.load_from_file((resources_path + "images/gpu_screen_recorder_logo.png").c_str()))
goto error;
- if(!theme->checkbox_circle_texture.load_from_file((resources_path + "images/checkbox_circle.png").c_str()))
+ if(!theme->checkbox_circle_texture.load_from_file((resources_path + "images/checkbox_circle.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
+ goto error;
+
+ if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str()))
+ if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str()))
+ if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str()))
+ if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str()))
+ if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str()))
+ if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str()))
+ if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, true}))
+ if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->ps4_options_texture.load_from_file((resources_path + "images/ps4_options.png").c_str(), mgl::Texture::LoadOptions{false, false, true}))
+ if(!theme->ps4_options_texture.load_from_file((resources_path + "images/ps4_options.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->ps4_dpad_up_texture.load_from_file((resources_path + "images/ps4_dpad_up.png").c_str(), mgl::Texture::LoadOptions{false, false, true}))
+ if(!theme->ps4_dpad_up_texture.load_from_file((resources_path + "images/ps4_dpad_up.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->ps4_dpad_down_texture.load_from_file((resources_path + "images/ps4_dpad_down.png").c_str(), mgl::Texture::LoadOptions{false, false, true}))
+ if(!theme->ps4_dpad_down_texture.load_from_file((resources_path + "images/ps4_dpad_down.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->ps4_dpad_left_texture.load_from_file((resources_path + "images/ps4_dpad_left.png").c_str(), mgl::Texture::LoadOptions{false, false, true}))
+ if(!theme->ps4_dpad_left_texture.load_from_file((resources_path + "images/ps4_dpad_left.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
+ goto error;
+
+ if(!theme->ps4_dpad_right_texture.load_from_file((resources_path + "images/ps4_dpad_right.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
+ goto error;
+
+ if(!theme->ps4_cross_texture.load_from_file((resources_path + "images/ps4_cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
- if(!theme->ps4_dpad_right_texture.load_from_file((resources_path + "images/ps4_dpad_right.png").c_str(), mgl::Texture::LoadOptions{false, false, true}))
+ if(!theme->ps4_triangle_texture.load_from_file((resources_path + "images/ps4_triangle.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
return true;
diff --git a/src/Utils.cpp b/src/Utils.cpp
index df6db2f..c36a64a 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -22,6 +22,38 @@ namespace gsr {
}
}
+ bool starts_with(std::string_view str, const char *substr) {
+ size_t len = strlen(substr);
+ return str.size() >= len && memcmp(str.data(), substr, len) == 0;
+ }
+
+ bool ends_with(std::string_view str, const char *substr) {
+ size_t len = strlen(substr);
+ return str.size() >= len && memcmp(str.data() + str.size() - len, substr, len) == 0;
+ }
+
+ std::string strip(const std::string &str) {
+ int start_index = 0;
+ int str_len = str.size();
+
+ for(int i = 0; i < str_len; ++i) {
+ if(str[i] != ' ') {
+ start_index += i;
+ str_len -= i;
+ break;
+ }
+ }
+
+ for(int i = str_len - 1; i >= 0; --i) {
+ if(str[i] != ' ') {
+ str_len = i + 1;
+ break;
+ }
+ }
+
+ return str.substr(start_index, str_len);
+ }
+
std::string get_home_dir() {
const char *home_dir = getenv("HOME");
if(!home_dir) {
diff --git a/src/WindowSelector.cpp b/src/WindowSelector.cpp
new file mode 100644
index 0000000..f04d600
--- /dev/null
+++ b/src/WindowSelector.cpp
@@ -0,0 +1,229 @@
+#include "../include/WindowSelector.hpp"
+#include "../include/WindowUtils.hpp"
+
+#include <stdio.h>
+#include <string.h>
+
+#include <X11/extensions/shape.h>
+#include <X11/cursorfont.h>
+#include <X11/keysym.h>
+
+namespace gsr {
+ static const int rectangle_border_size = 2;
+
+ static int max_int(int a, int b) {
+ return a >= b ? a : b;
+ }
+
+ static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) {
+ if(width < 0) {
+ x += width;
+ width = abs(width);
+ }
+
+ if(height < 0) {
+ y += height;
+ height = abs(height);
+ }
+
+ XRectangle rectangles[] = {
+ {
+ (short)max_int(0, x), (short)max_int(0, y),
+ (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
+ }, // Left
+ {
+ (short)max_int(0, x + width - border_size), (short)max_int(0, y),
+ (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
+ }, // Right
+ {
+ (short)max_int(0, x + border_size), (short)max_int(0, y),
+ (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
+ }, // Top
+ {
+ (short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size),
+ (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
+ }, // Bottom
+ };
+ XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
+ XFlush(dpy);
+ }
+
+ static unsigned long mgl_color_to_x11_color(mgl::Color color) {
+ if(color.a == 0)
+ return 0;
+ return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF);
+ }
+
+ static Window get_cursor_window(Display *dpy) {
+ Window root_window = None;
+ Window window = None;
+ int dummy_i;
+ unsigned int dummy_u;
+ mgl::vec2i root_pos;
+ XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
+ return window;
+ }
+
+ static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) {
+ Window root_window;
+ int x = 0;
+ int y = 0;
+ unsigned int w = 0;
+ unsigned int h = 0;
+ unsigned int dummy_border, dummy_depth;
+ XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth);
+ pos.x = x;
+ pos.y = y;
+ size.x = w;
+ size.y = h;
+ }
+
+ WindowSelector::WindowSelector() {
+
+ }
+
+ WindowSelector::~WindowSelector() {
+ stop();
+ }
+
+ bool WindowSelector::start(mgl::Color border_color) {
+ if(dpy)
+ return false;
+
+ const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color);
+ dpy = XOpenDisplay(nullptr);
+ if(!dpy) {
+ fprintf(stderr, "Error: WindowSelector::start: failed to connect to the X11 server\n");
+ return false;
+ }
+
+ const Window cursor_window = get_cursor_window(dpy);
+ mgl::vec2i cursor_window_pos, cursor_window_size;
+ get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size);
+
+ XVisualInfo vinfo;
+ memset(&vinfo, 0, sizeof(vinfo));
+ XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
+ border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
+
+ XSetWindowAttributes window_attr;
+ window_attr.background_pixel = border_color_x11;
+ window_attr.border_pixel = 0;
+ window_attr.override_redirect = true;
+ window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
+ window_attr.colormap = border_window_colormap;
+
+ Screen *screen = XDefaultScreenOfDisplay(dpy);
+ border_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0,
+ vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
+ if(!border_window) {
+ fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n");
+ stop();
+ return false;
+ }
+ set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
+ if(cursor_window && cursor_window != DefaultRootWindow(dpy))
+ set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size);
+ else
+ set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
+ make_window_click_through(dpy, border_window);
+ XMapWindow(dpy, border_window);
+
+ crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair);
+ XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime);
+ XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime);
+ XFlush(dpy);
+
+ selected = false;
+ canceled = false;
+ selected_window = None;
+ return true;
+ }
+
+ void WindowSelector::stop() {
+ if(!dpy)
+ return;
+
+ XUngrabPointer(dpy, CurrentTime);
+ XUngrabKeyboard(dpy, CurrentTime);
+
+ if(border_window_colormap) {
+ XFreeColormap(dpy, border_window_colormap);
+ border_window_colormap = 0;
+ }
+
+ if(border_window) {
+ XDestroyWindow(dpy, border_window);
+ border_window = 0;
+ }
+
+ if(crosshair_cursor) {
+ XFreeCursor(dpy, crosshair_cursor);
+ crosshair_cursor = None;
+ }
+
+ XFlush(dpy);
+ XCloseDisplay(dpy);
+ dpy = nullptr;
+ }
+
+ bool WindowSelector::is_started() const {
+ return dpy != nullptr;
+ }
+
+ bool WindowSelector::failed() const {
+ return !dpy;
+ }
+
+ bool WindowSelector::poll_events() {
+ if(!dpy || selected)
+ return false;
+
+ XEvent xev;
+ while(XPending(dpy)) {
+ XNextEvent(dpy, &xev);
+
+ if(xev.type == MotionNotify) {
+ const Window motion_window = xev.xmotion.subwindow;
+ mgl::vec2i motion_window_pos, motion_window_size;
+ get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size);
+ if(motion_window && motion_window != DefaultRootWindow(dpy))
+ set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size);
+ else
+ set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
+ XFlush(dpy);
+ } else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) {
+ selected_window = xev.xbutton.subwindow;
+ const Window clicked_window_real = window_get_target_window_child(dpy, selected_window);
+ if(clicked_window_real)
+ selected_window = clicked_window_real;
+ selected = true;
+
+ stop();
+ break;
+ } else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) {
+ canceled = true;
+ selected = false;
+ stop();
+ break;
+ }
+ }
+ return true;
+ }
+
+ bool WindowSelector::take_selection() {
+ const bool result = selected;
+ selected = false;
+ return result;
+ }
+
+ bool WindowSelector::take_canceled() {
+ const bool result = canceled;
+ canceled = false;
+ return result;
+ }
+
+ Window WindowSelector::get_selection() const {
+ return selected_window;
+ }
+} \ No newline at end of file
diff --git a/src/WindowUtils.cpp b/src/WindowUtils.cpp
index 49fd65b..c6b278b 100644
--- a/src/WindowUtils.cpp
+++ b/src/WindowUtils.cpp
@@ -1,10 +1,12 @@
#include "../include/WindowUtils.hpp"
+#include "../include/Utils.hpp"
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/extensions/XInput2.h>
#include <X11/extensions/Xfixes.h>
#include <X11/extensions/shapeconst.h>
+#include <X11/extensions/Xrandr.h>
#include <mglpp/system/Utf8.hpp>
@@ -61,7 +63,7 @@ namespace gsr {
return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
}
- static Window window_get_target_window_child(Display *display, Window window) {
+ Window window_get_target_window_child(Display *display, Window window) {
if(window == None)
return None;
@@ -211,28 +213,6 @@ namespace gsr {
return result;
}
- static std::string strip(const std::string &str) {
- int start_index = 0;
- int str_len = str.size();
-
- for(int i = 0; i < str_len; ++i) {
- if(str[i] != ' ') {
- start_index += i;
- str_len -= i;
- break;
- }
- }
-
- for(int i = str_len - 1; i >= 0; --i) {
- if(str[i] != ' ') {
- str_len = i + 1;
- break;
- }
- }
-
- return str.substr(start_index, str_len);
- }
-
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
std::string result;
const Window focused_window = get_focused_window(dpy, window_capture_type);
@@ -518,14 +498,21 @@ namespace gsr {
return XGetSelectionOwner(dpy, prop_atom) != None;
}
- static void get_monitors_callback(const mgl_monitor *monitor, void *userdata) {
- std::vector<Monitor> *monitors = (std::vector<Monitor>*)userdata;
- monitors->push_back({mgl::vec2i(monitor->pos.x, monitor->pos.y), mgl::vec2i(monitor->size.x, monitor->size.y)});
- }
-
std::vector<Monitor> get_monitors(Display *dpy) {
std::vector<Monitor> monitors;
- mgl_for_each_active_monitor_output(dpy, get_monitors_callback, &monitors);
+ int nmonitors = 0;
+ XRRMonitorInfo *monitor_info = XRRGetMonitors(dpy, DefaultRootWindow(dpy), True, &nmonitors);
+ if(monitor_info) {
+ for(int i = 0; i < nmonitors; ++i) {
+ char *monitor_name = XGetAtomName(dpy, monitor_info[i].name);
+ if(!monitor_name)
+ continue;
+
+ monitors.push_back({mgl::vec2i(monitor_info[i].x, monitor_info[i].y), mgl::vec2i(monitor_info[i].width, monitor_info[i].height), std::string(monitor_name)});
+ XFree(monitor_name);
+ }
+ XRRFreeMonitors(monitor_info);
+ }
return monitors;
}
diff --git a/src/gui/Button.cpp b/src/gui/Button.cpp
index d8cb85b..6e343c4 100644
--- a/src/gui/Button.cpp
+++ b/src/gui/Button.cpp
@@ -63,7 +63,7 @@ namespace gsr {
window.draw(sprite);
const int padding_icon_right = padding_right_icon_scale * get_button_height();
- text.set_position((sprite.get_position() + mgl::vec2f(sprite.get_size().x + padding_icon_right, sprite.get_size().y * 0.5f - text.get_bounds().size.y * 0.5f)).floor());
+ text.set_position((sprite.get_position() + mgl::vec2f(sprite.get_size().x + padding_icon_right, sprite.get_size().y * 0.5f - text.get_bounds().size.y * 0.52f)).floor());
window.draw(text);
} else {
text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor());
diff --git a/src/gui/ComboBox.cpp b/src/gui/ComboBox.cpp
index dbe9aa0..4287a53 100644
--- a/src/gui/ComboBox.cpp
+++ b/src/gui/ComboBox.cpp
@@ -85,7 +85,7 @@ namespace gsr {
void ComboBox::add_item(const std::string &text, const std::string &id) {
items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}});
- items.back().text.set_max_width(font->get_character_size() * 22); // TODO: Make a proper solution
+ items.back().text.set_max_width(font->get_character_size() * 20); // TODO: Make a proper solution
//items.back().text.set_max_rows(1);
dirty = true;
}
diff --git a/src/gui/DropdownButton.cpp b/src/gui/DropdownButton.cpp
index bdc4027..5d1cc38 100644
--- a/src/gui/DropdownButton.cpp
+++ b/src/gui/DropdownButton.cpp
@@ -110,6 +110,14 @@ namespace gsr {
window.draw(rect);
}
+ if(activated) {
+ description.set_color(get_color_theme().tint_color);
+ icon_sprite.set_color(get_color_theme().tint_color);
+ } else {
+ description.set_color(mgl::Color(150, 150, 150));
+ icon_sprite.set_color(mgl::Color(255, 255, 255));
+ }
+
const int text_margin = size.y * 0.085;
const auto title_bounds = title.get_bounds();
@@ -148,7 +156,7 @@ namespace gsr {
window.draw(separator);
}
- if(mouse_inside_item == -1) {
+ if(mouse_inside_item == -1 && item.enabled) {
const bool inside = mgl::FloatRect(item_position, item_size).contains({ (float)mouse_pos.x, (float)mouse_pos.y });
if(inside) {
draw_rectangle_outline(window, item_position, item_size, get_color_theme().tint_color, border_size);
@@ -161,16 +169,18 @@ namespace gsr {
mgl::Sprite icon(item.icon_texture);
icon.set_height((int)(item_size.y * 0.4f));
icon.set_position((item_position + mgl::vec2f(padding_left, item_size.y * 0.5f - icon.get_size().y * 0.5f)).floor());
+ icon.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80));
window.draw(icon);
icon_offset = icon.get_size().x + icon_spacing;
}
item.text.set_position((item_position + mgl::vec2f(padding_left + icon_offset, item_size.y * 0.5f - text_bounds.size.y * 0.5f)).floor());
+ item.text.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80));
window.draw(item.text);
const auto description_bounds = item.description_text.get_bounds();
item.description_text.set_position((item_position + mgl::vec2f(item_size.x - description_bounds.size.x - padding_right, item_size.y * 0.5f - description_bounds.size.y * 0.5f)).floor());
- item.description_text.set_color(mgl::Color(255, 255, 255, 120));
+ item.description_text.set_color(item.enabled ? mgl::Color(255, 255, 255, 120) : mgl::Color(255, 255, 255, 40));
window.draw(item.description_text);
item_position.y += item_size.y;
@@ -179,6 +189,10 @@ namespace gsr {
}
void DropdownButton::add_item(const std::string &text, const std::string &id, const std::string &description) {
+ for(auto &item : items) {
+ if(item.id == id)
+ return;
+ }
items.push_back({mgl::Text(text, *title_font), mgl::Text(description, *description_font), nullptr, id});
dirty = true;
}
@@ -210,6 +224,15 @@ namespace gsr {
}
}
+ void DropdownButton::set_item_enabled(const std::string &id, bool enabled) {
+ for(auto &item : items) {
+ if(item.id == id) {
+ item.enabled = enabled;
+ return;
+ }
+ }
+ }
+
void DropdownButton::set_description(std::string description_text) {
description.set_string(std::move(description_text));
}
@@ -219,14 +242,6 @@ namespace gsr {
return;
this->activated = activated;
-
- if(activated) {
- description.set_color(get_color_theme().tint_color);
- icon_sprite.set_color(get_color_theme().tint_color);
- } else {
- description.set_color(mgl::Color(150, 150, 150));
- icon_sprite.set_color(mgl::Color(255, 255, 255));
- }
}
void DropdownButton::update_if_dirty() {
diff --git a/src/gui/GlobalSettingsPage.cpp b/src/gui/GlobalSettingsPage.cpp
index 1e2a444..6650c69 100644
--- a/src/gui/GlobalSettingsPage.cpp
+++ b/src/gui/GlobalSettingsPage.cpp
@@ -1,7 +1,6 @@
#include "../../include/gui/GlobalSettingsPage.hpp"
#include "../../include/Overlay.hpp"
-#include "../../include/GlobalHotkeys.hpp"
#include "../../include/Theme.hpp"
#include "../../include/Process.hpp"
#include "../../include/gui/GsrPage.hpp"
@@ -149,7 +148,7 @@ namespace gsr {
tint_color_radio_button_ptr = tint_color_radio_button.get();
tint_color_radio_button->add_item("Red", "amd");
tint_color_radio_button->add_item("Green", "nvidia");
- tint_color_radio_button->add_item("blue", "intel");
+ tint_color_radio_button->add_item("Blue", "intel");
tint_color_radio_button->on_selection_changed = [](const std::string&, const std::string &id) {
if(id == "amd")
get_color_theme().tint_color = mgl::Color(221, 0, 49);
@@ -256,6 +255,30 @@ namespace gsr {
return list;
}
+ std::unique_ptr<List> GlobalSettingsPage::create_replay_partial_save_hotkey_options() {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
+
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save 1 minute replay:", get_color_theme().text_color));
+ auto save_replay_1_min_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ save_replay_1_min_button_ptr = save_replay_1_min_button.get();
+ list->add_widget(std::move(save_replay_1_min_button));
+
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save 10 minute replay:", get_color_theme().text_color));
+ auto save_replay_10_min_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ save_replay_10_min_button_ptr = save_replay_10_min_button.get();
+ list->add_widget(std::move(save_replay_10_min_button));
+
+ save_replay_1_min_button_ptr->on_click = [this] {
+ configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE_1_MIN);
+ };
+
+ save_replay_10_min_button_ptr->on_click = [this] {
+ configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE_10_MIN);
+ };
+
+ return list;
+ }
+
std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_options() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
@@ -335,6 +358,8 @@ namespace gsr {
config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0};
config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0};
+ config.replay_config.save_1_min_hotkey = {mgl::Keyboard::Unknown, 0};
+ config.replay_config.save_10_min_hotkey = {mgl::Keyboard::Unknown, 0};
config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0};
config.screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Unknown, 0};
config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
@@ -374,6 +399,7 @@ namespace gsr {
list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
list_ptr->add_widget(create_show_hide_hotkey_options());
list_ptr->add_widget(create_replay_hotkey_options());
+ list_ptr->add_widget(create_replay_partial_save_hotkey_options());
list_ptr->add_widget(create_record_hotkey_options());
list_ptr->add_widget(create_stream_hotkey_options());
list_ptr->add_widget(create_screenshot_hotkey_options());
@@ -395,6 +421,8 @@ namespace gsr {
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_down_texture, get_theme().body_font.get_character_size(), "to save a replay"));
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_left_texture, get_theme().body_font.get_character_size(), "to start/stop recording"));
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_right_texture, get_theme().body_font.get_character_size(), "to turn replay on/off"));
+ list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_cross_texture, get_theme().body_font.get_character_size(), "to save a 1 minute replay"));
+ list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_triangle_texture, get_theme().body_font.get_character_size(), "to save a 10 minute replay"));
return subsection;
}
@@ -490,6 +518,8 @@ namespace gsr {
void GlobalSettingsPage::load_hotkeys() {
turn_replay_on_off_button_ptr->set_text(config.replay_config.start_stop_hotkey.to_string());
save_replay_button_ptr->set_text(config.replay_config.save_hotkey.to_string());
+ save_replay_1_min_button_ptr->set_text(config.replay_config.save_1_min_hotkey.to_string());
+ save_replay_10_min_button_ptr->set_text(config.replay_config.save_10_min_hotkey.to_string());
start_stop_recording_button_ptr->set_text(config.record_config.start_stop_hotkey.to_string());
pause_unpause_recording_button_ptr->set_text(config.record_config.pause_unpause_hotkey.to_string());
@@ -567,6 +597,10 @@ namespace gsr {
return turn_replay_on_off_button_ptr;
case ConfigureHotkeyType::REPLAY_SAVE:
return save_replay_button_ptr;
+ case ConfigureHotkeyType::REPLAY_SAVE_1_MIN:
+ return save_replay_1_min_button_ptr;
+ case ConfigureHotkeyType::REPLAY_SAVE_10_MIN:
+ return save_replay_10_min_button_ptr;
case ConfigureHotkeyType::RECORD_START_STOP:
return start_stop_recording_button_ptr;
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
@@ -591,6 +625,10 @@ namespace gsr {
return &config.replay_config.start_stop_hotkey;
case ConfigureHotkeyType::REPLAY_SAVE:
return &config.replay_config.save_hotkey;
+ case ConfigureHotkeyType::REPLAY_SAVE_1_MIN:
+ return &config.replay_config.save_1_min_hotkey;
+ case ConfigureHotkeyType::REPLAY_SAVE_10_MIN:
+ return &config.replay_config.save_10_min_hotkey;
case ConfigureHotkeyType::RECORD_START_STOP:
return &config.record_config.start_stop_hotkey;
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
@@ -643,6 +681,12 @@ namespace gsr {
case ConfigureHotkeyType::REPLAY_SAVE:
hotkey_configure_action_name = "Save replay";
break;
+ case ConfigureHotkeyType::REPLAY_SAVE_1_MIN:
+ hotkey_configure_action_name = "Save 1 minute replay";
+ break;
+ case ConfigureHotkeyType::REPLAY_SAVE_10_MIN:
+ hotkey_configure_action_name = "Save 10 minute replay";
+ break;
case ConfigureHotkeyType::RECORD_START_STOP:
hotkey_configure_action_name = "Start/stop recording";
break;
diff --git a/src/gui/GsrPage.cpp b/src/gui/GsrPage.cpp
index 663187c..b4005f5 100644
--- a/src/gui/GsrPage.cpp
+++ b/src/gui/GsrPage.cpp
@@ -39,8 +39,9 @@ namespace gsr {
// Process widgets by visibility (backwards)
return widgets.for_each_reverse([selected_widget, &window, &event, content_page_position](std::unique_ptr<Widget> &widget) {
- if(widget.get() != selected_widget) {
- if(!widget->on_event(event, window, content_page_position))
+ Widget *p = widget.get();
+ if(p != selected_widget) {
+ if(!p->on_event(event, window, content_page_position))
return false;
}
return true;
diff --git a/src/gui/List.cpp b/src/gui/List.cpp
index 5294e36..57a6045 100644
--- a/src/gui/List.cpp
+++ b/src/gui/List.cpp
@@ -24,14 +24,23 @@ namespace gsr {
// Process widgets by visibility (backwards)
return widgets.for_each_reverse([selected_widget, &event, &window](std::unique_ptr<Widget> &widget) {
// Ignore offset because widgets are positioned with offset in ::draw, this solution is simpler
- if(widget.get() != selected_widget) {
- if(!widget->on_event(event, window, mgl::vec2f(0.0f, 0.0f)))
+ Widget *p = widget.get();
+ if(p != selected_widget) {
+ if(!p->on_event(event, window, mgl::vec2f(0.0f, 0.0f)))
return false;
}
return true;
});
}
+ List::~List() {
+ widgets.for_each([this](std::unique_ptr<Widget> &widget) {
+ if(widget->parent_widget == this)
+ widget->parent_widget = nullptr;
+ return true;
+ }, true);
+ }
+
void List::draw(mgl::Window &window, mgl::vec2f offset) {
if(!visible)
return;
@@ -104,15 +113,6 @@ namespace gsr {
selected_widget->draw(window, mgl::vec2f(0.0f, 0.0f));
}
- // void List::remove_child_widget(Widget *widget) {
- // for(auto it = widgets.begin(), end = widgets.end(); it != end; ++it) {
- // if(it->get() == widget) {
- // widgets.erase(it);
- // return;
- // }
- // }
- // }
-
void List::add_widget(std::unique_ptr<Widget> widget) {
widget->parent_widget = this;
widgets.push_back(std::move(widget));
@@ -122,6 +122,10 @@ namespace gsr {
widgets.remove(widget);
}
+ void List::replace_widget(Widget *widget, std::unique_ptr<Widget> new_widget) {
+ widgets.replace_item(widget, std::move(new_widget));
+ }
+
void List::clear() {
widgets.clear();
}
@@ -137,6 +141,10 @@ namespace gsr {
return nullptr;
}
+ size_t List::get_num_children() const {
+ return widgets.size();
+ }
+
void List::set_spacing(float spacing) {
spacing_scale = spacing;
}
diff --git a/src/gui/Page.cpp b/src/gui/Page.cpp
index ae13d82..5f21b71 100644
--- a/src/gui/Page.cpp
+++ b/src/gui/Page.cpp
@@ -1,14 +1,13 @@
#include "../../include/gui/Page.hpp"
namespace gsr {
- // void Page::remove_child_widget(Widget *widget) {
- // for(auto it = widgets.begin(), end = widgets.end(); it != end; ++it) {
- // if(it->get() == widget) {
- // widgets.erase(it);
- // return;
- // }
- // }
- // }
+ Page::~Page() {
+ widgets.for_each([this](std::unique_ptr<Widget> &widget) {
+ if(widget->parent_widget == this)
+ widget->parent_widget = nullptr;
+ return true;
+ }, true);
+ }
void Page::add_widget(std::unique_ptr<Widget> widget) {
widget->parent_widget = this;
diff --git a/src/gui/RadioButton.cpp b/src/gui/RadioButton.cpp
index a6ef96a..bbb958a 100644
--- a/src/gui/RadioButton.cpp
+++ b/src/gui/RadioButton.cpp
@@ -169,7 +169,7 @@ namespace gsr {
}
}
- const std::string RadioButton::get_selected_id() const {
+ const std::string& RadioButton::get_selected_id() const {
if(items.empty()) {
static std::string dummy;
return dummy;
@@ -177,4 +177,13 @@ namespace gsr {
return items[selected_item].id;
}
}
+
+ const std::string& RadioButton::get_selected_text() const {
+ if(items.empty()) {
+ static std::string dummy;
+ return dummy;
+ } else {
+ return items[selected_item].text.get_string();
+ }
+ }
} \ No newline at end of file
diff --git a/src/gui/ScreenshotSettingsPage.cpp b/src/gui/ScreenshotSettingsPage.cpp
index fd75660..27a94b0 100644
--- a/src/gui/ScreenshotSettingsPage.cpp
+++ b/src/gui/ScreenshotSettingsPage.cpp
@@ -35,11 +35,12 @@ namespace gsr {
std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_record_area_box() {
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
// TODO: Show options not supported but disable them
- // TODO: Enable this
- //if(capture_options.window)
- // record_area_box->add_item("Window", "window");
+ if(capture_options.window)
+ record_area_box->add_item("Window", "window");
if(capture_options.region)
record_area_box->add_item("Region", "region");
+ if(!capture_options.monitors.empty())
+ record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor");
for(const auto &monitor : capture_options.monitors) {
char name[256];
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
@@ -58,14 +59,6 @@ namespace gsr {
return record_area_list;
}
- std::unique_ptr<List> ScreenshotSettingsPage::create_select_window() {
- auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
- select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
- select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
- select_window_list_ptr = select_window_list.get();
- return select_window_list;
- }
-
std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_width_entry() {
auto image_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
image_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
@@ -122,7 +115,6 @@ namespace gsr {
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
capture_target_list->add_widget(create_record_area());
- capture_target_list->add_widget(create_select_window());
capture_target_list->add_widget(create_image_resolution_section());
capture_target_list->add_widget(create_restore_portal_session_section());
@@ -255,11 +247,8 @@ namespace gsr {
void ScreenshotSettingsPage::add_widgets() {
content_page_ptr->add_widget(create_settings());
- record_area_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) {
- (void)text;
- const bool window_selected = id == "window";
+ record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
const bool portal_selected = id == "portal";
- select_window_list_ptr->set_visible(window_selected);
image_resolution_list_ptr->set_visible(change_image_resolution_checkbox_ptr->is_checked());
restore_portal_session_list_ptr->set_visible(portal_selected);
return true;
@@ -270,7 +259,7 @@ namespace gsr {
};
if(!capture_options.monitors.empty())
- record_area_box_ptr->set_selected_item(capture_options.monitors.front().name);
+ record_area_box_ptr->set_selected_item("focused_monitor");
else if(capture_options.portal)
record_area_box_ptr->set_selected_item("portal");
else if(capture_options.window)
diff --git a/src/gui/ScrollablePage.cpp b/src/gui/ScrollablePage.cpp
index d5e92d0..cec20d3 100644
--- a/src/gui/ScrollablePage.cpp
+++ b/src/gui/ScrollablePage.cpp
@@ -15,6 +15,14 @@ namespace gsr {
ScrollablePage::ScrollablePage(mgl::vec2f size) : size(size) {}
+ ScrollablePage::~ScrollablePage() {
+ widgets.for_each([this](std::unique_ptr<Widget> &widget) {
+ if(widget->parent_widget == this)
+ widget->parent_widget = nullptr;
+ return true;
+ }, true);
+ }
+
bool ScrollablePage::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) {
if(!visible)
return true;
@@ -57,8 +65,9 @@ namespace gsr {
// Process widgets by visibility (backwards)
const bool continue_events = widgets.for_each_reverse([selected_widget, &window, &event, offset](std::unique_ptr<Widget> &widget) {
- if(widget.get() != selected_widget) {
- if(!widget->on_event(event, window, offset))
+ Widget *p = widget.get();
+ if(p != selected_widget) {
+ if(!p->on_event(event, window, offset))
return false;
}
return true;
diff --git a/src/gui/SettingsPage.cpp b/src/gui/SettingsPage.cpp
index 79b4525..26e7335 100644
--- a/src/gui/SettingsPage.cpp
+++ b/src/gui/SettingsPage.cpp
@@ -11,6 +11,8 @@
#include <string.h>
namespace gsr {
+ static const char *custom_app_audio_tag = "[custom]";
+
enum class AudioTrackType {
DEVICE,
APPLICATION,
@@ -63,13 +65,14 @@ namespace gsr {
std::unique_ptr<ComboBox> SettingsPage::create_record_area_box() {
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
// TODO: Show options not supported but disable them
- // TODO: Enable this
- //if(capture_options.window)
- // record_area_box->add_item("Window", "window");
- if(capture_options.region)
- record_area_box->add_item("Region", "region");
+ if(capture_options.window)
+ record_area_box->add_item("Window", "window");
if(capture_options.focused)
record_area_box->add_item("Follow focused window", "focused");
+ if(capture_options.region)
+ record_area_box->add_item("Region", "region");
+ if(!capture_options.monitors.empty())
+ record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor");
for(const auto &monitor : capture_options.monitors) {
char name[256];
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
@@ -88,14 +91,6 @@ namespace gsr {
return record_area_list;
}
- std::unique_ptr<List> SettingsPage::create_select_window() {
- auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
- select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
- select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
- select_window_list_ptr = select_window_list.get();
- return select_window_list;
- }
-
std::unique_ptr<Entry> SettingsPage::create_area_width_entry() {
auto area_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
area_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
@@ -182,7 +177,6 @@ namespace gsr {
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
capture_target_list->add_widget(create_record_area());
- capture_target_list->add_widget(create_select_window());
capture_target_list->add_widget(create_area_size_section());
capture_target_list->add_widget(create_video_resolution_section());
capture_target_list->add_widget(create_restore_portal_session_section());
@@ -192,129 +186,229 @@ namespace gsr {
return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
- std::unique_ptr<ComboBox> SettingsPage::create_audio_device_selection_combobox() {
+ static bool audio_device_is_output(const std::string &audio_device_id) {
+ return audio_device_id == "default_output" || ends_with(audio_device_id, ".monitor");
+ }
+
+ std::unique_ptr<ComboBox> SettingsPage::create_audio_device_selection_combobox(AudioDeviceType device_type) {
auto audio_device_box = std::make_unique<ComboBox>(&get_theme().body_font);
for(const auto &audio_device : audio_devices) {
- audio_device_box->add_item(audio_device.description, audio_device.name);
+ const bool device_is_output = audio_device_is_output(audio_device.name);
+ if((device_type == AudioDeviceType::OUTPUT && device_is_output) || (device_type == AudioDeviceType::INPUT && !device_is_output)) {
+ std::string description = audio_device.description;
+ if(starts_with(description, "Monitor of "))
+ description.erase(0, 11);
+ audio_device_box->add_item(description, audio_device.name);
+ }
}
return audio_device_box;
}
- std::unique_ptr<Button> SettingsPage::create_remove_audio_device_button(List *audio_device_list_ptr) {
- auto remove_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Remove", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
- remove_audio_track_button->on_click = [this, audio_device_list_ptr]() {
- audio_track_list_ptr->remove_widget(audio_device_list_ptr);
+ static void set_application_audio_options_visible(Subsection *audio_track_subsection, bool visible, const GsrInfo &gsr_info) {
+ if(!gsr_info.system_info.supports_app_audio)
+ visible = false;
+
+ List *audio_track_items_list = dynamic_cast<List*>(audio_track_subsection->get_inner_widget());
+
+ List *buttons_list = dynamic_cast<List*>(audio_track_items_list->get_child_widget_by_index(1));
+ Button *add_application_audio_button = dynamic_cast<Button*>(buttons_list->get_child_widget_by_index(2));
+ add_application_audio_button->set_visible(visible);
+
+ CheckBox *invert_app_audio_checkbox = dynamic_cast<CheckBox*>(audio_track_items_list->get_child_widget_by_index(3));
+ invert_app_audio_checkbox->set_visible(visible);
+ }
+
+ static void set_application_audio_options_visible(List *audio_track_section_list_ptr, bool visible, const GsrInfo &gsr_info) {
+ audio_track_section_list_ptr->for_each_child_widget([visible, &gsr_info](std::unique_ptr<Widget> &widget) {
+ Subsection *audio_track_subsection = dynamic_cast<Subsection*>(widget.get());
+ set_application_audio_options_visible(audio_track_subsection, visible, gsr_info);
+ return true;
+ });
+ }
+
+ std::unique_ptr<Button> SettingsPage::create_remove_audio_device_button(List *audio_input_list_ptr, List *audio_device_list_ptr) {
+ auto remove_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 0));
+ remove_audio_track_button->set_icon(&get_theme().trash_texture);
+ remove_audio_track_button->set_icon_padding_scale(0.75f);
+ remove_audio_track_button->on_click = [audio_input_list_ptr, audio_device_list_ptr]() {
+ audio_input_list_ptr->remove_widget(audio_device_list_ptr);
};
return remove_audio_track_button;
}
- std::unique_ptr<List> SettingsPage::create_audio_device() {
+ std::unique_ptr<List> SettingsPage::create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr) {
auto audio_device_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
audio_device_list->userdata = (void*)(uintptr_t)AudioTrackType::DEVICE;
- audio_device_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Device:", get_color_theme().text_color));
- audio_device_list->add_widget(create_audio_device_selection_combobox());
- audio_device_list->add_widget(create_remove_audio_device_button(audio_device_list.get()));
+ audio_device_list->add_widget(std::make_unique<Label>(&get_theme().body_font, device_type == AudioDeviceType::OUTPUT ? "Output device:" : "Input device: ", get_color_theme().text_color));
+ audio_device_list->add_widget(create_audio_device_selection_combobox(device_type));
+ audio_device_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, audio_device_list.get()));
return audio_device_list;
}
- std::unique_ptr<Button> SettingsPage::create_add_audio_device_button() {
- auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add audio device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
- add_audio_track_button->on_click = [this]() {
+ std::unique_ptr<Button> SettingsPage::create_add_audio_track_button() {
+ auto button = std::make_unique<Button>(&get_theme().body_font, "Add audio track", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ button->on_click = [this]() {
+ audio_track_section_list_ptr->add_widget(create_audio_track_section(audio_section_ptr));
+ };
+ button->set_visible(type != Type::STREAM);
+ return button;
+ }
+
+ std::unique_ptr<Button> SettingsPage::create_add_audio_output_device_button(List *audio_input_list_ptr) {
+ auto button = std::make_unique<Button>(&get_theme().body_font, "Add output device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ button->on_click = [this, audio_input_list_ptr]() {
audio_devices = get_audio_devices();
- audio_track_list_ptr->add_widget(create_audio_device());
+ audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::OUTPUT, audio_input_list_ptr));
};
- return add_audio_track_button;
+ return button;
+ }
+
+ std::unique_ptr<Button> SettingsPage::create_add_audio_input_device_button(List *audio_input_list_ptr) {
+ auto button = std::make_unique<Button>(&get_theme().body_font, "Add input device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ button->on_click = [this, audio_input_list_ptr]() {
+ audio_devices = get_audio_devices();
+ audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::INPUT, audio_input_list_ptr));
+ };
+ return button;
}
- std::unique_ptr<ComboBox> SettingsPage::create_application_audio_selection_combobox() {
+ std::unique_ptr<ComboBox> SettingsPage::create_application_audio_selection_combobox(List *application_audio_row) {
auto audio_device_box = std::make_unique<ComboBox>(&get_theme().body_font);
+ ComboBox *audio_device_box_ptr = audio_device_box.get();
for(const auto &app_audio : application_audio) {
audio_device_box->add_item(app_audio, app_audio);
}
+ audio_device_box->add_item("Custom...", custom_app_audio_tag);
+
+ audio_device_box->on_selection_changed = [application_audio_row, audio_device_box_ptr](const std::string&, const std::string &id) {
+ if(id == custom_app_audio_tag) {
+ application_audio_row->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION_CUSTOM;
+ auto custom_app_audio_entry = std::make_unique<Entry>(&get_theme().body_font, "", (int)(get_theme().body_font.get_character_size() * 10.0f));
+ application_audio_row->replace_widget(audio_device_box_ptr, std::move(custom_app_audio_entry));
+ }
+ };
+
return audio_device_box;
}
- std::unique_ptr<List> SettingsPage::create_application_audio() {
+ std::unique_ptr<List> SettingsPage::create_application_audio(List *audio_input_list_ptr) {
auto application_audio_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
application_audio_list->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION;
- application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "App: ", get_color_theme().text_color));
- application_audio_list->add_widget(create_application_audio_selection_combobox());
- application_audio_list->add_widget(create_remove_audio_device_button(application_audio_list.get()));
+ application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Application: ", get_color_theme().text_color));
+ application_audio_list->add_widget(create_application_audio_selection_combobox(application_audio_list.get()));
+ application_audio_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, application_audio_list.get()));
return application_audio_list;
}
- std::unique_ptr<List> SettingsPage::create_custom_application_audio() {
+ std::unique_ptr<List> SettingsPage::create_custom_application_audio(List *audio_input_list_ptr) {
auto application_audio_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
application_audio_list->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION_CUSTOM;
- application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "App: ", get_color_theme().text_color));
+ application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Application: ", get_color_theme().text_color));
application_audio_list->add_widget(std::make_unique<Entry>(&get_theme().body_font, "", (int)(get_theme().body_font.get_character_size() * 10.0f)));
- application_audio_list->add_widget(create_remove_audio_device_button(application_audio_list.get()));
+ application_audio_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, application_audio_list.get()));
return application_audio_list;
}
- std::unique_ptr<Button> SettingsPage::create_add_application_audio_button() {
+ std::unique_ptr<Button> SettingsPage::create_add_application_audio_button(List *audio_input_list_ptr) {
auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add application audio", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
- add_application_audio_button_ptr = add_audio_track_button.get();
- add_audio_track_button->on_click = [this]() {
+ add_audio_track_button->on_click = [this, audio_input_list_ptr]() {
application_audio = get_application_audio();
- audio_track_list_ptr->add_widget(create_application_audio());
- };
- return add_audio_track_button;
- }
-
- std::unique_ptr<Button> SettingsPage::create_add_custom_application_audio_button() {
- auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add custom application audio", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
- add_custom_application_audio_button_ptr = add_audio_track_button.get();
- add_audio_track_button->on_click = [this]() {
- audio_track_list_ptr->add_widget(create_custom_application_audio());
+ if(application_audio.empty())
+ audio_input_list_ptr->add_widget(create_custom_application_audio(audio_input_list_ptr));
+ else
+ audio_input_list_ptr->add_widget(create_application_audio(audio_input_list_ptr));
};
return add_audio_track_button;
}
- std::unique_ptr<List> SettingsPage::create_add_audio_buttons() {
+ std::unique_ptr<List> SettingsPage::create_add_audio_buttons(List *audio_input_list_ptr) {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
- list->add_widget(create_add_audio_device_button());
- list->add_widget(create_add_application_audio_button());
- list->add_widget(create_add_custom_application_audio_button());
+ list->add_widget(create_add_audio_output_device_button(audio_input_list_ptr));
+ list->add_widget(create_add_audio_input_device_button(audio_input_list_ptr));
+ list->add_widget(create_add_application_audio_button(audio_input_list_ptr));
return list;
}
- std::unique_ptr<List> SettingsPage::create_audio_track_track_section() {
+ std::unique_ptr<List> SettingsPage::create_audio_input_section() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
- audio_track_list_ptr = list.get();
- audio_track_list_ptr->add_widget(create_audio_device()); // Add default_output by default
+ //list->add_widget(create_audio_device(list.get())); // Add default_output by default
return list;
}
- std::unique_ptr<CheckBox> SettingsPage::create_split_audio_checkbox() {
- auto split_audio_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Split each device/app audio into separate audio tracks");
- split_audio_checkbox->set_checked(false);
- split_audio_checkbox_ptr = split_audio_checkbox.get();
- return split_audio_checkbox;
- }
-
std::unique_ptr<CheckBox> SettingsPage::create_application_audio_invert_checkbox() {
auto application_audio_invert_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record audio from all applications except the selected ones");
application_audio_invert_checkbox->set_checked(false);
- application_audio_invert_checkbox_ptr = application_audio_invert_checkbox.get();
return application_audio_invert_checkbox;
}
- std::unique_ptr<Widget> SettingsPage::create_audio_track_section() {
+ static void update_audio_track_titles(List *audio_track_section_list_ptr) {
+ int index = 0;
+ audio_track_section_list_ptr->for_each_child_widget([&index](std::unique_ptr<Widget> &widget) {
+ char audio_track_name[32];
+ snprintf(audio_track_name, sizeof(audio_track_name), "Audio track #%d", 1 + index);
+ ++index;
+
+ Subsection *subsection = dynamic_cast<Subsection*>(widget.get());
+ List *subesection_items = dynamic_cast<List*>(subsection->get_inner_widget());
+ Label *audio_track_title = dynamic_cast<Label*>(dynamic_cast<List*>(subesection_items->get_child_widget_by_index(0))->get_child_widget_by_index(0));
+ audio_track_title->set_text(audio_track_name);
+ return true;
+ });
+ }
+
+ std::unique_ptr<List> SettingsPage::create_audio_track_title_and_remove(Subsection *audio_track_subsection, const char *title) {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
+ list->add_widget(std::make_unique<Label>(&get_theme().title_font, title, get_color_theme().text_color));
+
+ auto remove_track_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 0));
+ remove_track_button->set_icon(&get_theme().trash_texture);
+ remove_track_button->set_icon_padding_scale(0.75f);
+ remove_track_button->on_click = [this, audio_track_subsection]() {
+ audio_track_section_list_ptr->remove_widget(audio_track_subsection);
+ update_audio_track_titles(audio_track_section_list_ptr);
+ };
+ list->add_widget(std::move(remove_track_button));
+ list->set_visible(type != Type::STREAM);
+ return list;
+ }
+
+ std::unique_ptr<Subsection> SettingsPage::create_audio_track_section(Widget *parent_widget) {
+ char audio_track_name[32];
+ snprintf(audio_track_name, sizeof(audio_track_name), "Audio track #%d", 1 + (int)audio_track_section_list_ptr->get_num_children());
+
+ auto audio_input_section = create_audio_input_section();
+ List *audio_input_section_ptr = audio_input_section.get();
+
+ auto list = std::make_unique<List>(List::Orientation::VERTICAL);
+ List *list_ptr = list.get();
+ auto subsection = std::make_unique<Subsection>("", std::move(std::move(list)), mgl::vec2f(parent_widget->get_inner_size().x, 0.0f));
+ subsection->set_bg_color(mgl::Color(35, 40, 44));
+
+ list_ptr->add_widget(create_audio_track_title_and_remove(subsection.get(), audio_track_name));
+ list_ptr->add_widget(create_add_audio_buttons(audio_input_section_ptr));
+ list_ptr->add_widget(std::move(audio_input_section));
+ list_ptr->add_widget(create_application_audio_invert_checkbox());
+
+ set_application_audio_options_visible(subsection.get(), view_radio_button_ptr->get_selected_id() == "advanced", *gsr_info);
+ return subsection;
+ }
+
+ std::unique_ptr<List> SettingsPage::create_audio_track_section_list() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
- list->add_widget(create_add_audio_buttons());
- list->add_widget(create_audio_track_track_section());
+ audio_track_section_list_ptr = list.get();
return list;
}
std::unique_ptr<Widget> SettingsPage::create_audio_section() {
auto audio_device_section_list = std::make_unique<List>(List::Orientation::VERTICAL);
- audio_device_section_list->add_widget(create_audio_track_section());
- if(type != Type::STREAM)
- audio_device_section_list->add_widget(create_split_audio_checkbox());
- audio_device_section_list->add_widget(create_application_audio_invert_checkbox());
- audio_device_section_list->add_widget(create_audio_codec());
- return std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
+ List *audio_device_section_list_ptr = audio_device_section_list.get();
+
+ auto subsection = std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
+ audio_section_ptr = subsection.get();
+ audio_device_section_list_ptr->add_widget(create_add_audio_track_button());
+ audio_device_section_list_ptr->add_widget(create_audio_track_section_list());
+ audio_device_section_list_ptr->add_widget(create_audio_codec());
+ return subsection;
}
std::unique_ptr<List> SettingsPage::create_video_quality_box() {
@@ -347,13 +441,13 @@ namespace gsr {
std::unique_ptr<List> SettingsPage::create_video_bitrate_entry() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
- auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "15000", (int)(get_theme().body_font.get_character_size() * 4.0f));
+ auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "8000", (int)(get_theme().body_font.get_character_size() * 4.0f));
video_bitrate_entry->validate_handler = create_entry_validator_integer_in_range(1, 500000);
video_bitrate_entry_ptr = video_bitrate_entry.get();
list->add_widget(std::move(video_bitrate_entry));
if(type == Type::STREAM) {
- auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "1.64MB", get_color_theme().text_color);
+ auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "", get_color_theme().text_color);
Label *size_mb_label_ptr = size_mb_label.get();
list->add_widget(std::move(size_mb_label));
@@ -529,12 +623,9 @@ namespace gsr {
void SettingsPage::add_widgets() {
content_page_ptr->add_widget(create_settings());
- record_area_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) {
- (void)text;
- const bool window_selected = id == "window";
+ record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
const bool focused_selected = id == "focused";
const bool portal_selected = id == "portal";
- select_window_list_ptr->set_visible(window_selected);
area_size_list_ptr->set_visible(focused_selected);
video_resolution_list_ptr->set_visible(!focused_selected && change_video_resolution_checkbox_ptr->is_checked());
change_video_resolution_checkbox_ptr->set_visible(!focused_selected);
@@ -547,8 +638,7 @@ namespace gsr {
video_resolution_list_ptr->set_visible(!focused_selected && checked);
};
- video_quality_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) {
- (void)text;
+ video_quality_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
const bool custom_selected = id == "custom";
video_bitrate_list_ptr->set_visible(custom_selected);
@@ -560,19 +650,13 @@ namespace gsr {
video_quality_box_ptr->on_selection_changed("", video_quality_box_ptr->get_selected_id());
if(!capture_options.monitors.empty())
- record_area_box_ptr->set_selected_item(capture_options.monitors.front().name);
+ record_area_box_ptr->set_selected_item("focused_monitor");
else if(capture_options.portal)
record_area_box_ptr->set_selected_item("portal");
else if(capture_options.window)
record_area_box_ptr->set_selected_item("window");
else
record_area_box_ptr->on_selection_changed("", "");
-
- if(!gsr_info->system_info.supports_app_audio) {
- add_application_audio_button_ptr->set_visible(false);
- add_custom_application_audio_button_ptr->set_visible(false);
- application_audio_invert_checkbox_ptr->set_visible(false);
- }
}
void SettingsPage::add_page_specific_widgets() {
@@ -639,7 +723,7 @@ namespace gsr {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto replay_time_entry = std::make_unique<Entry>(&get_theme().body_font, "60", get_theme().body_font.get_character_size() * 3);
- replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 10800);
+ replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 86400);
replay_time_entry_ptr = replay_time_entry.get();
list->add_widget(std::move(replay_time_entry));
@@ -657,6 +741,24 @@ namespace gsr {
return replay_time_list;
}
+ std::unique_ptr<List> SettingsPage::create_replay_storage() {
+ auto list = std::make_unique<List>(List::Orientation::VERTICAL);
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Where should temporary replay data be stored?", get_color_theme().text_color));
+ auto replay_storage_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
+ replay_storage_button_ptr = replay_storage_button.get();
+ replay_storage_button->add_item("RAM", "ram");
+ replay_storage_button->add_item("Disk (Not recommended on SSDs)", "disk");
+
+ replay_storage_button->on_selection_changed = [this](const std::string&, const std::string &id) {
+ update_estimated_replay_file_size(id);
+ return true;
+ };
+
+ list->add_widget(std::move(replay_storage_button));
+ list->set_visible(gsr_info->system_info.gsr_version >= GsrVersion{5, 5, 0});
+ return list;
+ }
+
std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() {
char fullscreen_text[256];
snprintf(fullscreen_text, sizeof(fullscreen_text), "Turn on replay when starting a fullscreen application%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
@@ -690,13 +792,13 @@ namespace gsr {
return label;
}
- void SettingsPage::update_estimated_replay_file_size() {
+ void SettingsPage::update_estimated_replay_file_size(const std::string &replay_storage_type) {
const int64_t replay_time_seconds = atoi(replay_time_entry_ptr->get_text().c_str());
const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL;
const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024;
char buffer[256];
- snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB.\nChange video bitrate or replay duration to change file size.", video_filesize_mb);
+ snprintf(buffer, sizeof(buffer), "Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.", replay_storage_type == "ram" ? "in RAM" : "on disk", video_filesize_mb);
estimated_file_size_ptr->set_text(buffer);
}
@@ -714,6 +816,16 @@ namespace gsr {
replay_time_label_ptr->set_text(buffer);
}
+ void SettingsPage::view_changed(bool advanced_view, Subsection *notifications_subsection_ptr) {
+ color_range_list_ptr->set_visible(advanced_view);
+ audio_codec_ptr->set_visible(advanced_view);
+ video_codec_ptr->set_visible(advanced_view);
+ framerate_mode_list_ptr->set_visible(advanced_view);
+ notifications_subsection_ptr->set_visible(advanced_view);
+ set_application_audio_options_visible(audio_track_section_list_ptr, advanced_view, *gsr_info);
+ settings_scrollable_page_ptr->reset_scroll();
+ }
+
void SettingsPage::add_replay_widgets() {
auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
@@ -725,12 +837,14 @@ namespace gsr {
settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto general_list = std::make_unique<List>(List::Orientation::VERTICAL);
- general_list->add_widget(create_start_replay_automatically());
+ general_list->add_widget(create_replay_storage());
general_list->add_widget(create_save_replay_in_game_folder());
if(gsr_info->system_info.gsr_version >= GsrVersion{5, 0, 3})
general_list->add_widget(create_restart_replay_on_save());
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
+ settings_list_ptr->add_widget(std::make_unique<Subsection>("Autostart", create_start_replay_automatically(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
+
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto show_replay_started_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show replay started notification");
@@ -752,27 +866,19 @@ namespace gsr {
Subsection *notifications_subsection_ptr = notifications_subsection.get();
settings_list_ptr->add_widget(std::move(notifications_subsection));
- view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) {
- (void)text;
- const bool advanced_view = id == "advanced";
- color_range_list_ptr->set_visible(advanced_view);
- audio_codec_ptr->set_visible(advanced_view);
- video_codec_ptr->set_visible(advanced_view);
- framerate_mode_list_ptr->set_visible(advanced_view);
- notifications_subsection_ptr->set_visible(advanced_view);
- split_audio_checkbox_ptr->set_visible(advanced_view);
- settings_scrollable_page_ptr->reset_scroll();
+ view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) {
+ view_changed(id == "advanced", notifications_subsection_ptr);
return true;
};
view_radio_button_ptr->on_selection_changed("Simple", "simple");
replay_time_entry_ptr->on_changed = [this](const std::string&) {
- update_estimated_replay_file_size();
+ update_estimated_replay_file_size(replay_storage_button_ptr->get_selected_id());
update_replay_time_text();
};
video_bitrate_entry_ptr->on_changed = [this](const std::string&) {
- update_estimated_replay_file_size();
+ update_estimated_replay_file_size(replay_storage_button_ptr->get_selected_id());
};
}
@@ -822,20 +928,17 @@ namespace gsr {
show_video_saved_notification_checkbox_ptr = show_video_saved_notification_checkbox.get();
checkboxes_list->add_widget(std::move(show_video_saved_notification_checkbox));
+ auto show_video_paused_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show video paused/unpaused notification");
+ show_video_paused_notification_checkbox->set_checked(true);
+ show_video_paused_notification_checkbox_ptr = show_video_paused_notification_checkbox.get();
+ checkboxes_list->add_widget(std::move(show_video_paused_notification_checkbox));
+
auto notifications_subsection = std::make_unique<Subsection>("Notifications", std::move(checkboxes_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
Subsection *notifications_subsection_ptr = notifications_subsection.get();
settings_list_ptr->add_widget(std::move(notifications_subsection));
- view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) {
- (void)text;
- const bool advanced_view = id == "advanced";
- color_range_list_ptr->set_visible(advanced_view);
- audio_codec_ptr->set_visible(advanced_view);
- video_codec_ptr->set_visible(advanced_view);
- framerate_mode_list_ptr->set_visible(advanced_view);
- notifications_subsection_ptr->set_visible(advanced_view);
- split_audio_checkbox_ptr->set_visible(advanced_view);
- settings_scrollable_page_ptr->reset_scroll();
+ view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) {
+ view_changed(id == "advanced", notifications_subsection_ptr);
return true;
};
view_radio_button_ptr->on_selection_changed("Simple", "simple");
@@ -849,6 +952,7 @@ namespace gsr {
auto streaming_service_box = std::make_unique<ComboBox>(&get_theme().body_font);
streaming_service_box->add_item("Twitch", "twitch");
streaming_service_box->add_item("YouTube", "youtube");
+ streaming_service_box->add_item("Rumble", "rumble");
streaming_service_box->add_item("Custom", "custom");
streaming_service_box_ptr = streaming_service_box.get();
return streaming_service_box;
@@ -873,6 +977,10 @@ namespace gsr {
youtube_stream_key_entry_ptr = youtube_stream_key_entry.get();
stream_key_list->add_widget(std::move(youtube_stream_key_entry));
+ auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
+ rumble_stream_key_entry_ptr = rumble_stream_key_entry.get();
+ stream_key_list->add_widget(std::move(rumble_stream_key_entry));
+
stream_key_list_ptr = stream_key_list.get();
return stream_key_list;
}
@@ -931,29 +1039,23 @@ namespace gsr {
Subsection *notifications_subsection_ptr = notifications_subsection.get();
settings_list_ptr->add_widget(std::move(notifications_subsection));
- streaming_service_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) {
- (void)text;
+ streaming_service_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
const bool twitch_option = id == "twitch";
const bool youtube_option = id == "youtube";
+ const bool rumble_option = id == "rumble";
const bool custom_option = id == "custom";
stream_key_list_ptr->set_visible(!custom_option);
stream_url_list_ptr->set_visible(custom_option);
container_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->set_visible(twitch_option);
youtube_stream_key_entry_ptr->set_visible(youtube_option);
+ rumble_stream_key_entry_ptr->set_visible(rumble_option);
return true;
};
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");
- view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) {
- (void)text;
- const bool advanced_view = id == "advanced";
- color_range_list_ptr->set_visible(advanced_view);
- audio_codec_ptr->set_visible(advanced_view);
- video_codec_ptr->set_visible(advanced_view);
- framerate_mode_list_ptr->set_visible(advanced_view);
- notifications_subsection_ptr->set_visible(advanced_view);
- settings_scrollable_page_ptr->reset_scroll();
+ view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) {
+ view_changed(id == "advanced", notifications_subsection_ptr);
return true;
};
view_radio_button_ptr->on_selection_changed("Simple", "simple");
@@ -1004,50 +1106,64 @@ namespace gsr {
return nullptr;
}
- static bool starts_with(std::string_view str, const char *substr) {
- size_t len = strlen(substr);
- return str.size() >= len && memcmp(str.data(), substr, len) == 0;
- }
-
void SettingsPage::load_audio_tracks(const RecordOptions &record_options) {
- audio_track_list_ptr->clear();
- for(const std::string &audio_track : record_options.audio_tracks) {
- if(starts_with(audio_track, "app:")) {
- if(!gsr_info->system_info.supports_app_audio)
- continue;
-
- std::string audio_track_name = audio_track.substr(4);
- const std::string *app_audio = get_application_audio_by_name_case_insensitive(application_audio, audio_track_name);
- if(app_audio) {
- std::unique_ptr<List> application_audio_widget = create_application_audio();
- ComboBox *application_audio_box = static_cast<ComboBox*>(application_audio_widget->get_child_widget_by_index(1));
- application_audio_box->set_selected_item(*app_audio);
- audio_track_list_ptr->add_widget(std::move(application_audio_widget));
+ audio_track_section_list_ptr->clear();
+ for(const AudioTrack &audio_track : record_options.audio_tracks_list) {
+ auto audio_track_section = create_audio_track_section(audio_section_ptr);
+ List *audio_track_section_items_list_ptr = dynamic_cast<List*>(audio_track_section->get_inner_widget());
+ List *audio_input_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(2));
+ CheckBox *application_audio_invert_checkbox_ptr = dynamic_cast<CheckBox*>(audio_track_section_items_list_ptr->get_child_widget_by_index(3));
+ application_audio_invert_checkbox_ptr->set_checked(audio_track.application_audio_invert);
+
+ audio_input_list_ptr->clear();
+ for(const std::string &audio_input : audio_track.audio_inputs) {
+ if(starts_with(audio_input, "app:")) {
+ if(!gsr_info->system_info.supports_app_audio)
+ continue;
+
+ std::string audio_track_name = audio_input.substr(4);
+ const std::string *app_audio = get_application_audio_by_name_case_insensitive(application_audio, audio_track_name);
+ if(app_audio) {
+ std::unique_ptr<List> application_audio_widget = create_application_audio(audio_input_list_ptr);
+ ComboBox *application_audio_box = dynamic_cast<ComboBox*>(application_audio_widget->get_child_widget_by_index(1));
+ application_audio_box->set_selected_item(*app_audio);
+ audio_input_list_ptr->add_widget(std::move(application_audio_widget));
+ } else {
+ std::unique_ptr<List> application_audio_widget = create_custom_application_audio(audio_input_list_ptr);
+ Entry *application_audio_entry = dynamic_cast<Entry*>(application_audio_widget->get_child_widget_by_index(1));
+ application_audio_entry->set_text(std::move(audio_track_name));
+ audio_input_list_ptr->add_widget(std::move(application_audio_widget));
+ }
+ } else if(starts_with(audio_input, "device:")) {
+ const std::string device_name = audio_input.substr(7);
+ const AudioDeviceType audio_device_type = audio_device_is_output(device_name) ? AudioDeviceType::OUTPUT : AudioDeviceType::INPUT;
+ std::unique_ptr<List> audio_track_widget = create_audio_device(audio_device_type, audio_input_list_ptr);
+ ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1));
+ audio_device_box->set_selected_item(device_name);
+ audio_input_list_ptr->add_widget(std::move(audio_track_widget));
} else {
- std::unique_ptr<List> application_audio_widget = create_custom_application_audio();
- Entry *application_audio_entry = static_cast<Entry*>(application_audio_widget->get_child_widget_by_index(1));
- application_audio_entry->set_text(std::move(audio_track_name));
- audio_track_list_ptr->add_widget(std::move(application_audio_widget));
+ const AudioDeviceType audio_device_type = audio_device_is_output(audio_input) ? AudioDeviceType::OUTPUT : AudioDeviceType::INPUT;
+ std::unique_ptr<List> audio_track_widget = create_audio_device(audio_device_type, audio_input_list_ptr);
+ ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1));
+ audio_device_box->set_selected_item(audio_input);
+ audio_input_list_ptr->add_widget(std::move(audio_track_widget));
}
- } else if(starts_with(audio_track, "device:")) {
- std::unique_ptr<List> audio_track_widget = create_audio_device();
- ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1));
- audio_device_box->set_selected_item(audio_track.substr(7));
- audio_track_list_ptr->add_widget(std::move(audio_track_widget));
- } else {
- std::unique_ptr<List> audio_track_widget = create_audio_device();
- ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1));
- audio_device_box->set_selected_item(audio_track);
- audio_track_list_ptr->add_widget(std::move(audio_track_widget));
}
+
+ audio_track_section_list_ptr->add_widget(std::move(audio_track_section));
+
+ if(type == Type::STREAM)
+ break;
+ }
+
+ if(type == Type::STREAM && audio_track_section_list_ptr->get_num_children() == 0) {
+ auto audio_track_section = create_audio_track_section(audio_section_ptr);
+ audio_track_section_list_ptr->add_widget(std::move(audio_track_section));
}
}
void SettingsPage::load_common(RecordOptions &record_options) {
record_area_box_ptr->set_selected_item(record_options.record_area_option);
- if(split_audio_checkbox_ptr)
- split_audio_checkbox_ptr->set_checked(!record_options.merge_audio_tracks);
- application_audio_invert_checkbox_ptr->set_checked(record_options.application_audio_invert);
change_video_resolution_checkbox_ptr->set_checked(record_options.change_video_resolution);
load_audio_tracks(record_options);
color_range_box_ptr->set_selected_item(record_options.color_range);
@@ -1100,6 +1216,7 @@ namespace gsr {
void SettingsPage::load_replay() {
load_common(config.replay_config.record_options);
+ replay_storage_button_ptr->set_selected_item(config.replay_config.replay_storage);
turn_on_replay_automatically_mode_ptr->set_selected_item(config.replay_config.turn_on_replay_automatically_mode);
save_replay_in_game_folder_ptr->set_checked(config.replay_config.save_video_in_game_folder);
if(restart_replay_on_save)
@@ -1112,8 +1229,8 @@ namespace gsr {
if(config.replay_config.replay_time < 2)
config.replay_config.replay_time = 2;
- if(config.replay_config.replay_time > 10800)
- config.replay_config.replay_time = 10800;
+ if(config.replay_config.replay_time > 86400)
+ config.replay_config.replay_time = 86400;
replay_time_entry_ptr->set_text(std::to_string(config.replay_config.replay_time));
}
@@ -1122,6 +1239,7 @@ namespace gsr {
save_recording_in_game_folder_ptr->set_checked(config.record_config.save_video_in_game_folder);
show_recording_started_notification_checkbox_ptr->set_checked(config.record_config.show_recording_started_notifications);
show_video_saved_notification_checkbox_ptr->set_checked(config.record_config.show_video_saved_notifications);
+ show_video_paused_notification_checkbox_ptr->set_checked(config.record_config.show_video_paused_notifications);
save_directory_button_ptr->set_text(config.record_config.save_directory);
container_box_ptr->set_selected_item(config.record_config.container);
}
@@ -1133,32 +1251,43 @@ namespace gsr {
streaming_service_box_ptr->set_selected_item(config.streaming_config.streaming_service);
youtube_stream_key_entry_ptr->set_text(config.streaming_config.youtube.stream_key);
twitch_stream_key_entry_ptr->set_text(config.streaming_config.twitch.stream_key);
+ rumble_stream_key_entry_ptr->set_text(config.streaming_config.rumble.stream_key);
stream_url_entry_ptr->set_text(config.streaming_config.custom.url);
container_box_ptr->set_selected_item(config.streaming_config.custom.container);
}
- static void save_audio_tracks(std::vector<std::string> &audio_devices, List *audio_devices_list_ptr) {
- audio_devices.clear();
- audio_devices_list_ptr->for_each_child_widget([&audio_devices](std::unique_ptr<Widget> &child_widget) {
- List *audio_track_line = static_cast<List*>(child_widget.get());
- const AudioTrackType audio_track_type = (AudioTrackType)(uintptr_t)audio_track_line->userdata;
- switch(audio_track_type) {
- case AudioTrackType::DEVICE: {
- ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1));
- audio_devices.push_back("device:" + audio_device_box->get_selected_id());
- break;
- }
- case AudioTrackType::APPLICATION: {
- ComboBox *application_audio_box = static_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1));
- audio_devices.push_back("app:" + application_audio_box->get_selected_id());
- break;
- }
- case AudioTrackType::APPLICATION_CUSTOM: {
- Entry *application_audio_entry = static_cast<Entry*>(audio_track_line->get_child_widget_by_index(1));
- audio_devices.push_back("app:" + application_audio_entry->get_text());
- break;
+ static void save_audio_tracks(std::vector<AudioTrack> &audio_tracks, List *audio_track_section_list_ptr) {
+ audio_tracks.clear();
+ audio_track_section_list_ptr->for_each_child_widget([&audio_tracks](std::unique_ptr<Widget> &child_widget) {
+ Subsection *audio_subsection = dynamic_cast<Subsection*>(child_widget.get());
+ List *audio_track_section_items_list_ptr = dynamic_cast<List*>(audio_subsection->get_inner_widget());
+ List *audio_input_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(2));
+ CheckBox *application_audio_invert_checkbox_ptr = dynamic_cast<CheckBox*>(audio_track_section_items_list_ptr->get_child_widget_by_index(3));
+
+ audio_tracks.push_back({std::vector<std::string>{}, application_audio_invert_checkbox_ptr->is_checked()});
+ audio_input_list_ptr->for_each_child_widget([&audio_tracks](std::unique_ptr<Widget> &child_widget){
+ List *audio_track_line = dynamic_cast<List*>(child_widget.get());
+ const AudioTrackType audio_track_type = (AudioTrackType)(uintptr_t)audio_track_line->userdata;
+ switch(audio_track_type) {
+ case AudioTrackType::DEVICE: {
+ ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1));
+ audio_tracks.back().audio_inputs.push_back("device:" + audio_device_box->get_selected_id());
+ break;
+ }
+ case AudioTrackType::APPLICATION: {
+ ComboBox *application_audio_box = dynamic_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1));
+ audio_tracks.back().audio_inputs.push_back("app:" + application_audio_box->get_selected_id());
+ break;
+ }
+ case AudioTrackType::APPLICATION_CUSTOM: {
+ Entry *application_audio_entry = dynamic_cast<Entry*>(audio_track_line->get_child_widget_by_index(1));
+ audio_tracks.back().audio_inputs.push_back("app:" + application_audio_entry->get_text());
+ break;
+ }
}
- }
+ return true;
+ });
+
return true;
});
}
@@ -1171,11 +1300,8 @@ namespace gsr {
record_options.video_height = atoi(video_height_entry_ptr->get_text().c_str());
record_options.fps = atoi(framerate_entry_ptr->get_text().c_str());
record_options.video_bitrate = atoi(video_bitrate_entry_ptr->get_text().c_str());
- if(split_audio_checkbox_ptr)
- record_options.merge_audio_tracks = !split_audio_checkbox_ptr->is_checked();
- record_options.application_audio_invert = application_audio_invert_checkbox_ptr->is_checked();
record_options.change_video_resolution = change_video_resolution_checkbox_ptr->is_checked();
- save_audio_tracks(record_options.audio_tracks, audio_track_list_ptr);
+ save_audio_tracks(record_options.audio_tracks_list, audio_track_section_list_ptr);
record_options.color_range = color_range_box_ptr->get_selected_id();
record_options.video_quality = video_quality_box_ptr->get_selected_id();
record_options.video_codec = video_codec_box_ptr->get_selected_id();
@@ -1242,6 +1368,7 @@ namespace gsr {
config.replay_config.save_directory = save_directory_button_ptr->get_text();
config.replay_config.container = container_box_ptr->get_selected_id();
config.replay_config.replay_time = atoi(replay_time_entry_ptr->get_text().c_str());
+ config.replay_config.replay_storage = replay_storage_button_ptr->get_selected_id();
if(config.replay_config.replay_time < 5) {
config.replay_config.replay_time = 5;
@@ -1254,6 +1381,7 @@ namespace gsr {
config.record_config.save_video_in_game_folder = save_recording_in_game_folder_ptr->is_checked();
config.record_config.show_recording_started_notifications = show_recording_started_notification_checkbox_ptr->is_checked();
config.record_config.show_video_saved_notifications = show_video_saved_notification_checkbox_ptr->is_checked();
+ config.record_config.show_video_paused_notifications = show_video_paused_notification_checkbox_ptr->is_checked();
config.record_config.save_directory = save_directory_button_ptr->get_text();
config.record_config.container = container_box_ptr->get_selected_id();
}
@@ -1265,6 +1393,7 @@ namespace gsr {
config.streaming_config.streaming_service = streaming_service_box_ptr->get_selected_id();
config.streaming_config.youtube.stream_key = youtube_stream_key_entry_ptr->get_text();
config.streaming_config.twitch.stream_key = twitch_stream_key_entry_ptr->get_text();
+ config.streaming_config.rumble.stream_key = rumble_stream_key_entry_ptr->get_text();
config.streaming_config.custom.url = stream_url_entry_ptr->get_text();
config.streaming_config.custom.container = container_box_ptr->get_selected_id();
}
diff --git a/src/gui/StaticPage.cpp b/src/gui/StaticPage.cpp
index 182464c..5147819 100644
--- a/src/gui/StaticPage.cpp
+++ b/src/gui/StaticPage.cpp
@@ -20,8 +20,9 @@ namespace gsr {
// Process widgets by visibility (backwards)
return widgets.for_each_reverse([selected_widget, &window, &event, offset](std::unique_ptr<Widget> &widget) {
- if(widget.get() != selected_widget) {
- if(!widget->on_event(event, window, offset))
+ Widget *p = widget.get();
+ if(p != selected_widget) {
+ if(!p->on_event(event, window, offset))
return false;
}
return true;
diff --git a/src/gui/Subsection.cpp b/src/gui/Subsection.cpp
index c97460e..bc75a9c 100644
--- a/src/gui/Subsection.cpp
+++ b/src/gui/Subsection.cpp
@@ -12,12 +12,17 @@ namespace gsr {
static const float title_spacing_scale = 0.010f;
Subsection::Subsection(const char *title, std::unique_ptr<Widget> inner_widget, mgl::vec2f size) :
- label(&get_theme().title_font, title, get_color_theme().text_color),
+ label(&get_theme().title_font, title ? title : "", get_color_theme().text_color),
inner_widget(std::move(inner_widget)),
size(size)
{
this->inner_widget->parent_widget = this;
}
+
+ Subsection::~Subsection() {
+ if(inner_widget->parent_widget == this)
+ inner_widget->parent_widget = nullptr;
+ }
bool Subsection::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f) {
if(!visible)
@@ -32,7 +37,7 @@ namespace gsr {
mgl::vec2f draw_pos = position + offset;
mgl::Rectangle background(draw_pos.floor(), get_size().floor());
- background.set_color(mgl::Color(25, 30, 34));
+ background.set_color(bg_color);
window.draw(background);
draw_pos += mgl::vec2f(margin_left_scale, margin_top_scale) * mgl::vec2f(get_theme().window_height, get_theme().window_height);
@@ -69,4 +74,12 @@ namespace gsr {
const mgl::vec2f margin_size = mgl::vec2f(margin_left_scale + margin_right_scale, margin_top_scale + margin_bottom_scale) * mgl::vec2f(get_theme().window_height, get_theme().window_height);
return get_size() - margin_size;
}
+
+ Widget* Subsection::get_inner_widget() {
+ return inner_widget.get();
+ }
+
+ void Subsection::set_bg_color(mgl::Color color) {
+ bg_color = color;
+ }
} \ No newline at end of file
diff --git a/src/gui/Widget.cpp b/src/gui/Widget.cpp
index 8732bd7..66cf193 100644
--- a/src/gui/Widget.cpp
+++ b/src/gui/Widget.cpp
@@ -1,14 +1,15 @@
#include "../../include/gui/Widget.hpp"
+#include <vector>
namespace gsr {
+ static std::vector<std::unique_ptr<Widget>> widgets_to_remove;
+
Widget::Widget() {
}
Widget::~Widget() {
remove_widget_as_selected_in_parent();
- // if(parent_widget)
- // parent_widget->remove_child_widget(this);
}
void Widget::set_position(mgl::vec2f position) {
@@ -62,4 +63,15 @@ namespace gsr {
void Widget::set_visible(bool visible) {
this->visible = visible;
}
+
+ void add_widget_to_remove(std::unique_ptr<Widget> widget) {
+ widgets_to_remove.push_back(std::move(widget));
+ }
+
+ void remove_widgets_to_be_removed() {
+ for(size_t i = 0; i < widgets_to_remove.size(); ++i) {
+ widgets_to_remove[i].reset();
+ }
+ widgets_to_remove.clear();
+ }
} \ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 7c10a6e..a68ff7d 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -30,6 +30,10 @@ static void sigint_handler(int signal) {
running = 0;
}
+static void signal_ignore(int) {
+
+}
+
static void disable_prime_run() {
unsetenv("__NV_PRIME_RENDER_OFFLOAD");
unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER");
@@ -74,6 +78,16 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
overlay->save_replay();
});
+ rpc->add_handler("replay-save-1-min", [overlay](const std::string &name) {
+ fprintf(stderr, "rpc command executed: %s\n", name.c_str());
+ overlay->save_replay_1_min();
+ });
+
+ rpc->add_handler("replay-save-10-min", [overlay](const std::string &name) {
+ fprintf(stderr, "rpc command executed: %s\n", name.c_str());
+ overlay->save_replay_10_min();
+ });
+
rpc->add_handler("take-screenshot", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->take_screenshot();
@@ -145,18 +159,35 @@ static bool is_flatpak() {
return getenv("FLATPAK_ID") != nullptr;
}
+static void set_display_server_environment_variables() {
+ // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY)
+ const char *display = getenv("DISPLAY");
+ if(!display) {
+ display = ":0";
+ setenv("DISPLAY", display, true);
+ }
+
+ const char *wayland_display = getenv("WAYLAND_DISPLAY");
+ if(!wayland_display) {
+ wayland_display = "wayland-1";
+ setenv("WAYLAND_DISPLAY", wayland_display, true);
+ }
+}
+
static void usage() {
printf("usage: gsr-ui [action]\n");
printf("OPTIONS:\n");
- printf(" action The launch action. Should be either \"launch-show\" or \"launch-hide\". Optional, defaults to \"launch-hide\".\n");
+ printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\" or \"launch-daemon\". Optional, defaults to \"launch-hide\".\n");
printf(" If \"launch-show\" is used then the program starts and the UI is immediately opened and can be shown/hidden with Alt+Z.\n");
- printf(" If \"launch-hide\" is used then the program starts but the UI is not opened until Alt+Z is pressed.\n");
+ printf(" If \"launch-hide\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will be opened if the program is already running in another process.\n");
+ printf(" If \"launch-daemon\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will not be opened if the program is already running in another process.\n");
exit(1);
}
enum class LaunchAction {
LAUNCH_SHOW,
- LAUNCH_HIDE
+ LAUNCH_HIDE,
+ LAUNCH_DAEMON
};
int main(int argc, char **argv) {
@@ -177,18 +208,17 @@ int main(int argc, char **argv) {
launch_action = LaunchAction::LAUNCH_SHOW;
} else if(strcmp(launch_action_opt, "launch-hide") == 0) {
launch_action = LaunchAction::LAUNCH_HIDE;
+ } else if(strcmp(launch_action_opt, "launch-daemon") == 0) {
+ launch_action = LaunchAction::LAUNCH_DAEMON;
} else {
- printf("error: invalid action \"%s\", expected \"launch-show\" or \"launch-hide\".\n", launch_action_opt);
+ printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\" or \"launch-daemon\".\n", launch_action_opt);
usage();
}
} else {
usage();
}
- if(is_flatpak())
- install_flatpak_systemd_service();
- else
- remove_flatpak_systemd_service();
+ set_display_server_environment_variables();
// TODO: This is a shitty method to detect if multiple instances of gsr-ui is running but this will work properly even in flatpak
// that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform.
@@ -196,6 +226,9 @@ int main(int argc, char **argv) {
// What do? creating a pid file doesn't work in flatpak either.
// TODO: This doesn't work in flatpak when disabling hotkeys.
if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) {
+ if(launch_action == LaunchAction::LAUNCH_DAEMON)
+ return 1;
+
gsr::Rpc rpc;
if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) {
fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n");
@@ -207,6 +240,16 @@ int main(int argc, char **argv) {
return 1;
}
+ if(gsr::pidof("gpu-screen-recorder", getpid()) != -1) {
+ const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
+ gsr::exec_program_daemonized(args);
+ }
+
+ if(is_flatpak())
+ install_flatpak_systemd_service();
+ else
+ remove_flatpak_systemd_service();
+
// 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,
@@ -218,6 +261,16 @@ int main(int argc, char **argv) {
unsetenv("vblank_mode");
signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+ signal(SIGUSR1, signal_ignore);
+ signal(SIGUSR2, signal_ignore);
+ signal(SIGRTMIN, signal_ignore);
+ signal(SIGRTMIN+1, signal_ignore);
+ signal(SIGRTMIN+2, signal_ignore);
+ signal(SIGRTMIN+3, signal_ignore);
+ signal(SIGRTMIN+4, signal_ignore);
+ signal(SIGRTMIN+5, signal_ignore);
+ signal(SIGRTMIN+6, signal_ignore);
gsr::GsrInfo gsr_info;
// TODO: Show the error in ui
@@ -235,7 +288,7 @@ int main(int argc, char **argv) {
disable_prime_run();
}
- if(mgl_init() != 0) {
+ if(mgl_init(MGL_WINDOW_SYSTEM_X11) != 0) {
fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n");
exit(1);
}
@@ -303,10 +356,10 @@ int main(int argc, char **argv) {
if(exit_reason == "back-to-old-ui") {
const char *args[] = { "gpu-screen-recorder-gtk", "use-old-ui", nullptr };
execvp(args[0], (char* const*)args);
- } else if(exit_reason == "restart") {
- const char *args[] = { "gsr-ui", "launch-show", nullptr };
- execvp(args[0], (char* const*)args);
+ return 0;
+ } else if(exit_reason == "exit") {
+ return 0;
}
- return 0;
+ return mgl_is_connected_to_display_server() ? 0 : 1;
}
diff --git a/tools/gsr-global-hotkeys/README.md b/tools/gsr-global-hotkeys/README.md
index 8744107..38585c1 100644
--- a/tools/gsr-global-hotkeys/README.md
+++ b/tools/gsr-global-hotkeys/README.md
@@ -18,4 +18,10 @@ To unbind all keys send `unbind_all<newline>` to the programs stdin, for example
```
unbind_all
+```
+## Exit
+To close gsr-global-hotkeys send `exit<newline>` to the programs stdin, for example:
+```
+exit
+
``` \ No newline at end of file
diff --git a/tools/gsr-global-hotkeys/keyboard_event.c b/tools/gsr-global-hotkeys/keyboard_event.c
index e5221dc..4ff7f11 100644
--- a/tools/gsr-global-hotkeys/keyboard_event.c
+++ b/tools/gsr-global-hotkeys/keyboard_event.c
@@ -69,7 +69,7 @@ static void keyboard_event_fetch_update_key_states(keyboard_event *self, event_e
if(ioctl(fd, EVIOCGKEY(KEY_STATES_SIZE), extra_data->key_states) == -1)
fprintf(stderr, "Warning: failed to fetch key states for device: /dev/input/event%d\n", extra_data->dev_input_id);
- if(!keyboard_event_has_exclusive_grab(self) || extra_data->grabbed)
+ if(!keyboard_event_has_exclusive_grab(self) || extra_data->grabbed || extra_data->is_non_keyboard_device)
return;
extra_data->num_keys_pressed = keyboard_event_get_num_keys_pressed(extra_data->key_states);
@@ -106,7 +106,7 @@ static void keyboard_event_process_key_state_change(keyboard_event *self, const
extra_data->key_states[byte_index] = key_byte_state;
- if(!keyboard_event_has_exclusive_grab(self) || extra_data->grabbed)
+ if(!keyboard_event_has_exclusive_grab(self) || extra_data->grabbed || extra_data->is_non_keyboard_device)
return;
if(extra_data->num_keys_pressed == 0) {
@@ -193,7 +193,8 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
//fprintf(stderr, "fd: %d, type: %d, pressed %d, value: %d\n", fd, event.type, event.code, event.value);
//}
- if(event.type == EV_KEY && is_keyboard_key(event.code)) {
+ const bool keyboard_key = is_keyboard_key(event.code);
+ if(event.type == EV_KEY && keyboard_key) {
keyboard_event_process_key_state_change(self, &event, extra_data, fd);
const uint32_t modifier_bit = keycode_to_modifier_bit(event.code);
if(modifier_bit == 0) {
@@ -214,6 +215,20 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
if(write(self->uinput_fd, &event, sizeof(event)) != sizeof(event))
fprintf(stderr, "Error: failed to write event data to virtual keyboard for exclusively grabbed device\n");
}
+
+ if(!extra_data->is_possibly_non_keyboard_device)
+ return;
+
+ /* TODO: What if some key is being pressed down while this is done? will it remain pressed down? */
+ if(!extra_data->is_non_keyboard_device && (event.type == EV_REL || event.type == EV_ABS || (event.type == EV_KEY && !keyboard_key))) {
+ fprintf(stderr, "Info: device /dev/input/event%d is likely a non-keyboard device as it received a non-keyboard event. This device will be ignored\n", extra_data->dev_input_id);
+ extra_data->is_non_keyboard_device = true;
+ if(extra_data->grabbed) {
+ extra_data->grabbed = false;
+ ioctl(fd, EVIOCGRAB, 0);
+ fprintf(stderr, "Info: ungrabbed device: /dev/input/event%d\n", extra_data->dev_input_id);
+ }
+ }
}
/* Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only */
@@ -292,6 +307,46 @@ static bool dev_input_is_virtual(int dev_input_id) {
return is_virtual;
}
+static inline bool supports_key(unsigned char *key_bits, unsigned int key) {
+ return key_bits[key/8] & (1 << (key % 8));
+}
+
+static bool supports_keyboard_keys(unsigned char *key_bits) {
+ const int keys[2] = { KEY_A, KEY_ESC };
+ for(int i = 0; i < 2; ++i) {
+ if(supports_key(key_bits, keys[i]))
+ return true;
+ }
+ return false;
+}
+
+static bool supports_mouse_keys(unsigned char *key_bits) {
+ const int keys[2] = { BTN_MOUSE, BTN_LEFT };
+ for(int i = 0; i < 2; ++i) {
+ if(supports_key(key_bits, keys[i]))
+ return true;
+ }
+ return false;
+}
+
+static bool supports_joystick_keys(unsigned char *key_bits) {
+ const int keys[9] = { BTN_JOYSTICK, BTN_A, BTN_B, BTN_X, BTN_Y, BTN_SELECT, BTN_START, BTN_SELECT, BTN_TRIGGER_HAPPY1 };
+ for(int i = 0; i < 9; ++i) {
+ if(supports_key(key_bits, keys[i]))
+ return true;
+ }
+ return false;
+}
+
+static bool supports_wheel_keys(unsigned char *key_bits) {
+ const int keys[2] = { BTN_WHEEL, BTN_GEAR_DOWN };
+ for(int i = 0; i < 2; ++i) {
+ if(supports_key(key_bits, keys[i]))
+ return true;
+ }
+ return false;
+}
+
static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, const char *dev_input_filepath) {
const int dev_input_id = get_dev_input_id_from_filepath(dev_input_filepath);
if(dev_input_id == -1)
@@ -320,15 +375,11 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
unsigned char key_bits[KEY_MAX/8 + 1] = {0};
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits);
- const bool supports_key_a = key_bits[KEY_A/8] & (1 << (KEY_A % 8));
- const bool supports_key_esc = key_bits[KEY_ESC/8] & (1 << (KEY_ESC % 8));
- const bool supports_key_volume_up = key_bits[KEY_VOLUMEUP/8] & (1 << (KEY_VOLUMEUP % 8));
- const bool supports_key_events = supports_key_a || supports_key_esc || supports_key_volume_up;
+ const bool supports_key_events = supports_keyboard_keys(key_bits);
+ const bool supports_mouse_events = supports_mouse_keys(key_bits);
+ const bool supports_joystick_events = supports_joystick_keys(key_bits);
+ const bool supports_wheel_events = supports_wheel_keys(key_bits);
- const bool supports_mouse_events = key_bits[BTN_MOUSE/8] & (1 << (BTN_MOUSE % 8));
- //const bool supports_touch_events = key_bits[BTN_TOUCH/8] & (1 << (BTN_TOUCH % 8));
- const bool supports_joystick_events = key_bits[BTN_JOYSTICK/8] & (1 << (BTN_JOYSTICK % 8));
- const bool supports_wheel_events = key_bits[BTN_WHEEL/8] & (1 << (BTN_WHEEL % 8));
if(supports_key_events && (is_virtual_device || (!supports_joystick_events && !supports_wheel_events))) {
unsigned char *key_states = calloc(1, KEY_STATES_SIZE);
unsigned char *key_presses_grabbed = calloc(1, KEY_STATES_SIZE);
@@ -349,6 +400,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
};
if(supports_mouse_events || supports_joystick_events || supports_wheel_events) {
+ self->event_extra_data[self->num_event_polls].is_possibly_non_keyboard_device = true;
fprintf(stderr, "Info: device not grabbed yet because it might be a mouse: /dev/input/event%d\n", dev_input_id);
fsync(fd);
if(ioctl(fd, EVIOCGKEY(KEY_STATES_SIZE), self->event_extra_data[self->num_event_polls].key_states) == -1)
@@ -404,8 +456,10 @@ static void keyboard_event_remove_event(keyboard_event *self, int index) {
if(index < 0 || index >= self->num_event_polls)
return;
- ioctl(self->event_polls[index].fd, EVIOCGRAB, 0);
- close(self->event_polls[index].fd);
+ if(self->event_polls[index].fd > 0) {
+ ioctl(self->event_polls[index].fd, EVIOCGRAB, 0);
+ close(self->event_polls[index].fd);
+ }
free(self->event_extra_data[index].key_states);
free(self->event_extra_data[index].key_presses_grabbed);
@@ -435,19 +489,20 @@ static int setup_virtual_keyboard_input(const char *name) {
success &= (ioctl(fd, UI_SET_EVBIT, EV_KEY) != -1);
success &= (ioctl(fd, UI_SET_EVBIT, EV_REP) != -1);
success &= (ioctl(fd, UI_SET_EVBIT, EV_REL) != -1);
- success &= (ioctl(fd, UI_SET_EVBIT, EV_LED) != -1);
+ //success &= (ioctl(fd, UI_SET_EVBIT, EV_LED) != -1);
success &= (ioctl(fd, UI_SET_MSCBIT, MSC_SCAN) != -1);
for(int i = 1; i < KEY_MAX; ++i) {
+ // TODO: Check for joystick button? if we accidentally grab joystick
if(is_keyboard_key(i) || is_mouse_button(i))
success &= (ioctl(fd, UI_SET_KEYBIT, i) != -1);
}
for(int i = 0; i < REL_MAX; ++i) {
success &= (ioctl(fd, UI_SET_RELBIT, i) != -1);
}
- for(int i = 0; i < LED_MAX; ++i) {
- success &= (ioctl(fd, UI_SET_LEDBIT, i) != -1);
- }
+ // for(int i = 0; i < LED_MAX; ++i) {
+ // success &= (ioctl(fd, UI_SET_LEDBIT, i) != -1);
+ // }
// success &= (ioctl(fd, UI_SET_EVBIT, EV_ABS) != -1);
// success &= (ioctl(fd, UI_SET_ABSBIT, ABS_X) != -1);
@@ -566,8 +621,10 @@ void keyboard_event_deinit(keyboard_event *self) {
}
for(int i = 0; i < self->num_event_polls; ++i) {
- ioctl(self->event_polls[i].fd, EVIOCGRAB, 0);
- close(self->event_polls[i].fd);
+ if(self->event_polls[i].fd > 0) {
+ ioctl(self->event_polls[i].fd, EVIOCGRAB, 0);
+ close(self->event_polls[i].fd);
+ }
free(self->event_extra_data[i].key_states);
free(self->event_extra_data[i].key_presses_grabbed);
}
@@ -707,8 +764,11 @@ static void keyboard_event_parse_stdin_command(keyboard_event *self, const char
}
self->num_global_hotkeys = 0;
fprintf(stderr, "Info: unbinded all hotkeys\n");
+ } else if(strncmp(command, "exit", 4) == 0) {
+ self->stdin_failed = true;
+ fprintf(stderr, "Info: received exit command\n");
} else {
- fprintf(stderr, "Warning: got invalid command: \"%s\", expected command to start with either \"bind\" or \"unbind_all\"\n", command);
+ fprintf(stderr, "Warning: got invalid command: \"%s\", expected command to start with either \"bind\", \"unbind_all\" or \"exit\"\n", command);
}
}
diff --git a/tools/gsr-global-hotkeys/keyboard_event.h b/tools/gsr-global-hotkeys/keyboard_event.h
index 720aaa1..4a4c1fd 100644
--- a/tools/gsr-global-hotkeys/keyboard_event.h
+++ b/tools/gsr-global-hotkeys/keyboard_event.h
@@ -39,6 +39,8 @@ typedef enum {
typedef struct {
int dev_input_id;
bool grabbed;
+ bool is_non_keyboard_device;
+ bool is_possibly_non_keyboard_device;
unsigned char *key_states;
unsigned char *key_presses_grabbed;
int num_keys_pressed;
diff --git a/tools/gsr-global-hotkeys/main.c b/tools/gsr-global-hotkeys/main.c
index 41e5ca5..c7e0403 100644
--- a/tools/gsr-global-hotkeys/main.c
+++ b/tools/gsr-global-hotkeys/main.c
@@ -44,42 +44,42 @@ int main(int argc, char **argv) {
} else if(strcmp(grab_type_arg, "--virtual") == 0) {
grab_type = KEYBOARD_GRAB_TYPE_VIRTUAL;
} else {
- fprintf(stderr, "Error: expected --all or --virtual, got %s\n", grab_type_arg);
+ fprintf(stderr, "gsr-global-hotkeys error: expected --all or --virtual, got %s\n", grab_type_arg);
usage();
return 1;
}
} else if(argc != 1) {
- fprintf(stderr, "Error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
+ fprintf(stderr, "gsr-global-hotkeys error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
usage();
return 1;
}
if(is_gsr_global_hotkeys_already_running()) {
- fprintf(stderr, "Error: gsr-global-hotkeys is already running\n");
+ fprintf(stderr, "gsr-global-hotkeys error: gsr-global-hotkeys is already running\n");
return 1;
}
const uid_t user_id = getuid();
if(geteuid() != 0) {
if(setuid(0) == -1) {
- fprintf(stderr, "Error: failed to change user to root\n");
+ fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");
return 1;
}
}
keyboard_event keyboard_ev;
if(!keyboard_event_init(&keyboard_ev, true, grab_type)) {
- fprintf(stderr, "Error: failed to setup hotplugging and no keyboard input devices were found\n");
+ fprintf(stderr, "gsr-global-hotkeys error: failed to setup hotplugging and no keyboard input devices were found\n");
setuid(user_id);
return 1;
}
- fprintf(stderr, "Info: global hotkeys setup, waiting for hotkeys to be pressed\n");
+ fprintf(stderr, "gsr-global-hotkeys info: global hotkeys setup, waiting for hotkeys to be pressed\n");
for(;;) {
keyboard_event_poll_events(&keyboard_ev, -1);
if(keyboard_event_stdin_has_failed(&keyboard_ev)) {
- fprintf(stderr, "Info: stdin closed (parent process likely closed this process), exiting...\n");
+ fprintf(stderr, "gsr-global-hotkeys info: stdin closed (parent process likely closed this process), exiting...\n");
break;
}
}
diff --git a/tools/gsr-ui-cli/main.c b/tools/gsr-ui-cli/main.c
index c34888c..feb5247 100644
--- a/tools/gsr-ui-cli/main.c
+++ b/tools/gsr-ui-cli/main.c
@@ -56,6 +56,10 @@ static void usage(void) {
printf(" Start/stop replay.\n");
printf(" replay-save\n");
printf(" Save replay.\n");
+ printf(" replay-save-1-min\n");
+ printf(" Save 1 minute replay.\n");
+ printf(" replay-save-10-min\n");
+ printf(" Save 10 minute replay.\n");
printf(" take-screenshot\n");
printf(" Take a screenshot.\n");
printf(" take-screenshot-region\n");
@@ -75,6 +79,8 @@ static bool is_valid_command(const char *command) {
"toggle-stream",
"toggle-replay",
"replay-save",
+ "replay-save-1-min",
+ "replay-save-10-min",
"take-screenshot",
"take-screenshot-region",
NULL