mirror of
https://repo.dec05eba.com/gpu-screen-recorder-ui
synced 2026-04-05 11:16:28 +09:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6339ac9c2 | ||
|
|
0341930394 | ||
|
|
3d673247a7 | ||
|
|
ed671e9d7c | ||
|
|
6a72717fe5 | ||
|
|
6ea867b9d2 | ||
|
|
756b993078 | ||
|
|
007e2546a9 | ||
|
|
ec98533f1b | ||
|
|
ebc460ecc8 | ||
|
|
540e2df322 | ||
|
|
d0c581684b | ||
|
|
d4dbb27213 | ||
|
|
bfc7df5c56 | ||
|
|
9c5688f61b | ||
|
|
9ccb4dd541 | ||
|
|
1e3e76fcee | ||
|
|
9c9df47d62 | ||
|
|
00ceaa989d | ||
|
|
23b1526092 | ||
|
|
9339d6760e | ||
|
|
8bf6e533c5 | ||
|
|
902fc7f6a9 | ||
|
|
794064a8b8 | ||
|
|
e44b2ec528 | ||
|
|
5f484bd82c | ||
|
|
0269387b9a | ||
|
|
5c4ebbab59 | ||
|
|
40b2af5668 | ||
|
|
aa717a95ec | ||
|
|
86424607b7 | ||
|
|
74bb6f0070 | ||
|
|
61bbaf3728 | ||
|
|
fed47000ce | ||
|
|
7f43adfbd5 | ||
|
|
1f6251baf3 | ||
|
|
d1220b013e | ||
|
|
93a55b6bdf | ||
|
|
974e760136 | ||
|
|
387141d36f | ||
|
|
3713d3d59e | ||
|
|
2bb6754523 | ||
|
|
df1610431d | ||
|
|
1ea9615584 | ||
|
|
45ae7c95cf | ||
|
|
f1b6df4d56 | ||
|
|
202c0b2415 | ||
|
|
fd5026489c | ||
|
|
8032cb2cf0 | ||
|
|
13562d2aa1 | ||
|
|
1971d4a288 | ||
|
|
c039b79174 | ||
|
|
245dcf5730 | ||
|
|
e68a342b81 | ||
|
|
11aa237821 | ||
|
|
d7be9b38b1 | ||
|
|
4717c64b03 | ||
|
|
2c2633ec58 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,3 +6,6 @@ compile_commands.json
|
||||
**/xdg-output-unstable-v1-protocol.c
|
||||
|
||||
depends/.wraplock
|
||||
|
||||
.cache
|
||||
build/
|
||||
@@ -27,6 +27,7 @@ These are the dependencies needed to build GPU Screen Recorder UI:
|
||||
* linux-api-headers
|
||||
* libpulse (libpulse-simple)
|
||||
* libdrm
|
||||
* libdbus
|
||||
* wayland (wayland-client, wayland-egl, wayland-scanner)
|
||||
* setcap (libcap)
|
||||
|
||||
@@ -44,7 +45,7 @@ as gpu screen recorder tries to grab keys and keyd grabs gpu screen recorder, le
|
||||
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`.\
|
||||
This software is licensed under GPL-3.0-only, see the LICENSE file for more information. 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 licensed under `CC BY 4.0`.
|
||||
|
||||
18
TODO
18
TODO
@@ -127,8 +127,6 @@ Add option to do screen-direct recording. But make it clear that it should not b
|
||||
|
||||
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.
|
||||
@@ -246,4 +244,18 @@ Remove all mgl::Clock usage in Overlay. We only need to get the time once per up
|
||||
|
||||
Handle stopping replay/stream when recording is running (show notification that the video is saved and move the video to folder with game name).
|
||||
|
||||
Support translations.
|
||||
Support translations.
|
||||
|
||||
Sometimes when opening gpu screen recorder ui gsr-global-hotkeys incorrectly detects that keyboard input is locked.
|
||||
|
||||
When running replay for a long time and then stopping it it takes a while. Improve this.
|
||||
|
||||
Make it possible to resize webcam box from top left, top right and bottom left as well.
|
||||
|
||||
The flatpak version can for some get stuck at shutdown when instant replay is running. It only happens in the flatpak version and only when instant replay is running and it happens always. Manual SIGINT on gsr-ui stops gsr-ui properly, so why does it fail when shutting down the computer when the systemd stop signal is SIGINT? Maybe its related to the flatpak version being launched through gsr-gtk. I cant personally reproduce it.
|
||||
|
||||
Redesign the UI to allow capturing multiple video sources. Move webcam to capture sources as well then. Maybe design the UI to work more like obs studio then, where you start recording and then add sources at capture time, with a preview.
|
||||
|
||||
Add option to choose video container (either flv or mpegts) for youtube livestreaming.
|
||||
|
||||
Get wayland cursor position for region selector, otherwise the start position before the cursor moves is off.
|
||||
Submodule depends/mglpp updated: b569045831...f69b0d3ee0
10
gpu-screen-recorder.desktop
Normal file
10
gpu-screen-recorder.desktop
Normal file
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=GPU Screen Recorder
|
||||
GenericName=Screen recorder
|
||||
Comment=A ShadowPlay-like screen recorder for Linux
|
||||
Icon=gpu-screen-recorder
|
||||
Exec=gsr-ui launch-hide-announce
|
||||
Terminal=false
|
||||
Keywords=gpu-screen-recorder;gsr-ui;screen recorder;streaming;twitch;replay;shadowplay;
|
||||
Categories=AudioVideo;Recorder;
|
||||
BIN
icons/hicolor/128x128/apps/gpu-screen-recorder.png
Normal file
BIN
icons/hicolor/128x128/apps/gpu-screen-recorder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
icons/hicolor/32x32/apps/gpu-screen-recorder.png
Normal file
BIN
icons/hicolor/32x32/apps/gpu-screen-recorder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
icons/hicolor/64x64/apps/gpu-screen-recorder.png
Normal file
BIN
icons/hicolor/64x64/apps/gpu-screen-recorder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
images/info.png
Normal file
BIN
images/info.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/question_mark.png
Normal file
BIN
images/question_mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/warning.png
Normal file
BIN
images/warning.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -60,6 +60,18 @@ namespace gsr {
|
||||
bool overclock = false;
|
||||
bool record_cursor = true;
|
||||
bool restore_portal_session = true;
|
||||
bool low_power_mode = false;
|
||||
|
||||
std::string webcam_source = "";
|
||||
bool webcam_flip_horizontally = false;
|
||||
std::string webcam_video_format = "auto";
|
||||
int32_t webcam_camera_width = 0;
|
||||
int32_t webcam_camera_height = 0;
|
||||
int32_t webcam_camera_fps = 0;
|
||||
int32_t webcam_x = 0; // A value between 0 and 100 (percentage)
|
||||
int32_t webcam_y = 0; // A value between 0 and 100 (percentage)
|
||||
int32_t webcam_width = 30; // A value between 0 and 100 (percentage), 0 = Don't scale it
|
||||
int32_t webcam_height = 30; // A value between 0 and 100 (percentage), 0 = Don't scale it
|
||||
|
||||
bool show_notifications = true;
|
||||
bool use_led_indicator = false;
|
||||
@@ -88,6 +100,11 @@ namespace gsr {
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct KickStreamConfig {
|
||||
std::string stream_url;
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct CustomStreamConfig {
|
||||
std::string url;
|
||||
std::string key;
|
||||
@@ -100,6 +117,7 @@ namespace gsr {
|
||||
YoutubeStreamConfig youtube;
|
||||
TwitchStreamConfig twitch;
|
||||
RumbleStreamConfig rumble;
|
||||
KickStreamConfig kick;
|
||||
CustomStreamConfig custom;
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
};
|
||||
@@ -111,6 +129,8 @@ namespace gsr {
|
||||
std::string container = "mp4";
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
ConfigHotkey pause_unpause_hotkey;
|
||||
ConfigHotkey start_stop_region_hotkey;
|
||||
ConfigHotkey start_stop_window_hotkey;
|
||||
};
|
||||
|
||||
struct ReplayConfig {
|
||||
@@ -140,12 +160,15 @@ namespace gsr {
|
||||
|
||||
bool save_screenshot_in_game_folder = false;
|
||||
bool save_screenshot_to_clipboard = false;
|
||||
bool save_screenshot_to_disk = true;
|
||||
bool show_notifications = true;
|
||||
bool use_led_indicator = false;
|
||||
std::string save_directory;
|
||||
ConfigHotkey take_screenshot_hotkey;
|
||||
ConfigHotkey take_screenshot_region_hotkey;
|
||||
ConfigHotkey take_screenshot_window_hotkey; // Or desktop portal, on wayland
|
||||
|
||||
std::string custom_script;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
@@ -164,4 +187,4 @@ namespace gsr {
|
||||
|
||||
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options);
|
||||
void save_config(Config &config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,23 @@
|
||||
#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 char *card_path, struct wl_display *wayland_dpy);
|
||||
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 clear_monitors();
|
||||
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;
|
||||
struct wl_display *wayland_dpy = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -25,11 +25,28 @@ namespace gsr {
|
||||
bool png = false;
|
||||
};
|
||||
|
||||
enum GsrCameraPixelFormat {
|
||||
YUYV,
|
||||
MJPEG
|
||||
};
|
||||
|
||||
struct GsrMonitor {
|
||||
std::string name;
|
||||
mgl::vec2i size;
|
||||
};
|
||||
|
||||
struct GsrCameraSetup {
|
||||
mgl::vec2i resolution;
|
||||
int fps;
|
||||
//GsrCameraPixelFormat pixel_format;
|
||||
};
|
||||
|
||||
struct GsrCamera {
|
||||
std::string path;
|
||||
std::vector<GsrCameraSetup> yuyv_setups;
|
||||
std::vector<GsrCameraSetup> mjpeg_setups;
|
||||
};
|
||||
|
||||
struct GsrVersion {
|
||||
uint8_t major = 0;
|
||||
uint8_t minor = 0;
|
||||
@@ -51,6 +68,7 @@ namespace gsr {
|
||||
bool focused = false;
|
||||
bool portal = false;
|
||||
std::vector<GsrMonitor> monitors;
|
||||
std::vector<GsrCamera> cameras;
|
||||
};
|
||||
|
||||
enum class DisplayServer {
|
||||
@@ -103,4 +121,5 @@ namespace gsr {
|
||||
std::vector<AudioDevice> get_audio_devices();
|
||||
std::vector<std::string> get_application_audio();
|
||||
SupportedCaptureOptions get_supported_capture_options(const GsrInfo &gsr_info);
|
||||
std::vector<GsrCamera> get_v4l2_devices();
|
||||
}
|
||||
13
include/HyprlandWorkaround.hpp
Normal file
13
include/HyprlandWorkaround.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace gsr {
|
||||
struct ActiveHyprlandWindow {
|
||||
std::string window_id = "";
|
||||
std::string title = "Game";
|
||||
};
|
||||
|
||||
void start_hyprland_listener_thread();
|
||||
std::string get_current_hyprland_window_title();
|
||||
}
|
||||
12
include/KwinWorkaround.hpp
Normal file
12
include/KwinWorkaround.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace gsr {
|
||||
struct ActiveKwinWindow {
|
||||
std::string title = "Game";
|
||||
};
|
||||
|
||||
void start_kwin_helper_thread();
|
||||
std::string get_current_kwin_window_title();
|
||||
}
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
class DropdownButton;
|
||||
class GlobalHotkeys;
|
||||
@@ -40,7 +42,8 @@ namespace gsr {
|
||||
RECORD,
|
||||
REPLAY,
|
||||
STREAM,
|
||||
SCREENSHOT
|
||||
SCREENSHOT,
|
||||
NOTICE
|
||||
};
|
||||
|
||||
enum class NotificationLevel {
|
||||
@@ -48,6 +51,12 @@ namespace gsr {
|
||||
ERROR,
|
||||
};
|
||||
|
||||
enum class RecordForceType {
|
||||
NONE,
|
||||
REGION,
|
||||
WINDOW
|
||||
};
|
||||
|
||||
enum class ScreenshotForceType {
|
||||
NONE,
|
||||
REGION,
|
||||
@@ -73,7 +82,7 @@ namespace gsr {
|
||||
void show();
|
||||
void hide_next_frame();
|
||||
void toggle_show();
|
||||
void toggle_record();
|
||||
void toggle_record(RecordForceType force_type);
|
||||
void toggle_pause();
|
||||
void toggle_stream();
|
||||
void toggle_replay();
|
||||
@@ -98,6 +107,7 @@ namespace gsr {
|
||||
|
||||
bool global_hotkeys_ungrab_keyboard = false;
|
||||
private:
|
||||
const char* notification_type_to_string(NotificationType notification_type);
|
||||
void update_upause_status();
|
||||
|
||||
void hide();
|
||||
@@ -106,6 +116,7 @@ namespace gsr {
|
||||
void on_event(mgl::Event &event);
|
||||
|
||||
void recreate_global_hotkeys(const char *hotkey_option);
|
||||
void update_led_indicator_after_settings_change();
|
||||
void create_frontpage_ui_components();
|
||||
void xi_setup();
|
||||
void handle_xi_events();
|
||||
@@ -117,7 +128,7 @@ namespace gsr {
|
||||
|
||||
double get_time_passed_in_replay_buffer_seconds();
|
||||
void update_notification_process_status();
|
||||
void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type);
|
||||
void save_video_in_current_game_directory(std::string &video_filepath, NotificationType notification_type);
|
||||
void on_replay_saved(const char *replay_saved_filepath);
|
||||
void process_gsr_output();
|
||||
void on_gsr_process_error(int exit_code, NotificationType notification_type);
|
||||
@@ -129,7 +140,7 @@ namespace gsr {
|
||||
void update_power_supply_status();
|
||||
void update_system_startup_status();
|
||||
|
||||
void on_stop_recording(int exit_code, const std::string &video_filepath);
|
||||
void on_stop_recording(int exit_code, std::string &video_filepath);
|
||||
|
||||
void update_ui_recording_paused();
|
||||
void update_ui_recording_unpaused();
|
||||
@@ -148,11 +159,14 @@ namespace gsr {
|
||||
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_record(bool finished_selection, RecordForceType force_type);
|
||||
void on_press_start_stream(bool finished_selection);
|
||||
void on_press_take_screenshot(bool finished_selection, ScreenshotForceType force_type);
|
||||
bool update_compositor_texture(const Monitor &monitor);
|
||||
|
||||
void add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size);
|
||||
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 std::string ®ion_area_option, RecordForceType force_type = RecordForceType::NONE);
|
||||
|
||||
std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);
|
||||
|
||||
void force_window_on_top();
|
||||
@@ -171,6 +185,8 @@ namespace gsr {
|
||||
Config config;
|
||||
Config current_recording_config;
|
||||
|
||||
std::string gsr_icon_path;
|
||||
|
||||
bool visible = false;
|
||||
|
||||
mgl::Texture window_texture_texture;
|
||||
@@ -239,6 +255,8 @@ namespace gsr {
|
||||
Display *x11_dpy = nullptr;
|
||||
XEvent x11_mapping_xev;
|
||||
|
||||
struct wl_display *wayland_dpy = nullptr;
|
||||
|
||||
mgl::Clock replay_save_clock;
|
||||
bool replay_save_show_notification = false;
|
||||
ReplayStartupMode replay_startup_mode = ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
|
||||
|
||||
@@ -13,13 +13,17 @@ namespace gsr {
|
||||
|
||||
// Arguments ending with NULL
|
||||
bool exec_program_daemonized(const char **args, bool debug = true);
|
||||
// Arguments ending with NULL.
|
||||
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
|
||||
// host machine with flatpak-spawn --host.
|
||||
bool exec_program_on_host_daemonized(const char **args, bool debug = true);
|
||||
// Arguments ending with NULL. |read_fd| can be NULL
|
||||
pid_t exec_program(const char **args, int *read_fd, bool debug = true);
|
||||
// Arguments ending with NULL. Returns the exit status of the program or -1 on error
|
||||
int exec_program_get_stdout(const char **args, std::string &result, bool debug = true);
|
||||
// Arguments ending with NULL. Returns the exit status of the program or -1 on error.
|
||||
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
|
||||
// host machine with flatpak-spawn --host
|
||||
// host machine with flatpak-spawn --host.
|
||||
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug = true);
|
||||
pid_t pidof(const char *process_name, pid_t ignore_pid);
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
struct Region {
|
||||
mgl::vec2i pos;
|
||||
@@ -28,7 +30,7 @@ namespace gsr {
|
||||
bool poll_events();
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Region get_selection() const;
|
||||
Region get_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const;
|
||||
private:
|
||||
void on_button_press(const void *de);
|
||||
void on_button_release(const void *de);
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace gsr {
|
||||
mgl::Font body_font;
|
||||
mgl::Font title_font;
|
||||
mgl::Font top_bar_font;
|
||||
mgl::Font camera_setup_font;
|
||||
|
||||
mgl::Texture combobox_arrow_texture;
|
||||
mgl::Texture settings_texture;
|
||||
@@ -46,6 +47,9 @@ namespace gsr {
|
||||
mgl::Texture trash_texture;
|
||||
mgl::Texture masked_texture;
|
||||
mgl::Texture unmasked_texture;
|
||||
mgl::Texture warning_texture;
|
||||
mgl::Texture info_texture;
|
||||
mgl::Texture question_mark_texture;
|
||||
|
||||
mgl::Texture ps4_home_texture;
|
||||
mgl::Texture ps4_options_texture;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <optional>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
enum class WindowCaptureType {
|
||||
FOCUSED,
|
||||
@@ -13,14 +15,14 @@ namespace gsr {
|
||||
};
|
||||
|
||||
struct Monitor {
|
||||
mgl::vec2i position;
|
||||
mgl::vec2i size;
|
||||
mgl::vec2i position; // Logical position on Wayland
|
||||
mgl::vec2i size; // Logical size on Wayland
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::optional<std::string> get_window_title(Display *dpy, Window window);
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type, bool fallback_cursor_focused = true);
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type, bool fallback_cursor_focused = true);
|
||||
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);
|
||||
@@ -30,6 +32,7 @@ namespace gsr {
|
||||
std::string get_window_manager_name(Display *display);
|
||||
bool is_compositor_running(Display *dpy, int screen);
|
||||
std::vector<Monitor> get_monitors(Display *dpy);
|
||||
std::vector<Monitor> get_monitors_wayland(struct wl_display *dpy);
|
||||
void xi_grab_all_mouse_devices(Display *dpy);
|
||||
void xi_ungrab_all_mouse_devices(Display *dpy);
|
||||
void xi_warp_all_mouse_devices(Display *dpy, mgl::vec2i position);
|
||||
|
||||
@@ -18,7 +18,9 @@ namespace gsr {
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
void add_item(const std::string &text, const std::string &id);
|
||||
void add_item(const std::string &text, const std::string &id, bool allow_duplicate = true);
|
||||
void clear_items();
|
||||
|
||||
// The item can only be selected if it's enabled
|
||||
void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true);
|
||||
void set_item_enabled(const std::string &id, bool enabled);
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace gsr {
|
||||
REPLAY_SAVE_10_MIN,
|
||||
RECORD_START_STOP,
|
||||
RECORD_PAUSE_UNPAUSE,
|
||||
RECORD_START_STOP_REGION,
|
||||
RECORD_START_STOP_WINDOW,
|
||||
STREAM_START_STOP,
|
||||
TAKE_SCREENSHOT,
|
||||
TAKE_SCREENSHOT_REGION,
|
||||
@@ -61,6 +63,7 @@ namespace gsr {
|
||||
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_record_hotkey_window_region_options();
|
||||
std::unique_ptr<List> create_stream_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_region_hotkey_options();
|
||||
@@ -100,6 +103,8 @@ namespace gsr {
|
||||
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_recording_region_button_ptr = nullptr;
|
||||
Button *start_stop_recording_window_button_ptr = nullptr;
|
||||
Button *start_stop_streaming_button_ptr = nullptr;
|
||||
Button *take_screenshot_button_ptr = nullptr;
|
||||
Button *take_screenshot_region_button_ptr = nullptr;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Widget.hpp"
|
||||
|
||||
#include <mglpp/graphics/Sprite.hpp>
|
||||
#include <functional>
|
||||
|
||||
namespace gsr {
|
||||
class Image : public Widget {
|
||||
@@ -21,6 +22,8 @@ namespace gsr {
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
std::function<void(bool inside)> on_mouse_move;
|
||||
private:
|
||||
mgl::Sprite sprite;
|
||||
mgl::vec2f size;
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace gsr {
|
||||
void load();
|
||||
void save();
|
||||
void on_navigate_away_from_page() override;
|
||||
|
||||
std::function<void()> on_config_changed;
|
||||
private:
|
||||
std::unique_ptr<ComboBox> create_record_area_box();
|
||||
std::unique_ptr<Widget> create_record_area();
|
||||
@@ -43,10 +45,14 @@ namespace gsr {
|
||||
std::unique_ptr<Widget> create_file_info_section();
|
||||
std::unique_ptr<CheckBox> create_save_screenshot_in_game_folder();
|
||||
std::unique_ptr<CheckBox> create_save_screenshot_to_clipboard();
|
||||
std::unique_ptr<CheckBox> create_save_screenshot_to_disk();
|
||||
std::unique_ptr<Widget> create_notifications();
|
||||
std::unique_ptr<Widget> create_led_indicator();
|
||||
std::unique_ptr<Widget> create_general_section();
|
||||
std::unique_ptr<Widget> create_screenshot_indicator_section();
|
||||
std::unique_ptr<Widget> create_custom_script_screenshot_section();
|
||||
std::unique_ptr<List> create_custom_script_screenshot_entry();
|
||||
std::unique_ptr<List> create_custom_script_screenshot();
|
||||
std::unique_ptr<Widget> create_settings();
|
||||
void add_widgets();
|
||||
|
||||
@@ -73,9 +79,11 @@ namespace gsr {
|
||||
Button *save_directory_button_ptr = nullptr;
|
||||
CheckBox *save_screenshot_in_game_folder_checkbox_ptr = nullptr;
|
||||
CheckBox *save_screenshot_to_clipboard_checkbox_ptr = nullptr;
|
||||
CheckBox *save_screenshot_to_disk_checkbox_ptr = nullptr;
|
||||
CheckBox *show_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *led_indicator_checkbox_ptr = nullptr;
|
||||
Entry *create_custom_script_screenshot_entry_ptr = nullptr;
|
||||
|
||||
PageStack *page_stack = nullptr;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,14 @@ namespace gsr {
|
||||
INPUT
|
||||
};
|
||||
|
||||
enum class WebcamBoxResizeCorner {
|
||||
NONE,
|
||||
//TOP_LEFT,
|
||||
//TOP_RIGHT,
|
||||
//BOTTOM_LEFT,
|
||||
BOTTOM_RIGHT
|
||||
};
|
||||
|
||||
class SettingsPage : public StaticPage {
|
||||
public:
|
||||
enum class Type {
|
||||
@@ -58,10 +66,19 @@ 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<List> create_webcam_sources();
|
||||
std::unique_ptr<List> create_webcam_video_setups();
|
||||
std::unique_ptr<List> create_webcam_video_format();
|
||||
std::unique_ptr<List> create_webcam_video_setup_list();
|
||||
std::unique_ptr<Widget> create_webcam_location_widget();
|
||||
std::unique_ptr<CheckBox> create_flip_camera_checkbox();
|
||||
std::unique_ptr<List> create_webcam_body();
|
||||
std::unique_ptr<Widget> create_webcam_section();
|
||||
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();
|
||||
void update_application_audio_warning_visibility();
|
||||
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);
|
||||
@@ -71,6 +88,7 @@ namespace gsr {
|
||||
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_application_audio_warning();
|
||||
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();
|
||||
@@ -115,6 +133,7 @@ namespace gsr {
|
||||
std::unique_ptr<CheckBox> create_led_indicator(const char *type);
|
||||
std::unique_ptr<CheckBox> create_notifications(const char *type);
|
||||
std::unique_ptr<List> create_indicator(const char *type);
|
||||
std::unique_ptr<Widget> create_low_power_mode();
|
||||
void add_replay_widgets();
|
||||
void add_record_widgets();
|
||||
|
||||
@@ -140,6 +159,8 @@ namespace gsr {
|
||||
void save_stream();
|
||||
|
||||
void view_changed(bool advanced_view);
|
||||
|
||||
RecordOptions& get_current_record_options();
|
||||
private:
|
||||
Type type;
|
||||
Config &config;
|
||||
@@ -187,6 +208,8 @@ namespace gsr {
|
||||
Entry *twitch_stream_key_entry_ptr = nullptr;
|
||||
Entry *youtube_stream_key_entry_ptr = nullptr;
|
||||
Entry *rumble_stream_key_entry_ptr = nullptr;
|
||||
Entry *kick_stream_url_entry_ptr = nullptr;
|
||||
Entry *kick_stream_key_entry_ptr = nullptr;
|
||||
Entry *stream_url_entry_ptr = nullptr;
|
||||
Entry *stream_key_entry_ptr = nullptr;
|
||||
Entry *replay_time_entry_ptr = nullptr;
|
||||
@@ -197,7 +220,34 @@ namespace gsr {
|
||||
List *audio_track_section_list_ptr = nullptr;
|
||||
CheckBox *led_indicator_checkbox_ptr = nullptr;
|
||||
CheckBox *show_notification_checkbox_ptr = nullptr;
|
||||
ComboBox *webcam_sources_box_ptr = nullptr;
|
||||
ComboBox *webcam_video_setup_box_ptr = nullptr;
|
||||
ComboBox *webcam_video_format_box_ptr = nullptr;
|
||||
List *webcam_body_list_ptr = nullptr;
|
||||
CheckBox *flip_camera_horizontally_checkbox_ptr = nullptr;
|
||||
CheckBox *low_power_mode_checkbox_ptr = nullptr;
|
||||
|
||||
PageStack *page_stack = nullptr;
|
||||
|
||||
mgl::vec2f webcam_box_pos;
|
||||
mgl::vec2f webcam_box_size;
|
||||
|
||||
mgl::vec2f webcam_box_drawn_pos;
|
||||
mgl::vec2f webcam_box_drawn_size;
|
||||
mgl::vec2f webcam_box_grab_offset;
|
||||
|
||||
mgl::vec2f camera_screen_size;
|
||||
mgl::vec2f screen_inner_size;
|
||||
bool moving_webcam_box = false;
|
||||
|
||||
WebcamBoxResizeCorner webcam_resize_corner = WebcamBoxResizeCorner::NONE;
|
||||
mgl::vec2f webcam_resize_start_pos;
|
||||
mgl::vec2f webcam_box_pos_resize_start;
|
||||
mgl::vec2f webcam_box_size_resize_start;
|
||||
|
||||
std::optional<GsrCamera> selected_camera;
|
||||
std::optional<GsrCameraSetup> selected_camera_setup;
|
||||
|
||||
bool supports_window_title = false;
|
||||
};
|
||||
}
|
||||
22
include/gui/Tooltip.hpp
Normal file
22
include/gui/Tooltip.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "Widget.hpp"
|
||||
#include <mglpp/graphics/Text.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class Tooltip : public Widget {
|
||||
public:
|
||||
Tooltip(mgl::Font *font);
|
||||
Tooltip(const Tooltip&) = delete;
|
||||
Tooltip& operator=(const Tooltip&) = delete;
|
||||
|
||||
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;
|
||||
|
||||
void set_text(std::string text);
|
||||
private:
|
||||
mgl::Text label;
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,10 @@ namespace mgl {
|
||||
}
|
||||
|
||||
namespace gsr {
|
||||
mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b);
|
||||
mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b);
|
||||
mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max);
|
||||
|
||||
// Inner border
|
||||
void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size);
|
||||
double get_frame_delta_seconds();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace mgl {
|
||||
class Event;
|
||||
@@ -44,9 +45,14 @@ namespace gsr {
|
||||
Alignment get_vertical_alignment() const;
|
||||
|
||||
void set_visible(bool visible);
|
||||
bool is_visible() const;
|
||||
|
||||
Widget* get_parent_widget();
|
||||
|
||||
void set_tooltip_text(std::string text);
|
||||
const std::string& get_tooltip_text() const;
|
||||
void handle_tooltip_event(mgl::Event &event, mgl::vec2f position, mgl::vec2f size);
|
||||
|
||||
void *userdata = nullptr;
|
||||
protected:
|
||||
void set_widget_as_selected_in_parent();
|
||||
@@ -61,8 +67,13 @@ namespace gsr {
|
||||
Alignment vertical_aligment = Alignment::START;
|
||||
|
||||
bool visible = true;
|
||||
std::string tooltip_text;
|
||||
};
|
||||
|
||||
void add_widget_to_remove(std::unique_ptr<Widget> widget);
|
||||
void remove_widgets_to_be_removed();
|
||||
|
||||
void set_current_tooltip(Widget *widget);
|
||||
void remove_as_current_tooltip(Widget *widget);
|
||||
void draw_tooltip(mgl::Window &window);
|
||||
}
|
||||
43
meson.build
43
meson.build
@@ -1,4 +1,4 @@
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.8.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.10.4', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
|
||||
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
|
||||
|
||||
@@ -34,12 +34,15 @@ src = [
|
||||
'src/gui/GlobalSettingsPage.cpp',
|
||||
'src/gui/GsrPage.cpp',
|
||||
'src/gui/Subsection.cpp',
|
||||
'src/gui/Tooltip.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/HyprlandWorkaround.cpp',
|
||||
'src/KwinWorkaround.cpp',
|
||||
'src/WindowUtils.cpp',
|
||||
'src/RegionSelector.cpp',
|
||||
'src/WindowSelector.cpp',
|
||||
@@ -64,9 +67,12 @@ mglpp_dep = mglpp_proj.get_variable('mglpp_dep')
|
||||
prefix = get_option('prefix')
|
||||
datadir = get_option('datadir')
|
||||
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
|
||||
icons_path = join_paths(prefix, datadir, 'icons')
|
||||
|
||||
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
|
||||
add_project_arguments('-DGSR_FLATPAK_VERSION="5.9.0"', language: ['c', 'cpp'])
|
||||
add_project_arguments('-DGSR_FLATPAK_VERSION="5.12.1"', language: ['c', 'cpp'])
|
||||
|
||||
add_project_arguments('-DKWIN_HELPER_SCRIPT_PATH="' + gsr_ui_resources_path + '/gsrkwinhelper.js"', language: ['c', 'cpp'])
|
||||
|
||||
executable(
|
||||
meson.project_name(),
|
||||
@@ -109,9 +115,42 @@ executable(
|
||||
install : true
|
||||
)
|
||||
|
||||
executable(
|
||||
'gsr-kwin-helper',
|
||||
[
|
||||
'tools/gsr-kwin-helper/main.cpp'
|
||||
],
|
||||
install : true,
|
||||
dependencies: [
|
||||
dependency('dbus-1'),
|
||||
]
|
||||
)
|
||||
|
||||
install_data(
|
||||
'tools/gsr-kwin-helper/gsrkwinhelper.js',
|
||||
install_dir: gsr_ui_resources_path,
|
||||
install_mode: 'rwxr-xr-x'
|
||||
)
|
||||
|
||||
executable(
|
||||
'gsr-hyprland-helper',
|
||||
[
|
||||
'tools/gsr-hyprland-helper/main.c'
|
||||
],
|
||||
install : true
|
||||
)
|
||||
|
||||
install_subdir('images', install_dir : gsr_ui_resources_path)
|
||||
install_subdir('fonts', install_dir : gsr_ui_resources_path)
|
||||
|
||||
if get_option('desktop-files') == true
|
||||
install_data(files('gpu-screen-recorder.desktop'), install_dir : join_paths(prefix, datadir, 'applications'))
|
||||
install_subdir('icons/hicolor', install_dir : icons_path)
|
||||
|
||||
gnome = import('gnome')
|
||||
gnome.post_install(update_desktop_database : true)
|
||||
endif
|
||||
|
||||
if get_option('systemd') == true
|
||||
install_data(files('extra/gpu-screen-recorder-ui.service'), install_dir : 'lib/systemd/user')
|
||||
endif
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
option('systemd', type : 'boolean', value : true, description : 'Install systemd service file')
|
||||
option('capabilities', type : 'boolean', value : true, description : 'Set binary setuid capability on gsr-global-hotkeys binary to allow global hotkeys')
|
||||
option('capabilities', type : 'boolean', value : true, description : 'Set binary setuid capability on gsr-global-hotkeys binary to allow global hotkeys')
|
||||
option('desktop-files', type : 'boolean', value : true, description : 'Install desktop files')
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gsr-ui"
|
||||
type = "executable"
|
||||
version = "1.8.2"
|
||||
version = "1.10.4"
|
||||
platforms = ["posix"]
|
||||
|
||||
[lang.cpp]
|
||||
|
||||
@@ -48,11 +48,18 @@ namespace gsr {
|
||||
XNextEvent(dpy, &xev);
|
||||
switch(xev.type) {
|
||||
case SelectionClear: {
|
||||
should_clear_selection = true;
|
||||
if(clipboard_copies.empty()) {
|
||||
should_clear_selection = false;
|
||||
set_current_file("", file_type);
|
||||
bool clear_current_file = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
should_clear_selection = true;
|
||||
if(clipboard_copies.empty()) {
|
||||
should_clear_selection = false;
|
||||
clear_current_file = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(clear_current_file)
|
||||
set_current_file("", file_type);
|
||||
break;
|
||||
}
|
||||
case SelectionRequest:
|
||||
@@ -205,6 +212,9 @@ namespace gsr {
|
||||
uint8_t file_buffer[1<<16];
|
||||
ssize_t file_bytes_read = 0;
|
||||
|
||||
if(file_fd <= 0)
|
||||
return;
|
||||
|
||||
if(lseek(file_fd, clipboard_copy->file_offset, SEEK_SET) == -1) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile::send_clipboard: failed to seek in clipboard file to offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
|
||||
clipboard_copy->file_offset = 0;
|
||||
@@ -262,7 +272,13 @@ namespace gsr {
|
||||
}
|
||||
clipboard_copies.clear();
|
||||
|
||||
if(XGetSelectionOwner(dpy, clipboard_atom) == clipboard_window) {
|
||||
XSetSelectionOwner(dpy, clipboard_atom, None, CurrentTime);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
if(filepath.empty()) {
|
||||
// TODO: Cancel transfer
|
||||
if(file_fd > 0) {
|
||||
close(file_fd);
|
||||
file_fd = -1;
|
||||
|
||||
@@ -145,6 +145,8 @@ namespace gsr {
|
||||
|
||||
record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
|
||||
record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
|
||||
record_config.start_stop_region_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LCTRL};
|
||||
record_config.start_stop_window_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LSHIFT};
|
||||
|
||||
replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
|
||||
replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
|
||||
@@ -199,12 +201,25 @@ namespace gsr {
|
||||
{"streaming.record_options.overclock", &config.streaming_config.record_options.overclock},
|
||||
{"streaming.record_options.record_cursor", &config.streaming_config.record_options.record_cursor},
|
||||
{"streaming.record_options.restore_portal_session", &config.streaming_config.record_options.restore_portal_session},
|
||||
{"streaming.record_options.low_power_mode", &config.streaming_config.record_options.low_power_mode},
|
||||
{"streaming.record_options.webcam_source", &config.streaming_config.record_options.webcam_source},
|
||||
{"streaming.record_options.webcam_flip_horizontally", &config.streaming_config.record_options.webcam_flip_horizontally},
|
||||
{"streaming.record_options.webcam_video_format", &config.streaming_config.record_options.webcam_video_format},
|
||||
{"streaming.record_options.webcam_camera_width", &config.streaming_config.record_options.webcam_camera_width},
|
||||
{"streaming.record_options.webcam_camera_height", &config.streaming_config.record_options.webcam_camera_height},
|
||||
{"streaming.record_options.webcam_camera_fps", &config.streaming_config.record_options.webcam_camera_fps},
|
||||
{"streaming.record_options.webcam_x", &config.streaming_config.record_options.webcam_x},
|
||||
{"streaming.record_options.webcam_y", &config.streaming_config.record_options.webcam_y},
|
||||
{"streaming.record_options.webcam_width", &config.streaming_config.record_options.webcam_width},
|
||||
{"streaming.record_options.webcam_height", &config.streaming_config.record_options.webcam_height},
|
||||
{"streaming.record_options.show_notifications", &config.streaming_config.record_options.show_notifications},
|
||||
{"streaming.record_options.use_led_indicator", &config.streaming_config.record_options.use_led_indicator},
|
||||
{"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.kick.url", &config.streaming_config.kick.stream_url},
|
||||
{"streaming.kick.key", &config.streaming_config.kick.stream_key},
|
||||
{"streaming.custom.url", &config.streaming_config.custom.url},
|
||||
{"streaming.custom.key", &config.streaming_config.custom.key},
|
||||
{"streaming.custom.container", &config.streaming_config.custom.container},
|
||||
@@ -231,6 +246,17 @@ namespace gsr {
|
||||
{"record.record_options.overclock", &config.record_config.record_options.overclock},
|
||||
{"record.record_options.record_cursor", &config.record_config.record_options.record_cursor},
|
||||
{"record.record_options.restore_portal_session", &config.record_config.record_options.restore_portal_session},
|
||||
{"record.record_options.low_power_mode", &config.record_config.record_options.low_power_mode},
|
||||
{"record.record_options.webcam_source", &config.record_config.record_options.webcam_source},
|
||||
{"record.record_options.webcam_flip_horizontally", &config.record_config.record_options.webcam_flip_horizontally},
|
||||
{"record.record_options.webcam_video_format", &config.record_config.record_options.webcam_video_format},
|
||||
{"record.record_options.webcam_camera_width", &config.record_config.record_options.webcam_camera_width},
|
||||
{"record.record_options.webcam_camera_height", &config.record_config.record_options.webcam_camera_height},
|
||||
{"record.record_options.webcam_camera_fps", &config.record_config.record_options.webcam_camera_fps},
|
||||
{"record.record_options.webcam_x", &config.record_config.record_options.webcam_x},
|
||||
{"record.record_options.webcam_y", &config.record_config.record_options.webcam_y},
|
||||
{"record.record_options.webcam_width", &config.record_config.record_options.webcam_width},
|
||||
{"record.record_options.webcam_height", &config.record_config.record_options.webcam_height},
|
||||
{"record.record_options.show_notifications", &config.record_config.record_options.show_notifications},
|
||||
{"record.record_options.use_led_indicator", &config.record_config.record_options.use_led_indicator},
|
||||
{"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder},
|
||||
@@ -238,6 +264,8 @@ namespace gsr {
|
||||
{"record.container", &config.record_config.container},
|
||||
{"record.start_stop_hotkey", &config.record_config.start_stop_hotkey},
|
||||
{"record.pause_unpause_hotkey", &config.record_config.pause_unpause_hotkey},
|
||||
{"record.start_stop_region_hotkey", &config.record_config.start_stop_region_hotkey},
|
||||
{"record.start_stop_window_hotkey", &config.record_config.start_stop_window_hotkey},
|
||||
|
||||
{"replay.record_options.record_area_option", &config.replay_config.record_options.record_area_option},
|
||||
{"replay.record_options.record_area_width", &config.replay_config.record_options.record_area_width},
|
||||
@@ -260,6 +288,17 @@ namespace gsr {
|
||||
{"replay.record_options.overclock", &config.replay_config.record_options.overclock},
|
||||
{"replay.record_options.record_cursor", &config.replay_config.record_options.record_cursor},
|
||||
{"replay.record_options.restore_portal_session", &config.replay_config.record_options.restore_portal_session},
|
||||
{"replay.record_options.low_power_mode", &config.replay_config.record_options.low_power_mode},
|
||||
{"replay.record_options.webcam_source", &config.replay_config.record_options.webcam_source},
|
||||
{"replay.record_options.webcam_flip_horizontally", &config.replay_config.record_options.webcam_flip_horizontally},
|
||||
{"replay.record_options.webcam_video_format", &config.replay_config.record_options.webcam_video_format},
|
||||
{"replay.record_options.webcam_camera_width", &config.replay_config.record_options.webcam_camera_width},
|
||||
{"replay.record_options.webcam_camera_height", &config.replay_config.record_options.webcam_camera_height},
|
||||
{"replay.record_options.webcam_camera_fps", &config.replay_config.record_options.webcam_camera_fps},
|
||||
{"replay.record_options.webcam_x", &config.replay_config.record_options.webcam_x},
|
||||
{"replay.record_options.webcam_y", &config.replay_config.record_options.webcam_y},
|
||||
{"replay.record_options.webcam_width", &config.replay_config.record_options.webcam_width},
|
||||
{"replay.record_options.webcam_height", &config.replay_config.record_options.webcam_height},
|
||||
{"replay.record_options.show_notifications", &config.replay_config.record_options.show_notifications},
|
||||
{"replay.record_options.use_led_indicator", &config.replay_config.record_options.use_led_indicator},
|
||||
{"replay.turn_on_replay_automatically_mode", &config.replay_config.turn_on_replay_automatically_mode},
|
||||
@@ -284,12 +323,14 @@ namespace gsr {
|
||||
{"screenshot.restore_portal_session", &config.screenshot_config.restore_portal_session},
|
||||
{"screenshot.save_screenshot_in_game_folder", &config.screenshot_config.save_screenshot_in_game_folder},
|
||||
{"screenshot.save_screenshot_to_clipboard", &config.screenshot_config.save_screenshot_to_clipboard},
|
||||
{"screenshot.save_screenshot_to_disk", &config.screenshot_config.save_screenshot_to_disk},
|
||||
{"screenshot.show_notifications", &config.screenshot_config.show_notifications},
|
||||
{"screenshot.use_led_indicator", &config.screenshot_config.use_led_indicator},
|
||||
{"screenshot.save_directory", &config.screenshot_config.save_directory},
|
||||
{"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey},
|
||||
{"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_hotkey},
|
||||
{"screenshot.take_screenshot_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey}
|
||||
{"screenshot.take_screenshot_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey},
|
||||
{"screenshot.custom_script", &config.screenshot_config.custom_script},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -487,4 +528,4 @@ namespace gsr {
|
||||
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#include "../../include/CursorTracker/CursorTrackerWayland.hpp"
|
||||
#include "../../include/WindowUtils.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;
|
||||
@@ -136,177 +135,14 @@ namespace gsr {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
static const Monitor* get_wayland_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 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) {
|
||||
@@ -368,8 +204,7 @@ namespace gsr {
|
||||
if(!properties)
|
||||
goto next_crtc;
|
||||
|
||||
if(!get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled))
|
||||
goto next_crtc;
|
||||
get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled);
|
||||
|
||||
connector = get_drm_connector_by_crtc_id(drm_connectors, crtc->crtc_id);
|
||||
if(!connector)
|
||||
@@ -391,7 +226,7 @@ namespace gsr {
|
||||
drmModeFreeResources(resources);
|
||||
}
|
||||
|
||||
CursorTrackerWayland::CursorTrackerWayland(const char *card_path) {
|
||||
CursorTrackerWayland::CursorTrackerWayland(const char *card_path, struct wl_display *wayland_dpy) : wayland_dpy(wayland_dpy) {
|
||||
drm_fd = open(card_path, O_RDONLY);
|
||||
if(drm_fd <= 0) {
|
||||
fprintf(stderr, "Error: CursorTrackerWayland: failed to open %s\n", card_path);
|
||||
@@ -403,7 +238,6 @@ namespace gsr {
|
||||
}
|
||||
|
||||
CursorTrackerWayland::~CursorTrackerWayland() {
|
||||
clear_monitors();
|
||||
if(drm_fd > 0)
|
||||
close(drm_fd);
|
||||
}
|
||||
@@ -466,80 +300,19 @@ namespace gsr {
|
||||
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);
|
||||
}
|
||||
|
||||
void CursorTrackerWayland::clear_monitors() {
|
||||
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();
|
||||
}
|
||||
|
||||
std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() {
|
||||
if(drm_fd <= 0 || latest_crtc_id == -1)
|
||||
if(drm_fd <= 0 || latest_crtc_id == -1 || !wayland_dpy)
|
||||
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");
|
||||
const std::vector<Monitor> wayland_monitors = get_monitors_wayland(wayland_dpy);
|
||||
const Monitor *wayland_monitor = get_wayland_monitor_by_name(wayland_monitors, monitor_name);
|
||||
if(!wayland_monitor)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
clear_monitors();
|
||||
struct wl_registry *registry = wl_display_get_registry(dpy);
|
||||
wl_registry_add_listener(registry, ®istry_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) {
|
||||
clear_monitors();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
cursor_position = wayland_monitor->pos + latest_cursor_position;
|
||||
clear_monitors();
|
||||
|
||||
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) };
|
||||
return CursorInfo{ wayland_monitor->position + latest_cursor_position, std::move(monitor_name) };
|
||||
}
|
||||
}
|
||||
113
src/GsrInfo.cpp
113
src/GsrInfo.cpp
@@ -288,6 +288,58 @@ namespace gsr {
|
||||
return application_audio;
|
||||
}
|
||||
|
||||
struct KeyValue3 {
|
||||
std::string_view value1;
|
||||
std::string_view value2;
|
||||
std::string_view value3;
|
||||
};
|
||||
|
||||
static std::optional<KeyValue3> parse_3(std::string_view line) {
|
||||
const size_t space_index1 = line.find('|');
|
||||
if(space_index1 == std::string_view::npos)
|
||||
return std::nullopt;
|
||||
|
||||
const size_t space_index2 = line.find('|', space_index1 + 1);
|
||||
if(space_index2 == std::string_view::npos)
|
||||
return std::nullopt;
|
||||
|
||||
return KeyValue3{
|
||||
line.substr(0, space_index1),
|
||||
line.substr(space_index1 + 1, space_index2 - (space_index1 + 1)),
|
||||
line.substr(space_index2 + 1),
|
||||
};
|
||||
}
|
||||
|
||||
static bool parse_camera_pixel_format(std::string_view line, GsrCameraPixelFormat &pixel_format) {
|
||||
if(line == "yuyv") {
|
||||
pixel_format = YUYV;
|
||||
return true;
|
||||
} else if(line == "mjpeg") {
|
||||
pixel_format = MJPEG;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool capture_option_line_to_camera(std::string_view line, std::string &path, GsrCameraSetup &camera_setup, GsrCameraPixelFormat &pixel_format) {
|
||||
const std::optional<KeyValue3> key_value3 = parse_3(line);
|
||||
if(!key_value3)
|
||||
return false;
|
||||
|
||||
path = key_value3->value1;
|
||||
|
||||
char value_buffer[256];
|
||||
snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value3->value2.size(), key_value3->value2.data());
|
||||
if(sscanf(value_buffer, "%dx%d@%dhz", &camera_setup.resolution.x, &camera_setup.resolution.y, &camera_setup.fps) != 3)
|
||||
return false;
|
||||
|
||||
if(!parse_camera_pixel_format(key_value3->value3, pixel_format))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) {
|
||||
std::optional<GsrMonitor> monitor;
|
||||
const std::optional<KeyValue> key_value = parse_key_value(line);
|
||||
@@ -304,16 +356,49 @@ namespace gsr {
|
||||
return monitor;
|
||||
}
|
||||
|
||||
static GsrCamera* get_gsr_camera_by_path(std::vector<GsrCamera> &cameras, const std::string &path) {
|
||||
for(GsrCamera &camera : cameras) {
|
||||
if(camera.path == path)
|
||||
return &camera;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void parse_camera_line(std::string_view line, std::vector<GsrCamera> &cameras) {
|
||||
std::string camera_path;
|
||||
GsrCameraSetup camera_setup;
|
||||
GsrCameraPixelFormat pixel_format;
|
||||
if(!capture_option_line_to_camera(line, camera_path, camera_setup, pixel_format))
|
||||
return;
|
||||
|
||||
GsrCamera *existing_camera = get_gsr_camera_by_path(cameras, camera_path);
|
||||
if(!existing_camera) {
|
||||
cameras.push_back(GsrCamera{camera_path, std::vector<GsrCameraSetup>{}, std::vector<GsrCameraSetup>{}});
|
||||
existing_camera = &cameras.back();
|
||||
}
|
||||
|
||||
switch(pixel_format) {
|
||||
case YUYV:
|
||||
existing_camera->yuyv_setups.push_back(camera_setup);
|
||||
break;
|
||||
case MJPEG:
|
||||
existing_camera->mjpeg_setups.push_back(camera_setup);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void parse_capture_options_line(SupportedCaptureOptions &capture_options, std::string_view line) {
|
||||
if(line == "window")
|
||||
if(line == "window") {
|
||||
capture_options.window = true;
|
||||
else if(line == "region")
|
||||
} else if(line == "region") {
|
||||
capture_options.region = true;
|
||||
else if(line == "focused")
|
||||
} else if(line == "focused") {
|
||||
capture_options.focused = true;
|
||||
else if(line == "portal")
|
||||
} else if(line == "portal") {
|
||||
capture_options.portal = true;
|
||||
else {
|
||||
} else if(!line.empty() && line[0] == '/') {
|
||||
parse_camera_line(line, capture_options.cameras);
|
||||
} else {
|
||||
std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line);
|
||||
if(monitor)
|
||||
capture_options.monitors.push_back(std::move(monitor.value()));
|
||||
@@ -348,4 +433,22 @@ namespace gsr {
|
||||
|
||||
return capture_options;
|
||||
}
|
||||
|
||||
std::vector<GsrCamera> get_v4l2_devices() {
|
||||
std::vector<GsrCamera> cameras;
|
||||
|
||||
std::string stdout_str;
|
||||
const char *args[] = { "gpu-screen-recorder", "--list-v4l2-devices", nullptr };
|
||||
if(exec_program_get_stdout(args, stdout_str) != 0) {
|
||||
fprintf(stderr, "error: 'gpu-screen-recorder --list-v4l2-devices' failed\n");
|
||||
return cameras;
|
||||
}
|
||||
|
||||
string_split_char(stdout_str, '\n', [&](std::string_view line) {
|
||||
parse_camera_line(line, cameras);
|
||||
return true;
|
||||
});
|
||||
|
||||
return cameras;
|
||||
}
|
||||
}
|
||||
|
||||
62
src/HyprlandWorkaround.cpp
Normal file
62
src/HyprlandWorkaround.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "../include/HyprlandWorkaround.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
#include <thread>
|
||||
|
||||
namespace gsr {
|
||||
static ActiveHyprlandWindow active_hyprland_window;
|
||||
static bool hyprland_listener_thread_started = false;
|
||||
|
||||
static void hyprland_listener_thread() {
|
||||
const bool inside_flatpak = access("/app/manifest.json", F_OK) == 0;
|
||||
|
||||
const char *hyprland_helper_bin =
|
||||
inside_flatpak ?
|
||||
"flatpak-spawn --host -- /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/gsr-hyprland-helper"
|
||||
: "gsr-hyprland-helper";
|
||||
|
||||
FILE* pipe = popen(hyprland_helper_bin, "r");
|
||||
if (!pipe) {
|
||||
std::cerr << "Failed to start gsr-hyprland-helper process\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::cerr << "Started Hyprland helper thread\n";
|
||||
|
||||
char buffer[4096];
|
||||
const std::string prefix = "Window title changed: ";
|
||||
|
||||
std::string line;
|
||||
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
|
||||
line = buffer;
|
||||
|
||||
if (!line.empty() && line.back() == '\n') {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
size_t pos = line.find(prefix);
|
||||
if (pos != std::string::npos) {
|
||||
active_hyprland_window.title = line.substr(pos + prefix.length());
|
||||
}
|
||||
}
|
||||
|
||||
pclose(pipe);
|
||||
}
|
||||
|
||||
std::string get_current_hyprland_window_title() {
|
||||
return active_hyprland_window.title;
|
||||
}
|
||||
|
||||
void start_hyprland_listener_thread() {
|
||||
if (hyprland_listener_thread_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
hyprland_listener_thread_started = true;
|
||||
|
||||
std::thread([&] {
|
||||
hyprland_listener_thread();
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
67
src/KwinWorkaround.cpp
Normal file
67
src/KwinWorkaround.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "../include/KwinWorkaround.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <sys/types.h>
|
||||
#include <thread>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
namespace gsr {
|
||||
static ActiveKwinWindow active_kwin_window;
|
||||
static bool kwin_helper_thread_started = false;
|
||||
|
||||
void kwin_script_thread() {
|
||||
FILE* pipe = popen("gsr-kwin-helper", "r");
|
||||
if (!pipe) {
|
||||
std::cerr << "Failed to start gsr-kwin-helper process\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::cerr << "Started a KWin helper thread\n";
|
||||
|
||||
char buffer[4096];
|
||||
const std::string prefix = "Active window title set to: ";
|
||||
|
||||
std::string line;
|
||||
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
|
||||
line = buffer;
|
||||
|
||||
if (!line.empty() && line.back() == '\n') {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
size_t pos = line.find(prefix);
|
||||
if (pos != std::string::npos) {
|
||||
std::string title = line.substr(pos + prefix.length());
|
||||
|
||||
if (title == "gsr ui" || title == "gsr notify") {
|
||||
continue; // ignore the overlay and notification
|
||||
}
|
||||
|
||||
active_kwin_window.title = std::move(title);
|
||||
}
|
||||
}
|
||||
|
||||
pclose(pipe);
|
||||
}
|
||||
|
||||
std::string get_current_kwin_window_title() {
|
||||
return active_kwin_window.title;
|
||||
}
|
||||
|
||||
void start_kwin_helper_thread() {
|
||||
if (kwin_helper_thread_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
kwin_helper_thread_started = true;
|
||||
|
||||
std::thread([&] {
|
||||
kwin_script_thread();
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
@@ -152,19 +152,24 @@ namespace gsr {
|
||||
if(read_led_brightness_timer.get_elapsed_time_seconds() > 0.2) {
|
||||
read_led_brightness_timer.restart();
|
||||
|
||||
bool led_status_outdated = false;
|
||||
bool any_keyboard_with_led_enabled = false;
|
||||
bool any_keyboard_with_led_disabled = false;
|
||||
char buffer[32];
|
||||
for(int led_brightness_file_fd : led_brightness_files) {
|
||||
const ssize_t bytes_read = read(led_brightness_file_fd, buffer, sizeof(buffer));
|
||||
if(bytes_read > 0) {
|
||||
if(buffer[0] == '0')
|
||||
led_status_outdated = true;
|
||||
any_keyboard_with_led_disabled = true;
|
||||
else
|
||||
any_keyboard_with_led_enabled = true;
|
||||
lseek(led_brightness_file_fd, 0, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
if(led_status_outdated && led_enabled)
|
||||
if(led_enabled && any_keyboard_with_led_disabled)
|
||||
run_gsr_global_hotkeys_set_leds(true);
|
||||
else if(!led_enabled && any_keyboard_with_led_enabled)
|
||||
run_gsr_global_hotkeys_set_leds(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
456
src/Overlay.cpp
456
src/Overlay.cpp
@@ -10,6 +10,8 @@
|
||||
#include "../include/gui/ScreenshotSettingsPage.hpp"
|
||||
#include "../include/gui/GlobalSettingsPage.hpp"
|
||||
#include "../include/gui/Utils.hpp"
|
||||
#include "../include/KwinWorkaround.hpp"
|
||||
#include "../include/HyprlandWorkaround.hpp"
|
||||
#include "../include/gui/PageStack.hpp"
|
||||
#include "../include/WindowUtils.hpp"
|
||||
#include "../include/GlobalHotkeys/GlobalHotkeys.hpp"
|
||||
@@ -17,6 +19,7 @@
|
||||
#include "../include/CursorTracker/CursorTrackerX11.hpp"
|
||||
#include "../include/CursorTracker/CursorTrackerWayland.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <sys/wait.h>
|
||||
@@ -38,6 +41,9 @@
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <X11/extensions/shapeconst.h>
|
||||
#include <X11/Xcursor/Xcursor.h>
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
#include <mglpp/system/Rect.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <mglpp/system/Utf8.hpp>
|
||||
@@ -322,7 +328,7 @@ namespace gsr {
|
||||
config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_hotkey),
|
||||
"record", [overlay](const std::string &id) {
|
||||
fprintf(stderr, "pressed %s\n", id.c_str());
|
||||
overlay->toggle_record();
|
||||
overlay->toggle_record(RecordForceType::NONE);
|
||||
});
|
||||
|
||||
global_hotkeys->bind_key_press(
|
||||
@@ -332,6 +338,20 @@ namespace gsr {
|
||||
overlay->toggle_pause();
|
||||
});
|
||||
|
||||
global_hotkeys->bind_key_press(
|
||||
config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_region_hotkey),
|
||||
"record_region", [overlay](const std::string &id) {
|
||||
fprintf(stderr, "pressed %s\n", id.c_str());
|
||||
overlay->toggle_record(RecordForceType::REGION);
|
||||
});
|
||||
|
||||
global_hotkeys->bind_key_press(
|
||||
config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_window_hotkey),
|
||||
"record_window", [overlay](const std::string &id) {
|
||||
fprintf(stderr, "pressed %s\n", id.c_str());
|
||||
overlay->toggle_record(RecordForceType::WINDOW);
|
||||
});
|
||||
|
||||
global_hotkeys->bind_key_press(
|
||||
config_hotkey_to_hotkey(overlay->get_config().streaming_config.start_stop_hotkey),
|
||||
"stream", [overlay](const std::string &id) {
|
||||
@@ -440,7 +460,7 @@ namespace gsr {
|
||||
|
||||
global_hotkeys_js->bind_action("toggle_record", [overlay](const std::string &id) {
|
||||
fprintf(stderr, "pressed %s\n", id.c_str());
|
||||
overlay->toggle_record();
|
||||
overlay->toggle_record(RecordForceType::NONE);
|
||||
});
|
||||
|
||||
global_hotkeys_js->bind_action("toggle_replay", [overlay](const std::string &id) {
|
||||
@@ -472,6 +492,16 @@ namespace gsr {
|
||||
top_bar_background({0.0f, 0.0f}),
|
||||
close_button_widget({0.0f, 0.0f})
|
||||
{
|
||||
if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
|
||||
wayland_dpy = wl_display_connect(nullptr);
|
||||
if(!wayland_dpy)
|
||||
fprintf(stderr, "Warning: failed to connect to the wayland server\n");
|
||||
} else {
|
||||
wayland_dpy = nullptr;
|
||||
}
|
||||
|
||||
gsr_icon_path = this->resources_path + "images/gpu_screen_recorder_logo.png";
|
||||
|
||||
key_bindings[0].key_event.code = mgl::Keyboard::Escape;
|
||||
key_bindings[0].key_event.alt = false;
|
||||
key_bindings[0].key_event.control = false;
|
||||
@@ -513,17 +543,24 @@ namespace gsr {
|
||||
cursor_tracker = std::make_unique<CursorTrackerX11>((Display*)mgl_get_context()->connection);
|
||||
else if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
|
||||
if(!this->gsr_info.gpu_info.card_path.empty())
|
||||
cursor_tracker = std::make_unique<CursorTrackerWayland>(this->gsr_info.gpu_info.card_path.c_str());
|
||||
cursor_tracker = std::make_unique<CursorTrackerWayland>(this->gsr_info.gpu_info.card_path.c_str(), wayland_dpy);
|
||||
|
||||
if(!config.main_config.wayland_warning_shown) {
|
||||
config.main_config.wayland_warning_shown = true;
|
||||
save_config(config);
|
||||
show_notification("Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE, nullptr, NotificationLevel::ERROR);
|
||||
show_notification("Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
|
||||
}
|
||||
|
||||
const std::string wm_name = get_window_manager_name(x11_dpy);
|
||||
|
||||
if (wm_name.find("Hyprland") != std::string::npos) {
|
||||
start_hyprland_listener_thread();
|
||||
} else if (wm_name == "KWin") {
|
||||
start_kwin_helper_thread();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Only do this if led indicator is enabled (at startup or when changing recording/screenshot settings to enabled it)
|
||||
led_indicator = std::make_unique<LedIndicator>();
|
||||
update_led_indicator_after_settings_change();
|
||||
}
|
||||
|
||||
Overlay::~Overlay() {
|
||||
@@ -566,6 +603,9 @@ namespace gsr {
|
||||
|
||||
if(x11_dpy)
|
||||
XCloseDisplay(x11_dpy);
|
||||
|
||||
if(wayland_dpy)
|
||||
wl_display_disconnect(wayland_dpy);
|
||||
}
|
||||
|
||||
void Overlay::xi_setup() {
|
||||
@@ -734,7 +774,7 @@ namespace gsr {
|
||||
show_notification(
|
||||
"Some keyboard remapping software conflicts with GPU Screen Recorder on your system.\n"
|
||||
"Keyboards have been ungrabbed, applications will now receive the hotkeys you press."
|
||||
, 7.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE, nullptr, NotificationLevel::ERROR);
|
||||
, 7.0, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
|
||||
|
||||
config.main_config.hotkeys_enable_option = "enable_hotkeys_no_grab";
|
||||
save_config(config);
|
||||
@@ -773,7 +813,7 @@ namespace gsr {
|
||||
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, nullptr, NotificationLevel::ERROR);
|
||||
show_notification("No window selected", notification_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
|
||||
}
|
||||
on_window_selected = nullptr;
|
||||
}
|
||||
@@ -824,7 +864,7 @@ 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), mgl::Color(255, 0, 0), NotificationType::NONE, nullptr, NotificationLevel::ERROR);
|
||||
show_notification("Failed to start region capture", notification_error_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
|
||||
on_region_selected = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -833,7 +873,7 @@ namespace gsr {
|
||||
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, nullptr, NotificationLevel::ERROR);
|
||||
show_notification("Failed to start window capture", notification_error_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
|
||||
on_window_selected = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -877,6 +917,7 @@ namespace gsr {
|
||||
|
||||
close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f));
|
||||
page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
|
||||
draw_tooltip(*window);
|
||||
|
||||
if(cursor_texture.is_valid()) {
|
||||
cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f());
|
||||
@@ -1022,7 +1063,11 @@ namespace gsr {
|
||||
// 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_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor)) || is_wlroots || is_hyprland;
|
||||
const Window x11_focused_window = get_focused_window(display, WindowCaptureType::FOCUSED, false);
|
||||
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND
|
||||
|| (x11_focused_window && is_window_fullscreen_on_monitor(display, x11_focused_window, *focused_monitor))
|
||||
|| is_wlroots
|
||||
|| is_hyprland;
|
||||
|
||||
if(prevent_game_minimizing) {
|
||||
window_pos = focused_monitor->position;
|
||||
@@ -1058,7 +1103,6 @@ namespace gsr {
|
||||
window.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
//window->set_low_latency(true);
|
||||
|
||||
unsigned char data = 2; // Prefer being composed to allow transparency
|
||||
@@ -1173,6 +1217,15 @@ namespace gsr {
|
||||
global_hotkeys.reset();
|
||||
}
|
||||
|
||||
void Overlay::update_led_indicator_after_settings_change() {
|
||||
if(config.record_config.record_options.use_led_indicator || config.replay_config.record_options.use_led_indicator || config.streaming_config.record_options.use_led_indicator || config.screenshot_config.use_led_indicator) {
|
||||
if(!led_indicator)
|
||||
led_indicator = std::make_unique<LedIndicator>();
|
||||
} else {
|
||||
led_indicator.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Overlay::create_frontpage_ui_components() {
|
||||
bg_screenshot_overlay = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height));
|
||||
top_bar_background = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height*0.06f).floor());
|
||||
@@ -1263,12 +1316,14 @@ namespace gsr {
|
||||
record_settings_page->on_config_changed = [this]() {
|
||||
if(recording_status == RecordingStatus::RECORD)
|
||||
show_notification("Recording settings have been modified.\nYou may need to restart recording to apply the changes.", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
|
||||
|
||||
update_led_indicator_after_settings_change();
|
||||
};
|
||||
page_stack.push(std::move(record_settings_page));
|
||||
} else if(id == "pause") {
|
||||
toggle_pause();
|
||||
} else if(id == "start") {
|
||||
on_press_start_record(false);
|
||||
on_press_start_record(false, RecordForceType::NONE);
|
||||
}
|
||||
};
|
||||
button->set_item_enabled("pause", false);
|
||||
@@ -1288,6 +1343,8 @@ namespace gsr {
|
||||
stream_settings_page->on_config_changed = [this]() {
|
||||
if(recording_status == RecordingStatus::STREAM)
|
||||
show_notification("Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
|
||||
|
||||
update_led_indicator_after_settings_change();
|
||||
};
|
||||
page_stack.push(std::move(stream_settings_page));
|
||||
} else if(id == "start") {
|
||||
@@ -1317,12 +1374,12 @@ namespace gsr {
|
||||
|
||||
if(exit_status == 127) {
|
||||
if(enable)
|
||||
show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 7.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE, nullptr, NotificationLevel::ERROR);
|
||||
show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 7.0, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
|
||||
} else {
|
||||
if(enable)
|
||||
show_notification("Failed to add GPU Screen Recorder to system startup", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE, nullptr, NotificationLevel::ERROR);
|
||||
show_notification("Failed to add GPU Screen Recorder to system startup", notification_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
|
||||
else
|
||||
show_notification("Failed to remove GPU Screen Recorder from system startup", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE, nullptr, NotificationLevel::ERROR);
|
||||
show_notification("Failed to remove GPU Screen Recorder from system startup", notification_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1370,6 +1427,9 @@ namespace gsr {
|
||||
button->set_icon_padding_scale(1.2f);
|
||||
button->on_click = [&]() {
|
||||
auto screenshot_settings_page = std::make_unique<ScreenshotSettingsPage>(&gsr_info, config, &page_stack);
|
||||
screenshot_settings_page->on_config_changed = [this]() {
|
||||
update_led_indicator_after_settings_change();
|
||||
};
|
||||
page_stack.push(std::move(screenshot_settings_page));
|
||||
};
|
||||
front_page_ptr->add_widget(std::move(button));
|
||||
@@ -1507,8 +1567,8 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
void Overlay::toggle_record() {
|
||||
on_press_start_record(false);
|
||||
void Overlay::toggle_record(RecordForceType force_type) {
|
||||
on_press_start_record(false, force_type);
|
||||
}
|
||||
|
||||
void Overlay::toggle_pause() {
|
||||
@@ -1572,13 +1632,14 @@ namespace gsr {
|
||||
on_press_take_screenshot(false, ScreenshotForceType::WINDOW);
|
||||
}
|
||||
|
||||
static const char* notification_type_to_string(NotificationType notification_type) {
|
||||
const char* Overlay::notification_type_to_string(NotificationType notification_type) {
|
||||
switch(notification_type) {
|
||||
case NotificationType::NONE: return nullptr;
|
||||
case NotificationType::RECORD: return "record";
|
||||
case NotificationType::REPLAY: return "replay";
|
||||
case NotificationType::STREAM: return "stream";
|
||||
case NotificationType::SCREENSHOT: return "screenshot";
|
||||
case NotificationType::NOTICE: return gsr_icon_path.c_str();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1909,25 +1970,39 @@ namespace gsr {
|
||||
return ClipboardFile::FileType::PNG;
|
||||
}
|
||||
|
||||
void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
|
||||
void Overlay::save_video_in_current_game_directory(std::string &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 std::string video_filename = filepath_get_filename(video_filepath.c_str());
|
||||
|
||||
const std::string wm_name = get_window_manager_name(display);
|
||||
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
|
||||
const bool is_kwin_wayland = wm_name == "KWin" && gsr_info.system_info.display_server == DisplayServer::WAYLAND;
|
||||
|
||||
std::string focused_window_name;
|
||||
if (is_hyprland) {
|
||||
focused_window_name = get_current_hyprland_window_title();
|
||||
} else if (is_kwin_wayland) {
|
||||
focused_window_name = get_current_kwin_window_title();
|
||||
} else {
|
||||
const Window gsr_ui_window = window ? (Window)window->get_system_handle() : None;
|
||||
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, false);
|
||||
}
|
||||
|
||||
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);
|
||||
if(focused_window_name.empty())
|
||||
focused_window_name = "Game";
|
||||
|
||||
string_replace_characters(focused_window_name.data(), "/\\", ' ');
|
||||
|
||||
std::string video_directory = filepath_get_directory(video_filepath) + "/" + focused_window_name;
|
||||
std::string video_directory = filepath_get_directory(video_filepath.c_str()) + "/" + focused_window_name;
|
||||
create_directory_recursive(video_directory.data());
|
||||
|
||||
const std::string new_video_filepath = video_directory + "/" + video_filename;
|
||||
rename(video_filepath, new_video_filepath.c_str());
|
||||
rename(video_filepath.c_str(), new_video_filepath.c_str());
|
||||
video_filepath = new_video_filepath;
|
||||
|
||||
truncate_string(focused_window_name, 40);
|
||||
const char *capture_target = nullptr;
|
||||
@@ -1935,15 +2010,6 @@ namespace gsr {
|
||||
|
||||
switch(notification_type) {
|
||||
case NotificationType::RECORD: {
|
||||
if(led_indicator) {
|
||||
if(recording_status == RecordingStatus::REPLAY && !current_recording_config.replay_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(false);
|
||||
else if(recording_status == RecordingStatus::STREAM && !current_recording_config.streaming_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(false);
|
||||
else if(config.record_config.record_options.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
}
|
||||
|
||||
if(!config.record_config.record_options.show_notifications)
|
||||
return;
|
||||
|
||||
@@ -1955,9 +2021,6 @@ namespace gsr {
|
||||
break;
|
||||
}
|
||||
case NotificationType::REPLAY: {
|
||||
if(led_indicator && config.replay_config.record_options.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
|
||||
if(!config.replay_config.record_options.show_notifications)
|
||||
return;
|
||||
|
||||
@@ -1969,22 +2032,17 @@ namespace gsr {
|
||||
break;
|
||||
}
|
||||
case NotificationType::SCREENSHOT: {
|
||||
if(led_indicator && config.screenshot_config.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
|
||||
if(!config.screenshot_config.show_notifications)
|
||||
return;
|
||||
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s\nto \"%s\"",
|
||||
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str(), focused_window_name.c_str());
|
||||
capture_target = screenshot_capture_target.c_str();
|
||||
|
||||
if(config.screenshot_config.save_screenshot_to_clipboard)
|
||||
clipboard_file.set_current_file(new_video_filepath, filename_to_clipboard_file_type(new_video_filepath));
|
||||
break;
|
||||
}
|
||||
case NotificationType::NONE:
|
||||
case NotificationType::STREAM:
|
||||
case NotificationType::NOTICE:
|
||||
break;
|
||||
}
|
||||
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type, capture_target);
|
||||
@@ -2003,22 +2061,20 @@ namespace gsr {
|
||||
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);
|
||||
return;
|
||||
} else {
|
||||
if(config.replay_config.record_options.show_notifications) {
|
||||
const std::string duration_str = to_duration_string(get_time_passed_in_replay_buffer_seconds());
|
||||
std::string filepath = replay_saved_filepath;
|
||||
save_video_in_current_game_directory(filepath, NotificationType::REPLAY);
|
||||
} else if(config.replay_config.record_options.show_notifications) {
|
||||
const std::string duration_str = to_duration_string(get_time_passed_in_replay_buffer_seconds());
|
||||
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg), "Saved a %s replay of %s",
|
||||
duration_str.c_str(),
|
||||
capture_target_get_notification_name(recording_capture_target.c_str(), true).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());
|
||||
}
|
||||
|
||||
if(led_indicator && config.replay_config.record_options.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg), "Saved a %s replay of %s",
|
||||
duration_str.c_str(),
|
||||
capture_target_get_notification_name(recording_capture_target.c_str(), true).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());
|
||||
}
|
||||
|
||||
if(led_indicator && config.replay_config.record_options.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
}
|
||||
|
||||
void Overlay::process_gsr_output() {
|
||||
@@ -2045,7 +2101,8 @@ namespace gsr {
|
||||
|
||||
const std::string video_filepath = filepath_get_filename(line);
|
||||
if(starts_with(video_filepath, "Video_")) {
|
||||
on_stop_recording(0, line);
|
||||
record_filepath = line;
|
||||
on_stop_recording(0, record_filepath);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2084,6 +2141,7 @@ namespace gsr {
|
||||
const char *prefix = "";
|
||||
switch(notification_type) {
|
||||
case NotificationType::NONE:
|
||||
case NotificationType::NOTICE:
|
||||
break;
|
||||
case NotificationType::SCREENSHOT:
|
||||
prefix = "Failed to take a screenshot";
|
||||
@@ -2181,21 +2239,26 @@ namespace gsr {
|
||||
exit_code = WEXITSTATUS(status);
|
||||
|
||||
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 {
|
||||
if(config.screenshot_config.show_notifications) {
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s",
|
||||
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).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());
|
||||
if(config.screenshot_config.save_screenshot_in_game_folder && config.screenshot_config.save_screenshot_to_disk) {
|
||||
save_video_in_current_game_directory(screenshot_filepath, NotificationType::SCREENSHOT);
|
||||
} else if(config.screenshot_config.show_notifications) {
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str(), true).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());
|
||||
}
|
||||
|
||||
if(config.screenshot_config.save_screenshot_to_clipboard)
|
||||
clipboard_file.set_current_file(screenshot_filepath, filename_to_clipboard_file_type(screenshot_filepath));
|
||||
}
|
||||
if(config.screenshot_config.save_screenshot_to_clipboard)
|
||||
clipboard_file.set_current_file(screenshot_filepath, filename_to_clipboard_file_type(screenshot_filepath));
|
||||
|
||||
if(led_indicator && config.screenshot_config.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
if(led_indicator && config.screenshot_config.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
|
||||
if(!strip(config.screenshot_config.custom_script).empty()) {
|
||||
std::stringstream ss;
|
||||
ss << config.screenshot_config.custom_script << " " << std::quoted(screenshot_filepath);
|
||||
const std::string command = ss.str();
|
||||
const char *args[] = { "/bin/sh", "-c", command.c_str(), nullptr };
|
||||
exec_program_on_host_daemonized(args);
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code);
|
||||
@@ -2229,6 +2292,18 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_webcam_available_to_capture(const RecordOptions &record_options) {
|
||||
if(record_options.webcam_source.empty())
|
||||
return true;
|
||||
|
||||
const auto cameras = get_v4l2_devices();
|
||||
for(const GsrCamera &camera : cameras) {
|
||||
if(camera.path == record_options.webcam_source)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Overlay::replay_status_update_status() {
|
||||
if(replay_status_update_clock.get_elapsed_time_seconds() < replay_status_update_check_timeout_seconds)
|
||||
return;
|
||||
@@ -2246,7 +2321,7 @@ namespace gsr {
|
||||
mgl_context *context = mgl_get_context();
|
||||
Display *display = (Display*)context->connection;
|
||||
|
||||
const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED);
|
||||
const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED, false);
|
||||
if(window && focused_window == (Window)window->get_system_handle())
|
||||
return;
|
||||
|
||||
@@ -2254,7 +2329,7 @@ namespace gsr {
|
||||
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_list))
|
||||
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list) && is_webcam_available_to_capture(config.replay_config.record_options))
|
||||
on_press_start_replay(false, false);
|
||||
} else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) {
|
||||
on_press_start_replay(true, false);
|
||||
@@ -2271,7 +2346,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_list))
|
||||
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list) && is_webcam_available_to_capture(config.replay_config.record_options))
|
||||
on_press_start_replay(false, false);
|
||||
} else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) {
|
||||
on_press_start_replay(false, false);
|
||||
@@ -2283,33 +2358,31 @@ 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_list))
|
||||
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list) && is_webcam_available_to_capture(config.replay_config.record_options))
|
||||
on_press_start_replay(true, false);
|
||||
}
|
||||
|
||||
void Overlay::on_stop_recording(int exit_code, const std::string &video_filepath) {
|
||||
void Overlay::on_stop_recording(int exit_code, std::string &video_filepath) {
|
||||
if(exit_code == 0) {
|
||||
if(config.record_config.save_video_in_game_folder) {
|
||||
save_video_in_current_game_directory(video_filepath.c_str(), NotificationType::RECORD);
|
||||
} else {
|
||||
if(config.record_config.record_options.show_notifications) {
|
||||
const std::string duration_str = to_duration_string(recording_duration_clock.get_elapsed_time_seconds() - paused_total_time_seconds - (paused ? paused_clock.get_elapsed_time_seconds() : 0.0));
|
||||
save_video_in_current_game_directory(video_filepath, NotificationType::RECORD);
|
||||
} else if(config.record_config.record_options.show_notifications) {
|
||||
const std::string duration_str = to_duration_string(recording_duration_clock.get_elapsed_time_seconds() - paused_total_time_seconds - (paused ? paused_clock.get_elapsed_time_seconds() : 0.0));
|
||||
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg), "Saved a %s recording of %s",
|
||||
duration_str.c_str(),
|
||||
capture_target_get_notification_name(recording_capture_target.c_str(), true).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());
|
||||
}
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg), "Saved a %s recording of %s",
|
||||
duration_str.c_str(),
|
||||
capture_target_get_notification_name(recording_capture_target.c_str(), true).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());
|
||||
}
|
||||
|
||||
if(led_indicator) {
|
||||
if(recording_status == RecordingStatus::REPLAY && !current_recording_config.replay_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(false);
|
||||
else if(recording_status == RecordingStatus::STREAM && !current_recording_config.streaming_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(false);
|
||||
else if(config.record_config.record_options.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
}
|
||||
if(led_indicator) {
|
||||
if(recording_status == RecordingStatus::REPLAY && !current_recording_config.replay_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(false);
|
||||
else if(recording_status == RecordingStatus::STREAM && !current_recording_config.streaming_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(false);
|
||||
else if(config.record_config.record_options.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
}
|
||||
} else {
|
||||
on_gsr_process_error(exit_code, NotificationType::RECORD);
|
||||
@@ -2471,8 +2544,8 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
static void add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size, const RegionSelector ®ion_selector) {
|
||||
Region region = region_selector.get_selection();
|
||||
void Overlay::add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size) {
|
||||
Region region = region_selector.get_selection(x11_dpy, wayland_dpy);
|
||||
if(region.size.x <= 32 && region.size.y <= 32) {
|
||||
region.size.x = 0;
|
||||
region.size.y = 0;
|
||||
@@ -2482,7 +2555,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, char *region_str, int region_str_size, const RegionSelector ®ion_selector) {
|
||||
void Overlay::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 std::string ®ion_area_option, RecordForceType force_type) {
|
||||
if(record_options.video_quality == "custom") {
|
||||
args.push_back("-bm");
|
||||
args.push_back("cbr");
|
||||
@@ -2493,7 +2566,7 @@ namespace gsr {
|
||||
args.push_back(record_options.video_quality.c_str());
|
||||
}
|
||||
|
||||
if(record_options.record_area_option == "focused" || record_options.change_video_resolution) {
|
||||
if(region_area_option == "focused" || record_options.change_video_resolution) {
|
||||
args.push_back("-s");
|
||||
args.push_back(region);
|
||||
}
|
||||
@@ -2503,13 +2576,18 @@ namespace gsr {
|
||||
args.push_back(audio_track.c_str());
|
||||
}
|
||||
|
||||
if(record_options.restore_portal_session) {
|
||||
if(record_options.restore_portal_session && force_type != RecordForceType::WINDOW) {
|
||||
args.push_back("-restore-portal-session");
|
||||
args.push_back("yes");
|
||||
}
|
||||
|
||||
if(record_options.record_area_option == "region")
|
||||
add_region_command(args, region_str, region_str_size, region_selector);
|
||||
if(record_options.low_power_mode) {
|
||||
args.push_back("-low-power");
|
||||
args.push_back("yes");
|
||||
}
|
||||
|
||||
if(region_area_option == "region")
|
||||
add_region_command(args, region_str, region_str_size);
|
||||
}
|
||||
|
||||
static bool validate_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
|
||||
@@ -2682,6 +2760,69 @@ namespace gsr {
|
||||
return framerate_mode;
|
||||
}
|
||||
|
||||
struct CameraAlignment {
|
||||
std::string halign;
|
||||
std::string valign;
|
||||
mgl::vec2i pos;
|
||||
};
|
||||
|
||||
static CameraAlignment position_to_alignment(mgl::vec2i pos, mgl::vec2i size) {
|
||||
const mgl::vec2i pos_overflow = mgl::vec2i(100, 100) - (pos + size);
|
||||
if(pos_overflow.x < 0)
|
||||
pos.x += pos_overflow.x;
|
||||
if(pos_overflow.y < 0)
|
||||
pos.y += pos_overflow.y;
|
||||
|
||||
if(pos.x < 0)
|
||||
pos.x = 0;
|
||||
if(pos.y < 0)
|
||||
pos.y = 0;
|
||||
|
||||
CameraAlignment camera_alignment;
|
||||
const mgl::vec2i center = pos + size/2;
|
||||
|
||||
if(center.x < 50) {
|
||||
camera_alignment.halign = "start";
|
||||
camera_alignment.pos.x = pos.x;
|
||||
} else {
|
||||
camera_alignment.halign = "end";
|
||||
camera_alignment.pos.x = -(100 - (pos.x + size.x));
|
||||
}
|
||||
|
||||
if(center.y < 50) {
|
||||
camera_alignment.valign = "start";
|
||||
camera_alignment.pos.y = pos.y;
|
||||
} else {
|
||||
camera_alignment.valign = "end";
|
||||
camera_alignment.pos.y = -(100 - (pos.y + size.y));
|
||||
}
|
||||
|
||||
return camera_alignment;
|
||||
}
|
||||
|
||||
static std::string compose_capture_source_arg(const std::string &capture_target, const RecordOptions &record_options) {
|
||||
std::string capture_source_arg = capture_target;
|
||||
if(!record_options.webcam_source.empty()) {
|
||||
const mgl::vec2i webcam_size(record_options.webcam_width, record_options.webcam_height);
|
||||
const CameraAlignment camera_alignment = position_to_alignment(mgl::vec2i(record_options.webcam_x, record_options.webcam_y), webcam_size);
|
||||
|
||||
capture_source_arg += "|" + record_options.webcam_source;
|
||||
capture_source_arg += ";halign=" + camera_alignment.halign;
|
||||
capture_source_arg += ";valign=" + camera_alignment.valign;
|
||||
capture_source_arg += ";x=" + std::to_string(camera_alignment.pos.x) + "%";
|
||||
capture_source_arg += ";y=" + std::to_string(camera_alignment.pos.y) + "%";
|
||||
capture_source_arg += ";width=" + std::to_string(webcam_size.x) + "%";
|
||||
capture_source_arg += ";height=" + std::to_string(webcam_size.y) + "%";
|
||||
capture_source_arg += ";pixfmt=" + record_options.webcam_video_format;
|
||||
capture_source_arg += ";camera_width=" + std::to_string(record_options.webcam_camera_width);
|
||||
capture_source_arg += ";camera_height=" + std::to_string(record_options.webcam_camera_height);
|
||||
capture_source_arg += ";camera_fps=" + std::to_string(record_options.webcam_camera_fps);
|
||||
if(record_options.webcam_flip_horizontally)
|
||||
capture_source_arg += ";hflip=true";
|
||||
}
|
||||
return capture_source_arg;
|
||||
}
|
||||
|
||||
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return false;
|
||||
@@ -2771,8 +2912,10 @@ namespace gsr {
|
||||
if(config.replay_config.record_options.record_area_option != "focused" && config.replay_config.record_options.change_video_resolution)
|
||||
snprintf(size, sizeof(size), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height);
|
||||
|
||||
const std::string capture_source_arg = compose_capture_source_arg(recording_capture_target, config.replay_config.record_options);
|
||||
|
||||
std::vector<const char*> args = {
|
||||
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
|
||||
"gpu-screen-recorder", "-w", capture_source_arg.c_str(),
|
||||
"-c", container,
|
||||
"-ac", config.replay_config.record_options.audio_codec.c_str(),
|
||||
"-cursor", config.replay_config.record_options.record_cursor ? "yes" : "no",
|
||||
@@ -2800,7 +2943,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
char region_str[128];
|
||||
add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, size, 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), config.replay_config.record_options.record_area_option);
|
||||
|
||||
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
|
||||
args.push_back("-ro");
|
||||
@@ -2849,7 +2992,7 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Overlay::on_press_start_record(bool finished_selection) {
|
||||
void Overlay::on_press_start_record(bool finished_selection, RecordForceType force_type) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
@@ -2873,10 +3016,12 @@ namespace gsr {
|
||||
update_upause_status();
|
||||
|
||||
if(led_indicator) {
|
||||
if(!current_recording_config.replay_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(true);
|
||||
else if(config.record_config.record_options.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
if(config.record_config.record_options.use_led_indicator) {
|
||||
if(!current_recording_config.replay_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(true);
|
||||
else
|
||||
led_indicator->blink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2903,10 +3048,12 @@ namespace gsr {
|
||||
update_upause_status();
|
||||
|
||||
if(led_indicator) {
|
||||
if(!current_recording_config.streaming_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(true);
|
||||
else if(config.record_config.record_options.use_led_indicator)
|
||||
led_indicator->blink();
|
||||
if(config.record_config.record_options.use_led_indicator) {
|
||||
if(!current_recording_config.streaming_config.record_options.use_led_indicator)
|
||||
led_indicator->set_led(true);
|
||||
else
|
||||
led_indicator->blink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2947,27 +3094,40 @@ namespace gsr {
|
||||
|
||||
update_upause_status();
|
||||
|
||||
std::string record_area_option;
|
||||
switch(force_type) {
|
||||
case RecordForceType::NONE:
|
||||
record_area_option = config.record_config.record_options.record_area_option;
|
||||
break;
|
||||
case RecordForceType::REGION:
|
||||
record_area_option = "region";
|
||||
break;
|
||||
case RecordForceType::WINDOW:
|
||||
record_area_option = gsr_info.system_info.display_server == DisplayServer::X11 ? "window" : "portal";
|
||||
break;
|
||||
}
|
||||
|
||||
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)) {
|
||||
recording_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 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, nullptr, NotificationLevel::ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.record_config.record_options.record_area_option == "region" && !finished_selection) {
|
||||
if(record_area_option == "region" && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [this]() {
|
||||
on_press_start_record(true);
|
||||
on_region_selected = [this, force_type]() {
|
||||
on_press_start_record(true, force_type);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.record_config.record_options.record_area_option == "window" && !finished_selection) {
|
||||
if(record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [this]() {
|
||||
on_press_start_record(true);
|
||||
on_window_selected = [this, force_type]() {
|
||||
on_press_start_record(true, force_type);
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -2987,14 +3147,16 @@ namespace gsr {
|
||||
|
||||
char size[64];
|
||||
size[0] = '\0';
|
||||
if(config.record_config.record_options.record_area_option == "focused")
|
||||
if(record_area_option == "focused")
|
||||
snprintf(size, sizeof(size), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height);
|
||||
|
||||
if(config.record_config.record_options.record_area_option != "focused" && config.record_config.record_options.change_video_resolution)
|
||||
if(record_area_option != "focused" && config.record_config.record_options.change_video_resolution)
|
||||
snprintf(size, sizeof(size), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height);
|
||||
|
||||
const std::string capture_source_arg = compose_capture_source_arg(recording_capture_target, config.record_config.record_options);
|
||||
|
||||
std::vector<const char*> args = {
|
||||
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
|
||||
"gpu-screen-recorder", "-w", capture_source_arg.c_str(),
|
||||
"-c", container,
|
||||
"-ac", config.record_config.record_options.audio_codec.c_str(),
|
||||
"-cursor", config.record_config.record_options.record_cursor ? "yes" : "no",
|
||||
@@ -3007,8 +3169,17 @@ namespace gsr {
|
||||
"-o", output_file.c_str()
|
||||
};
|
||||
|
||||
const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gsr-ui-window-capture-token";
|
||||
if(record_area_option == "portal") {
|
||||
hide_ui = true;
|
||||
if(force_type == RecordForceType::WINDOW) {
|
||||
args.push_back("-portal-session-token-filepath");
|
||||
args.push_back(hotkey_window_capture_portal_session_token_filepath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
char region_str[128];
|
||||
add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, size, 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), record_area_option, force_type);
|
||||
|
||||
args.push_back(nullptr);
|
||||
|
||||
@@ -3041,9 +3212,6 @@ namespace gsr {
|
||||
show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
|
||||
}
|
||||
|
||||
if(config.record_config.record_options.record_area_option == "portal")
|
||||
hide_ui = true;
|
||||
|
||||
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
|
||||
// selected what to capture and accepted it.
|
||||
recording_duration_clock.restart();
|
||||
@@ -3051,6 +3219,7 @@ namespace gsr {
|
||||
|
||||
static std::string streaming_get_url(const Config &config) {
|
||||
std::string url;
|
||||
fprintf(stderr, "streaming service: %s\n", config.streaming_config.streaming_service.c_str());
|
||||
if(config.streaming_config.streaming_service == "twitch") {
|
||||
url += "rtmp://live.twitch.tv/app/";
|
||||
url += config.streaming_config.twitch.stream_key;
|
||||
@@ -3060,6 +3229,12 @@ namespace gsr {
|
||||
} 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 == "kick") {
|
||||
url += config.streaming_config.kick.stream_url;
|
||||
if(!url.empty() && url.back() != '/')
|
||||
url += "/";
|
||||
url += "app/";
|
||||
url += config.streaming_config.kick.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)
|
||||
@@ -3172,6 +3347,10 @@ namespace gsr {
|
||||
choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
|
||||
|
||||
const std::string url = streaming_get_url(config);
|
||||
if(config.streaming_config.streaming_service == "rumble" || config.streaming_config.streaming_service == "kick") {
|
||||
fprintf(stderr, "Info: forcing video codec to h264 as rumble/kick supports only h264\n");
|
||||
video_codec = "h264";
|
||||
}
|
||||
|
||||
char size[64];
|
||||
size[0] = '\0';
|
||||
@@ -3181,8 +3360,10 @@ namespace gsr {
|
||||
if(config.streaming_config.record_options.record_area_option != "focused" && config.streaming_config.record_options.change_video_resolution)
|
||||
snprintf(size, sizeof(size), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height);
|
||||
|
||||
const std::string capture_source_arg = compose_capture_source_arg(recording_capture_target, config.streaming_config.record_options);
|
||||
|
||||
std::vector<const char*> args = {
|
||||
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
|
||||
"gpu-screen-recorder", "-w", capture_source_arg.c_str(),
|
||||
"-c", container,
|
||||
"-ac", config.streaming_config.record_options.audio_codec.c_str(),
|
||||
"-cursor", config.streaming_config.record_options.record_cursor ? "yes" : "no",
|
||||
@@ -3196,7 +3377,7 @@ namespace gsr {
|
||||
};
|
||||
|
||||
char region_str[128];
|
||||
add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, size, 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), config.streaming_config.record_options.record_area_option);
|
||||
|
||||
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
|
||||
args.push_back("-ro");
|
||||
@@ -3288,7 +3469,12 @@ namespace gsr {
|
||||
}
|
||||
|
||||
// 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::string output_file;
|
||||
if(config.screenshot_config.save_screenshot_to_disk)
|
||||
output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format
|
||||
else
|
||||
output_file = "/tmp/gsr_ui_clipboard_screenshot." + config.screenshot_config.image_format;
|
||||
|
||||
const bool capture_cursor = force_type == ScreenshotForceType::NONE && config.screenshot_config.record_cursor;
|
||||
|
||||
std::vector<const char*> args = {
|
||||
@@ -3312,7 +3498,7 @@ namespace gsr {
|
||||
args.push_back("yes");
|
||||
}
|
||||
|
||||
const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gpu-screen-recorder/gsr-ui-window-capture-token";
|
||||
const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gsr-ui-window-capture-token";
|
||||
if(record_area_option == "portal") {
|
||||
hide_ui = true;
|
||||
if(force_type == ScreenshotForceType::WINDOW) {
|
||||
@@ -3323,10 +3509,12 @@ namespace gsr {
|
||||
|
||||
char region_str[128];
|
||||
if(record_area_option == "region")
|
||||
add_region_command(args, region_str, sizeof(region_str), region_selector);
|
||||
add_region_command(args, region_str, sizeof(region_str));
|
||||
|
||||
args.push_back(nullptr);
|
||||
|
||||
clipboard_file.set_current_file("", ClipboardFile::FileType::JPG);
|
||||
|
||||
screenshot_filepath = output_file;
|
||||
gpu_screen_recorder_screenshot_process = exec_program(args.data(), nullptr);
|
||||
if(gpu_screen_recorder_screenshot_process == -1) {
|
||||
|
||||
@@ -73,6 +73,28 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool exec_program_on_host_daemonized(const char **args, bool debug) {
|
||||
if(count_num_args(args) > 64 - 3) {
|
||||
fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
if(inside_flatpak) {
|
||||
// Assumes programs wont need more than 64 - 3 args
|
||||
const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
|
||||
for(int i = 3; i < 64; ++i) {
|
||||
const char *arg = args[i - 3];
|
||||
modified_args[i] = arg;
|
||||
if(!arg)
|
||||
break;
|
||||
}
|
||||
return exec_program_daemonized(modified_args, debug);
|
||||
} else {
|
||||
return exec_program_daemonized(args, debug);
|
||||
}
|
||||
}
|
||||
|
||||
pid_t exec_program(const char **args, int *read_fd, bool debug) {
|
||||
if(read_fd)
|
||||
*read_fd = -1;
|
||||
@@ -164,11 +186,9 @@ namespace gsr {
|
||||
const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
|
||||
for(int i = 3; i < 64; ++i) {
|
||||
const char *arg = args[i - 3];
|
||||
if(!arg) {
|
||||
modified_args[i] = nullptr;
|
||||
break;
|
||||
}
|
||||
modified_args[i] = arg;
|
||||
if(!arg)
|
||||
break;
|
||||
}
|
||||
return exec_program_get_stdout(modified_args, result, debug);
|
||||
} else {
|
||||
|
||||
@@ -166,6 +166,62 @@ namespace gsr {
|
||||
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 const Monitor* get_monitor_by_region_center(const std::vector<Monitor> &monitors, Region region) {
|
||||
const mgl::vec2i center = {region.pos.x + region.size.x / 2, region.pos.y + region.size.y / 2};
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(center.x >= monitor.position.x && center.x <= monitor.position.x + monitor.size.x
|
||||
&& center.y >= monitor.position.y && center.y <= monitor.position.y + monitor.size.y)
|
||||
{
|
||||
return &monitor;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Name is the x11 name. TODO: verify if this works on all wayland compositors
|
||||
static const Monitor* get_wayland_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 mgl::vec2d to_vec2d(mgl::vec2i v) {
|
||||
return { (double)v.x, (double)v.y };
|
||||
}
|
||||
|
||||
static Region x11_region_to_wayland_region(Display *dpy, struct wl_display *wayland_dpy, Region x11_region) {
|
||||
const std::vector<Monitor> x11_monitors = get_monitors(dpy);
|
||||
const Monitor *x11_selected_monitor = get_monitor_by_region_center(x11_monitors, x11_region);
|
||||
if(!x11_selected_monitor) {
|
||||
fprintf(stderr, "Warning: RegionSelector: failed to get x11 monitor\n");
|
||||
return x11_region;
|
||||
}
|
||||
|
||||
const std::vector<Monitor> wayland_monitors = get_monitors_wayland(wayland_dpy);
|
||||
const Monitor *wayland_monitor = get_wayland_monitor_by_name(wayland_monitors, x11_selected_monitor->name);
|
||||
if(!wayland_monitor) {
|
||||
fprintf(stderr, "Warning: RegionSelector: failed to get wayland monitor\n");
|
||||
return x11_region;
|
||||
}
|
||||
|
||||
const mgl::vec2d region_relative_pos = {
|
||||
(double)(x11_region.pos.x - x11_selected_monitor->position.x) / (double)x11_selected_monitor->size.x,
|
||||
(double)(x11_region.pos.y - x11_selected_monitor->position.y) / (double)x11_selected_monitor->size.y,
|
||||
};
|
||||
|
||||
const mgl::vec2d region_relative_size = {
|
||||
(double)x11_region.size.x / (double)x11_selected_monitor->size.x,
|
||||
(double)x11_region.size.y / (double)x11_selected_monitor->size.y,
|
||||
};
|
||||
|
||||
return Region {
|
||||
wayland_monitor->position + (region_relative_pos * to_vec2d(wayland_monitor->size)).to_vec2i(),
|
||||
(region_relative_size * to_vec2d(wayland_monitor->size)).to_vec2i(),
|
||||
};
|
||||
}
|
||||
|
||||
RegionSelector::RegionSelector() {
|
||||
|
||||
}
|
||||
@@ -385,8 +441,11 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
Region RegionSelector::get_selection() const {
|
||||
return region;
|
||||
Region RegionSelector::get_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const {
|
||||
Region returned_region = region;
|
||||
if(is_wayland && x11_dpy && wayland_dpy)
|
||||
returned_region = x11_region_to_wayland_region(x11_dpy, wayland_dpy, returned_region);
|
||||
return returned_region;
|
||||
}
|
||||
|
||||
void RegionSelector::on_button_press(const void *de) {
|
||||
|
||||
@@ -48,6 +48,9 @@ namespace gsr {
|
||||
if(!theme->body_font.load_from_file(theme->body_font_file, std::max(13.0f, window_size.y * 0.015f)))
|
||||
return false;
|
||||
|
||||
if(!theme->camera_setup_font.load_from_file(theme->body_font_file, 24))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,6 +129,15 @@ namespace gsr {
|
||||
if(!theme->unmasked_texture.load_from_file((resources_path + "images/unmasked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->warning_texture.load_from_file((resources_path + "images/warning.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->question_mark_texture.load_from_file((resources_path + "images/question_mark.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->info_texture.load_from_file((resources_path + "images/info.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, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include <X11/extensions/shapeconst.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
|
||||
#include <wayland-client.h>
|
||||
#include "xdg-output-unstable-v1-client-protocol.h"
|
||||
|
||||
#include <mglpp/system/Utf8.hpp>
|
||||
|
||||
extern "C" {
|
||||
@@ -23,6 +26,209 @@ extern "C" {
|
||||
#define MAX_PROPERTY_VALUE_LEN 4096
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
struct Wayland {
|
||||
std::vector<WaylandOutput> outputs;
|
||||
struct zxdg_output_manager_v1 *xdg_output_manager = nullptr;
|
||||
};
|
||||
|
||||
static WaylandOutput* get_wayland_monitor_by_output(Wayland &wayland, struct wl_output *output) {
|
||||
for(WaylandOutput &monitor : wayland.outputs) {
|
||||
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;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*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;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*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;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*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;
|
||||
Wayland *wayland = (Wayland*)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);
|
||||
wayland->outputs.push_back(
|
||||
WaylandOutput{
|
||||
name,
|
||||
output,
|
||||
nullptr,
|
||||
mgl::vec2i{0, 0},
|
||||
mgl::vec2i{0, 0},
|
||||
0,
|
||||
""
|
||||
});
|
||||
wl_output_add_listener(output, &output_listener, 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(wayland->xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(wayland->xdg_output_manager);
|
||||
wayland->xdg_output_manager = NULL;
|
||||
}
|
||||
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)xdg_output;
|
||||
WaylandOutput *monitor = (WaylandOutput*)data;
|
||||
monitor->size.x = width;
|
||||
monitor->size.y = 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,
|
||||
};
|
||||
|
||||
static const int transform_90 = 1;
|
||||
static const int transform_270 = 3;
|
||||
|
||||
static void transform_monitors(Wayland &wayland) {
|
||||
for(WaylandOutput &output : wayland.outputs) {
|
||||
if(output.transform == transform_90 || output.transform == transform_270)
|
||||
std::swap(output.size.x, output.size.y);
|
||||
}
|
||||
}
|
||||
|
||||
static void set_monitor_outputs_from_xdg_output(Wayland &wayland, struct wl_display *dpy) {
|
||||
if(!wayland.xdg_output_manager) {
|
||||
fprintf(stderr, "Warning: WindowUtils::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for(WaylandOutput &monitor : wayland.outputs) {
|
||||
monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(wayland.xdg_output_manager, monitor.output);
|
||||
zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor);
|
||||
}
|
||||
|
||||
// Fetch xdg_output
|
||||
wl_display_roundtrip(dpy);
|
||||
}
|
||||
|
||||
static unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
|
||||
Atom ret_property_type = None;
|
||||
int ret_format = 0;
|
||||
@@ -121,7 +327,7 @@ namespace gsr {
|
||||
return root_pos;
|
||||
}
|
||||
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type) {
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type, bool fallback_cursor_focused) {
|
||||
//const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
|
||||
Window focused_window = None;
|
||||
|
||||
@@ -146,6 +352,9 @@ namespace gsr {
|
||||
XGetInputFocus(dpy, &focused_window, &revert_to);
|
||||
if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
|
||||
return focused_window;
|
||||
|
||||
if(!fallback_cursor_focused)
|
||||
return None;
|
||||
}
|
||||
|
||||
get_cursor_position(dpy, &focused_window);
|
||||
@@ -213,9 +422,9 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type, bool fallback_cursor_focused) {
|
||||
std::string result;
|
||||
const Window focused_window = get_focused_window(dpy, window_capture_type);
|
||||
const Window focused_window = get_focused_window(dpy, window_capture_type, fallback_cursor_focused);
|
||||
if(focused_window == None)
|
||||
return result;
|
||||
|
||||
@@ -516,6 +725,47 @@ namespace gsr {
|
||||
return monitors;
|
||||
}
|
||||
|
||||
std::vector<Monitor> get_monitors_wayland(struct wl_display *dpy) {
|
||||
Wayland wayland;
|
||||
|
||||
struct wl_registry *registry = wl_display_get_registry(dpy);
|
||||
wl_registry_add_listener(registry, ®istry_listener, &wayland);
|
||||
|
||||
// Fetch globals
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
// Fetch wl_output
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
transform_monitors(wayland);
|
||||
set_monitor_outputs_from_xdg_output(wayland, dpy);
|
||||
|
||||
std::vector<Monitor> monitors;
|
||||
for(WaylandOutput &output : wayland.outputs) {
|
||||
monitors.push_back(Monitor{output.pos, output.size, std::move(output.name)});
|
||||
|
||||
if(output.output) {
|
||||
wl_output_destroy(output.output);
|
||||
output.output = nullptr;
|
||||
}
|
||||
|
||||
if(output.xdg_output) {
|
||||
zxdg_output_v1_destroy(output.xdg_output);
|
||||
output.xdg_output = nullptr;
|
||||
}
|
||||
}
|
||||
wayland.outputs.clear();
|
||||
|
||||
if(wayland.xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(wayland.xdg_output_manager);
|
||||
wayland.xdg_output_manager = nullptr;
|
||||
}
|
||||
|
||||
wl_registry_destroy(registry);
|
||||
|
||||
return monitors;
|
||||
}
|
||||
|
||||
static bool device_is_mouse(const XIDeviceInfo *dev) {
|
||||
for(int i = 0; i < dev->num_classes; ++i) {
|
||||
if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer)
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return true;
|
||||
|
||||
handle_tooltip_event(event, position + offset, get_size());
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
const bool clicked_inside = mgl::FloatRect(position + offset, get_size()).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });
|
||||
if(clicked_inside) {
|
||||
|
||||
@@ -83,13 +83,27 @@ namespace gsr {
|
||||
draw_unselected(window, draw_pos);
|
||||
}
|
||||
|
||||
void ComboBox::add_item(const std::string &text, const std::string &id) {
|
||||
void ComboBox::add_item(const std::string &text, const std::string &id, bool allow_duplicate) {
|
||||
if(!allow_duplicate) {
|
||||
for(const auto &item : items) {
|
||||
if(item.id == id)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}});
|
||||
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;
|
||||
}
|
||||
|
||||
void ComboBox::clear_items() {
|
||||
items.clear();
|
||||
selected_item = 0;
|
||||
show_dropdown = false;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void ComboBox::set_selected_item(const std::string &id, bool trigger_event, bool trigger_event_even_if_selection_not_changed) {
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
auto &item = items[i];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "../../include/gui/CustomRendererWidget.hpp"
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
@@ -17,11 +18,14 @@ namespace gsr {
|
||||
|
||||
const mgl::vec2f draw_pos = position + offset;
|
||||
|
||||
const mgl::Scissor prev_scissor = window.get_scissor();
|
||||
window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()});
|
||||
const mgl::Scissor parent_scissor = window.get_scissor();
|
||||
const mgl::Scissor scissor = scissor_get_sub_area(parent_scissor, {draw_pos.to_vec2i(), size.to_vec2i()});
|
||||
window.set_scissor(scissor);
|
||||
|
||||
if(draw_handler)
|
||||
draw_handler(window, draw_pos, size);
|
||||
window.set_scissor(prev_scissor);
|
||||
|
||||
window.set_scissor(parent_scissor);
|
||||
}
|
||||
|
||||
mgl::vec2f CustomRendererWidget::get_size() {
|
||||
|
||||
@@ -304,6 +304,36 @@ namespace gsr {
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_window_region_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop recording a region:", get_color_theme().text_color));
|
||||
auto start_stop_recording_region_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
start_stop_recording_region_button_ptr = start_stop_recording_region_button.get();
|
||||
list->add_widget(std::move(start_stop_recording_region_button));
|
||||
|
||||
char str[128];
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
snprintf(str, sizeof(str), "Start/stop recording a window:");
|
||||
else
|
||||
snprintf(str, sizeof(str), "Start/stop recording with desktop portal:");
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
auto start_stop_recording_window_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
start_stop_recording_window_button_ptr = start_stop_recording_window_button.get();
|
||||
list->add_widget(std::move(start_stop_recording_window_button));
|
||||
|
||||
start_stop_recording_region_button_ptr->on_click = [this] {
|
||||
configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP_REGION);
|
||||
};
|
||||
|
||||
start_stop_recording_window_button_ptr->on_click = [this] {
|
||||
configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP_WINDOW);
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_stream_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
@@ -375,17 +405,9 @@ namespace gsr {
|
||||
|
||||
auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
clear_hotkeys_button->on_click = [this] {
|
||||
config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
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.screenshot_config.take_screenshot_window_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) {
|
||||
*config_hotkey_item = {mgl::Keyboard::Unknown, 0};
|
||||
});
|
||||
load_hotkeys();
|
||||
overlay->rebind_all_keyboard_hotkeys();
|
||||
};
|
||||
@@ -424,6 +446,7 @@ namespace gsr {
|
||||
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_record_hotkey_window_region_options());
|
||||
list_ptr->add_widget(create_stream_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_region_hotkey_options());
|
||||
@@ -595,6 +618,8 @@ namespace gsr {
|
||||
|
||||
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());
|
||||
start_stop_recording_region_button_ptr->set_text(config.record_config.start_stop_region_hotkey.to_string());
|
||||
start_stop_recording_window_button_ptr->set_text(config.record_config.start_stop_window_hotkey.to_string());
|
||||
|
||||
start_stop_streaming_button_ptr->set_text(config.streaming_config.start_stop_hotkey.to_string());
|
||||
|
||||
@@ -679,6 +704,10 @@ namespace gsr {
|
||||
return start_stop_recording_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
return pause_unpause_recording_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
return start_stop_recording_region_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
return start_stop_recording_window_button_ptr;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
return start_stop_streaming_button_ptr;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT:
|
||||
@@ -709,6 +738,10 @@ namespace gsr {
|
||||
return &config.record_config.start_stop_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
return &config.record_config.pause_unpause_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
return &config.record_config.start_stop_region_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
return &config.record_config.start_stop_window_hotkey;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
return &config.streaming_config.start_stop_hotkey;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT:
|
||||
@@ -727,8 +760,12 @@ namespace gsr {
|
||||
ConfigHotkey *config_hotkeys[] = {
|
||||
&config.replay_config.start_stop_hotkey,
|
||||
&config.replay_config.save_hotkey,
|
||||
&config.replay_config.save_1_min_hotkey,
|
||||
&config.replay_config.save_10_min_hotkey,
|
||||
&config.record_config.start_stop_hotkey,
|
||||
&config.record_config.pause_unpause_hotkey,
|
||||
&config.record_config.start_stop_region_hotkey,
|
||||
&config.record_config.start_stop_window_hotkey,
|
||||
&config.streaming_config.start_stop_hotkey,
|
||||
&config.screenshot_config.take_screenshot_hotkey,
|
||||
&config.screenshot_config.take_screenshot_region_hotkey,
|
||||
@@ -772,6 +809,15 @@ namespace gsr {
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
hotkey_configure_action_name = "Pause/unpause recording";
|
||||
break;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
hotkey_configure_action_name = "Start/stop recording a region";
|
||||
break;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
hotkey_configure_action_name = "Start/stop recording a window";
|
||||
else
|
||||
hotkey_configure_action_name = "Start/stop recording with desktop portal";
|
||||
break;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
hotkey_configure_action_name = "Start/stop streaming";
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <mglpp/system/FloatRect.hpp>
|
||||
#include <mglpp/graphics/Texture.hpp>
|
||||
|
||||
namespace gsr {
|
||||
@@ -19,8 +21,15 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return;
|
||||
|
||||
const mgl::vec2f draw_pos = (position + offset).floor();
|
||||
|
||||
if(on_mouse_move) {
|
||||
const bool mouse_inside = mgl::FloatRect(draw_pos, get_size()).contains(window.get_mouse_position().to_vec2f());
|
||||
on_mouse_move(mouse_inside);
|
||||
}
|
||||
|
||||
sprite.set_size(get_size());
|
||||
sprite.set_position((position + offset).floor());
|
||||
sprite.set_position(draw_pos);
|
||||
window.draw(sprite);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_area() {
|
||||
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture source:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(create_record_area_box());
|
||||
return record_area_list;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ namespace gsr {
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_cursor_section() {
|
||||
auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record cursor");
|
||||
record_cursor_checkbox->set_checked(true);
|
||||
@@ -163,7 +163,7 @@ namespace gsr {
|
||||
auto select_directory_page = std::make_unique<GsrPage>("File", "Settings");
|
||||
select_directory_page->add_button("Save", "save", get_color_theme().tint_color);
|
||||
select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color);
|
||||
|
||||
|
||||
auto file_chooser = std::make_unique<FileChooser>(save_directory_button_ptr->get_text().c_str(), select_directory_page->get_inner_size());
|
||||
FileChooser *file_chooser_ptr = file_chooser.get();
|
||||
select_directory_page->add_widget(std::move(file_chooser));
|
||||
@@ -221,6 +221,13 @@ namespace gsr {
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_disk() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Save screenshot to disk");
|
||||
save_screenshot_to_disk_checkbox_ptr = checkbox.get();
|
||||
checkbox->set_checked(true);
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show screenshot notifications");
|
||||
checkbox->set_checked(true);
|
||||
@@ -239,6 +246,7 @@ namespace gsr {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(create_save_screenshot_in_game_folder());
|
||||
list->add_widget(create_save_screenshot_to_clipboard());
|
||||
list->add_widget(create_save_screenshot_to_disk());
|
||||
return std::make_unique<Subsection>("General", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
@@ -249,10 +257,31 @@ namespace gsr {
|
||||
return std::make_unique<Subsection>("Screenshot indicator", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_custom_script_screenshot_entry() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL, List::Alignment::CENTER);
|
||||
|
||||
auto create_custom_script_screenshot_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
create_custom_script_screenshot_entry_ptr = create_custom_script_screenshot_entry.get();
|
||||
list->add_widget(std::move(create_custom_script_screenshot_entry));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_custom_script_screenshot() {
|
||||
auto custom_script_screenshot_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
custom_script_screenshot_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Command to open the screenshot with:", get_color_theme().text_color));
|
||||
custom_script_screenshot_list->add_widget(create_custom_script_screenshot_entry());
|
||||
return custom_script_screenshot_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_custom_script_screenshot_section() {
|
||||
return std::make_unique<Subsection>("Script", create_custom_script_screenshot(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() {
|
||||
auto page_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
page_list->set_spacing(0.018f);
|
||||
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y + 0.018f * get_theme().window_height));
|
||||
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y));
|
||||
settings_scrollable_page_ptr = scrollable_page.get();
|
||||
page_list->add_widget(std::move(scrollable_page));
|
||||
|
||||
@@ -263,6 +292,7 @@ namespace gsr {
|
||||
settings_list->add_widget(create_file_info_section());
|
||||
settings_list->add_widget(create_general_section());
|
||||
settings_list->add_widget(create_screenshot_indicator_section());
|
||||
settings_list->add_widget(create_custom_script_screenshot_section());
|
||||
settings_scrollable_page_ptr->add_widget(std::move(settings_list));
|
||||
return page_list;
|
||||
}
|
||||
@@ -305,6 +335,7 @@ namespace gsr {
|
||||
save_directory_button_ptr->set_text(config.screenshot_config.save_directory);
|
||||
save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder);
|
||||
save_screenshot_to_clipboard_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_clipboard);
|
||||
save_screenshot_to_disk_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_disk);
|
||||
show_notification_checkbox_ptr->set_checked(config.screenshot_config.show_notifications);
|
||||
led_indicator_checkbox_ptr->set_checked(config.screenshot_config.use_led_indicator);
|
||||
|
||||
@@ -321,9 +352,13 @@ namespace gsr {
|
||||
if(config.screenshot_config.image_height < 32)
|
||||
config.screenshot_config.image_height = 32;
|
||||
image_height_entry_ptr->set_text(std::to_string(config.screenshot_config.image_height));
|
||||
|
||||
create_custom_script_screenshot_entry_ptr->set_text(config.screenshot_config.custom_script);
|
||||
}
|
||||
|
||||
void ScreenshotSettingsPage::save() {
|
||||
Config prev_config = config;
|
||||
|
||||
config.screenshot_config.record_area_option = record_area_box_ptr->get_selected_id();
|
||||
config.screenshot_config.image_width = atoi(image_width_entry_ptr->get_text().c_str());
|
||||
config.screenshot_config.image_height = atoi(image_height_entry_ptr->get_text().c_str());
|
||||
@@ -335,8 +370,10 @@ namespace gsr {
|
||||
config.screenshot_config.save_directory = save_directory_button_ptr->get_text();
|
||||
config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.save_screenshot_to_clipboard = save_screenshot_to_clipboard_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.save_screenshot_to_disk = save_screenshot_to_disk_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.show_notifications = show_notification_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.use_led_indicator = led_indicator_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.custom_script = create_custom_script_screenshot_entry_ptr->get_text();
|
||||
|
||||
if(config.screenshot_config.image_width == 0)
|
||||
config.screenshot_config.image_width = 1920;
|
||||
@@ -355,5 +392,8 @@ namespace gsr {
|
||||
}
|
||||
|
||||
save_config(config);
|
||||
|
||||
if(on_config_changed && config != prev_config)
|
||||
on_config_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed || event.type == mgl::Event::MouseButtonReleased) {
|
||||
// Pass release to children even if outside area, because we want to be able to release mouse when moved outside,
|
||||
// for example in Entry when selecting text
|
||||
if(event.type == mgl::Event::MouseButtonPressed/* || event.type == mgl::Event::MouseButtonReleased*/) {
|
||||
if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_button.x, event.mouse_button.y}))
|
||||
return true;
|
||||
} else if(event.type == mgl::Event::MouseMoved) {
|
||||
|
||||
@@ -4,11 +4,22 @@
|
||||
#include "../../include/gui/PageStack.hpp"
|
||||
#include "../../include/gui/FileChooser.hpp"
|
||||
#include "../../include/gui/Subsection.hpp"
|
||||
#include "../../include/gui/CustomRendererWidget.hpp"
|
||||
#include "../../include/gui/Image.hpp"
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include "../../include/GsrInfo.hpp"
|
||||
#include "../../include/Utils.hpp"
|
||||
#include "../../include/WindowUtils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
extern "C" {
|
||||
#include <mgl/mgl.h>
|
||||
}
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string.h>
|
||||
|
||||
namespace gsr {
|
||||
@@ -40,6 +51,14 @@ namespace gsr {
|
||||
application_audio = get_application_audio();
|
||||
capture_options = get_supported_capture_options(*gsr_info);
|
||||
|
||||
mgl_context *context = mgl_get_context();
|
||||
Display *display = (Display*)context->connection;
|
||||
|
||||
const std::string wm_name = get_window_manager_name(display);
|
||||
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
|
||||
const bool is_kwin = wm_name == "KWin";
|
||||
supports_window_title = gsr_info->system_info.display_server == DisplayServer::X11 || is_hyprland || is_kwin;
|
||||
|
||||
auto content_page = std::make_unique<GsrPage>(settings_page_type_to_title_text(type), "Settings");
|
||||
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
|
||||
content_page->on_click = [page_stack](const std::string &id) {
|
||||
@@ -87,7 +106,7 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_record_area() {
|
||||
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture source:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(create_record_area_box());
|
||||
return record_area_list;
|
||||
}
|
||||
@@ -187,6 +206,291 @@ namespace gsr {
|
||||
return std::make_unique<Subsection>("Capture", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_sources() {
|
||||
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
ll->add_widget(std::make_unique<Label>(&get_theme().body_font, "Webcam source:", get_color_theme().text_color));
|
||||
|
||||
auto combobox = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
combobox->add_item("None", "");
|
||||
for(const GsrCamera &camera : capture_options.cameras) {
|
||||
combobox->add_item(camera.path, camera.path);
|
||||
}
|
||||
webcam_sources_box_ptr = combobox.get();
|
||||
|
||||
webcam_sources_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
selected_camera = std::nullopt;
|
||||
selected_camera_setup = std::nullopt;
|
||||
webcam_video_format_box_ptr->clear_items();
|
||||
if(id == "") {
|
||||
webcam_body_list_ptr->set_visible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = std::find_if(capture_options.cameras.begin(), capture_options.cameras.end(), [&](const GsrCamera &camera) {
|
||||
return camera.path == id;
|
||||
});
|
||||
if(it == capture_options.cameras.end())
|
||||
return;
|
||||
|
||||
webcam_body_list_ptr->set_visible(true);
|
||||
webcam_video_format_box_ptr->add_item("Auto (recommended)", "auto");
|
||||
|
||||
if(!it->yuyv_setups.empty())
|
||||
webcam_video_format_box_ptr->add_item("YUYV", "yuyv");
|
||||
|
||||
if(!it->mjpeg_setups.empty())
|
||||
webcam_video_format_box_ptr->add_item("Motion-JPEG", "mjpeg");
|
||||
|
||||
webcam_video_format_box_ptr->set_selected_item("auto");
|
||||
webcam_video_format_box_ptr->set_selected_item(get_current_record_options().webcam_video_format);
|
||||
selected_camera = *it;
|
||||
|
||||
// TODO: Set from config
|
||||
if(webcam_video_format_box_ptr->get_selected_id() == "yuyv" && !it->yuyv_setups.empty())
|
||||
selected_camera_setup = selected_camera->yuyv_setups.front();
|
||||
else if(webcam_video_format_box_ptr->get_selected_id() == "mjpeg" && !it->mjpeg_setups.empty())
|
||||
selected_camera_setup = selected_camera->mjpeg_setups.front();
|
||||
else if(webcam_video_format_box_ptr->get_selected_id() == "auto") {
|
||||
if(!it->mjpeg_setups.empty())
|
||||
selected_camera_setup = selected_camera->mjpeg_setups.front();
|
||||
else if(!it->yuyv_setups.empty())
|
||||
selected_camera_setup = selected_camera->yuyv_setups.front();
|
||||
}
|
||||
};
|
||||
|
||||
ll->add_widget(std::move(combobox));
|
||||
return ll;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_video_setups() {
|
||||
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
ll->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video setup:", get_color_theme().text_color));
|
||||
|
||||
auto combobox = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
webcam_video_setup_box_ptr = combobox.get();
|
||||
|
||||
ll->add_widget(std::move(combobox));
|
||||
return ll;
|
||||
}
|
||||
|
||||
static std::vector<GsrCameraSetup> sort_camera_setup(const std::vector<GsrCameraSetup> &setups) {
|
||||
auto result = setups;
|
||||
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) {
|
||||
const uint64_t score_a = (uint64_t)a.resolution.x * (uint64_t)a.resolution.y * (uint64_t)a.fps;
|
||||
const uint64_t score_b = (uint64_t)b.resolution.x * (uint64_t)b.resolution.y * (uint64_t)b.fps;
|
||||
return score_a > score_b;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_video_format() {
|
||||
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
ll->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video format:", get_color_theme().text_color));
|
||||
|
||||
auto combobox = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
webcam_video_format_box_ptr = combobox.get();
|
||||
|
||||
webcam_video_format_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
get_current_record_options().webcam_video_format = id;
|
||||
|
||||
auto it = std::find_if(capture_options.cameras.begin(), capture_options.cameras.end(), [&](const GsrCamera &camera) {
|
||||
return camera.path == webcam_sources_box_ptr->get_selected_id();
|
||||
});
|
||||
if(it == capture_options.cameras.end())
|
||||
return;
|
||||
|
||||
webcam_video_setup_box_ptr->clear_items();
|
||||
if(id == "yuyv") {
|
||||
const auto setups = sort_camera_setup(it->yuyv_setups);
|
||||
for(const auto &setup : setups) {
|
||||
char setup_str[256];
|
||||
snprintf(setup_str, sizeof(setup_str), "%dx%d@%dhz", setup.resolution.x, setup.resolution.y, setup.fps);
|
||||
webcam_video_setup_box_ptr->add_item(setup_str, setup_str, false);
|
||||
}
|
||||
} else if(id == "mjpeg") {
|
||||
const auto setups = sort_camera_setup(it->mjpeg_setups);
|
||||
for(const auto &setup : setups) {
|
||||
char setup_str[256];
|
||||
snprintf(setup_str, sizeof(setup_str), "%dx%d@%dhz", setup.resolution.x, setup.resolution.y, setup.fps);
|
||||
webcam_video_setup_box_ptr->add_item(setup_str, setup_str, false);
|
||||
}
|
||||
} else if(id == "auto") {
|
||||
auto setups = it->yuyv_setups;
|
||||
setups.insert(setups.end(), it->mjpeg_setups.begin(), it->mjpeg_setups.end());
|
||||
setups = sort_camera_setup(setups);
|
||||
|
||||
for(const auto &setup : setups) {
|
||||
char setup_str[256];
|
||||
snprintf(setup_str, sizeof(setup_str), "%dx%d@%dhz", setup.resolution.x, setup.resolution.y, setup.fps);
|
||||
webcam_video_setup_box_ptr->add_item(setup_str, setup_str, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ll->add_widget(std::move(combobox));
|
||||
return ll;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_video_setup_list() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
list->add_widget(create_webcam_video_format());
|
||||
list->add_widget(create_webcam_video_setups());
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_webcam_location_widget() {
|
||||
const float camera_screen_width = std::min(400.0f, (float)settings_scrollable_page_ptr->get_inner_size().x * 0.90f);
|
||||
camera_screen_size = mgl::vec2f(camera_screen_width, camera_screen_width * 0.5625);
|
||||
|
||||
const float screen_border = 2.0f;
|
||||
const mgl::vec2f screen_border_size(screen_border, screen_border);
|
||||
screen_inner_size = mgl::vec2f(camera_screen_size - screen_border_size*2.0f);
|
||||
|
||||
const mgl::vec2f bounding_box_size(30.0f, 30.0f);
|
||||
|
||||
auto camera_location_widget = std::make_unique<CustomRendererWidget>(camera_screen_size);
|
||||
camera_location_widget->draw_handler = [this, screen_border_size, screen_border](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) {
|
||||
if(!selected_camera.has_value() || !selected_camera_setup.has_value())
|
||||
return;
|
||||
|
||||
pos = pos.floor();
|
||||
size = size.floor();
|
||||
const mgl::vec2i mouse_pos = window.get_mouse_position();
|
||||
const mgl::vec2f webcam_box_min_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), screen_inner_size * 0.2f);
|
||||
|
||||
if(moving_webcam_box) {
|
||||
webcam_box_pos = mouse_pos.to_vec2f() - screen_border_size - webcam_box_grab_offset - pos;
|
||||
} else if(webcam_resize_corner == WebcamBoxResizeCorner::BOTTOM_RIGHT) {
|
||||
const mgl::vec2f mouse_diff = mouse_pos.to_vec2f() - webcam_resize_start_pos;
|
||||
webcam_box_size = webcam_box_size_resize_start + mouse_diff;
|
||||
}
|
||||
|
||||
webcam_box_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
|
||||
if(webcam_box_pos.x < 0.0f)
|
||||
webcam_box_pos.x = 0.0f;
|
||||
else if(webcam_box_pos.x + webcam_box_size.x > screen_inner_size.x)
|
||||
webcam_box_pos.x = screen_inner_size.x - webcam_box_size.x;
|
||||
|
||||
if(webcam_box_pos.y < 0.0f)
|
||||
webcam_box_pos.y = 0.0f;
|
||||
else if(webcam_box_pos.y + webcam_box_size.y > screen_inner_size.y)
|
||||
webcam_box_pos.y = screen_inner_size.y - webcam_box_size.y;
|
||||
|
||||
if(webcam_box_size.x < webcam_box_min_size.x)
|
||||
webcam_box_size.x = webcam_box_min_size.x;
|
||||
else if(webcam_box_pos.x + webcam_box_size.x > screen_inner_size.x)
|
||||
webcam_box_size.x = screen_inner_size.x - webcam_box_pos.x;
|
||||
|
||||
//webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
|
||||
|
||||
if(webcam_box_size.y < webcam_box_min_size.y)
|
||||
webcam_box_size.y = webcam_box_min_size.y;
|
||||
else if(webcam_box_pos.y + webcam_box_size.y > screen_inner_size.y)
|
||||
webcam_box_size.y = screen_inner_size.y - webcam_box_pos.y;
|
||||
|
||||
webcam_box_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
|
||||
{
|
||||
draw_rectangle_outline(window, pos, size, mgl::Color(255, 0, 0, 255), screen_border);
|
||||
mgl::Text screen_text("Screen", get_theme().camera_setup_font);
|
||||
screen_text.set_position((pos + size * 0.5f - screen_text.get_bounds().size * 0.5f).floor());
|
||||
window.draw(screen_text);
|
||||
}
|
||||
|
||||
{
|
||||
webcam_box_drawn_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
webcam_box_drawn_pos = (pos + screen_border_size + webcam_box_pos).floor();
|
||||
|
||||
draw_rectangle_outline(window, webcam_box_drawn_pos, webcam_box_drawn_size, mgl::Color(0, 255, 0, 255), screen_border);
|
||||
|
||||
// mgl::Rectangle resize_area(webcam_box_drawn_pos + webcam_box_drawn_size - bounding_box_size*0.5f - screen_border_size*0.5f, bounding_box_size);
|
||||
// resize_area.set_color(mgl::Color(0, 0, 255, 255));
|
||||
// window.draw(resize_area);
|
||||
|
||||
mgl::Text webcam_text("Webcam", get_theme().camera_setup_font);
|
||||
webcam_text.set_position((webcam_box_drawn_pos + webcam_box_drawn_size * 0.5f - webcam_text.get_bounds().size * 0.5f).floor());
|
||||
window.draw(webcam_text);
|
||||
}
|
||||
};
|
||||
|
||||
camera_location_widget->event_handler = [this, screen_border_size, bounding_box_size](mgl::Event &event, mgl::Window&, mgl::vec2f, mgl::vec2f) {
|
||||
switch(event.type) {
|
||||
case mgl::Event::MouseButtonPressed: {
|
||||
if(event.mouse_button.button == mgl::Mouse::Left && webcam_resize_corner == WebcamBoxResizeCorner::NONE) {
|
||||
const mgl::vec2f mouse_button_pos(event.mouse_button.x, event.mouse_button.y);
|
||||
if(mgl::FloatRect(webcam_box_drawn_pos, webcam_box_drawn_size).contains(mouse_button_pos)) {
|
||||
moving_webcam_box = true;
|
||||
webcam_box_grab_offset = mouse_button_pos - webcam_box_drawn_pos;
|
||||
} else {
|
||||
moving_webcam_box = false;
|
||||
}
|
||||
} else if(event.mouse_button.button == mgl::Mouse::Right && !moving_webcam_box) {
|
||||
const mgl::vec2f mouse_button_pos(event.mouse_button.x, event.mouse_button.y);
|
||||
webcam_resize_start_pos = mouse_button_pos;
|
||||
webcam_box_pos_resize_start = webcam_box_pos;
|
||||
webcam_box_size_resize_start = webcam_box_size;
|
||||
webcam_box_grab_offset = mouse_button_pos - webcam_box_drawn_pos;
|
||||
|
||||
/*if(mgl::FloatRect(webcam_box_drawn_pos - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::TOP_LEFT;
|
||||
fprintf(stderr, "top left\n");
|
||||
} else if(mgl::FloatRect(webcam_box_drawn_pos + mgl::vec2f(webcam_box_drawn_size.x, 0.0f) - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::TOP_RIGHT;
|
||||
fprintf(stderr, "top right\n");
|
||||
} else if(mgl::FloatRect(webcam_box_drawn_pos + mgl::vec2f(0.0f, webcam_box_drawn_size.y) - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::BOTTOM_LEFT;
|
||||
fprintf(stderr, "bottom left\n");
|
||||
} else */if(mgl::FloatRect(webcam_box_drawn_pos + webcam_box_drawn_size - bounding_box_size*0.5f - screen_border_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::BOTTOM_RIGHT;
|
||||
} else {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::NONE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case mgl::Event::MouseButtonReleased: {
|
||||
if(event.mouse_button.button == mgl::Mouse::Left && webcam_resize_corner == WebcamBoxResizeCorner::NONE) {
|
||||
moving_webcam_box = false;
|
||||
} else if(event.mouse_button.button == mgl::Mouse::Right && !moving_webcam_box) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::NONE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return camera_location_widget;
|
||||
}
|
||||
|
||||
std::unique_ptr<CheckBox> SettingsPage::create_flip_camera_checkbox() {
|
||||
auto flip_camera_horizontally_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Flip camera horizontally");
|
||||
flip_camera_horizontally_checkbox_ptr = flip_camera_horizontally_checkbox.get();
|
||||
return flip_camera_horizontally_checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_body() {
|
||||
auto body_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
webcam_body_list_ptr = body_list.get();
|
||||
body_list->set_visible(false);
|
||||
body_list->add_widget(create_webcam_location_widget());
|
||||
body_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "* Right click in the bottom right corner to resize the webcam", get_color_theme().text_color));
|
||||
body_list->add_widget(create_flip_camera_checkbox());
|
||||
body_list->add_widget(create_webcam_video_setup_list());
|
||||
return body_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_webcam_section() {
|
||||
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
ll->add_widget(create_webcam_sources());
|
||||
ll->add_widget(create_webcam_body());
|
||||
return std::make_unique<Subsection>("Webcam", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
static bool audio_device_is_output(const std::string &audio_device_id) {
|
||||
return audio_device_id == "default_output" || ends_with(audio_device_id, ".monitor");
|
||||
}
|
||||
@@ -231,8 +535,9 @@ namespace gsr {
|
||||
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]() {
|
||||
remove_audio_track_button->on_click = [this, audio_input_list_ptr, audio_device_list_ptr]() {
|
||||
audio_input_list_ptr->remove_widget(audio_device_list_ptr);
|
||||
update_application_audio_warning_visibility();
|
||||
};
|
||||
return remove_audio_track_button;
|
||||
}
|
||||
@@ -255,11 +560,48 @@ namespace gsr {
|
||||
return button;
|
||||
}
|
||||
|
||||
void SettingsPage::update_application_audio_warning_visibility() {
|
||||
audio_track_section_list_ptr->for_each_child_widget([](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));
|
||||
List *application_audio_warning_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(4));
|
||||
|
||||
int num_output_devices = 0;
|
||||
int num_application_audio = 0;
|
||||
|
||||
audio_input_list_ptr->for_each_child_widget([&num_output_devices, &num_application_audio](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: {
|
||||
Label *label = dynamic_cast<Label*>(audio_track_line->get_child_widget_by_index(0));
|
||||
const bool is_output_device = starts_with(label->get_text().c_str(), "Output device");
|
||||
if(is_output_device)
|
||||
num_output_devices++;
|
||||
break;
|
||||
}
|
||||
case AudioTrackType::APPLICATION:
|
||||
case AudioTrackType::APPLICATION_CUSTOM: {
|
||||
num_application_audio++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
application_audio_warning_list_ptr->set_visible(num_output_devices > 0 && (num_application_audio > 0 || application_audio_invert_checkbox_ptr->is_checked()));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
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_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::OUTPUT, audio_input_list_ptr));
|
||||
update_application_audio_warning_visibility();
|
||||
};
|
||||
return button;
|
||||
}
|
||||
@@ -318,6 +660,8 @@ namespace gsr {
|
||||
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));
|
||||
|
||||
update_application_audio_warning_visibility();
|
||||
};
|
||||
return add_audio_track_button;
|
||||
}
|
||||
@@ -339,9 +683,24 @@ namespace gsr {
|
||||
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->on_changed = [this](bool) {
|
||||
update_application_audio_warning_visibility();
|
||||
};
|
||||
return application_audio_invert_checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_application_audio_warning() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
list->set_spacing(0.003f);
|
||||
list->set_visible(false);
|
||||
|
||||
const int font_character_size = get_theme().body_font.get_character_size();
|
||||
list->add_widget(std::make_unique<Image>(&get_theme().warning_texture, mgl::vec2f(font_character_size, font_character_size), Image::ScaleBehavior::SCALE));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Recording output devices and application audio may record all output audio, which is likely\nnot what you want to do. Remove the output devices.", get_color_theme().text_color));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -389,6 +748,7 @@ namespace gsr {
|
||||
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());
|
||||
list_ptr->add_widget(create_application_audio_warning());
|
||||
|
||||
set_application_audio_options_visible(subsection.get(), view_radio_button_ptr->get_selected_id() == "advanced", *gsr_info);
|
||||
return subsection;
|
||||
@@ -618,6 +978,7 @@ namespace gsr {
|
||||
auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
settings_list->set_spacing(0.018f);
|
||||
settings_list->add_widget(create_capture_target_section());
|
||||
settings_list->add_widget(create_webcam_section());
|
||||
settings_list->add_widget(create_audio_section());
|
||||
settings_list->add_widget(create_video_section());
|
||||
settings_list_ptr = settings_list.get();
|
||||
@@ -765,6 +1126,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() {
|
||||
// TODO: Support kde plasma wayland and hyprland (same ones that support getting window title)
|
||||
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)");
|
||||
|
||||
@@ -779,7 +1141,7 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<CheckBox> SettingsPage::create_save_replay_in_game_folder() {
|
||||
char text[256];
|
||||
snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
|
||||
snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", supports_window_title ? "" : " (X11 applications only)");
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
|
||||
save_replay_in_game_folder_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
@@ -830,6 +1192,16 @@ namespace gsr {
|
||||
settings_scrollable_page_ptr->reset_scroll();
|
||||
}
|
||||
|
||||
RecordOptions& SettingsPage::get_current_record_options() {
|
||||
switch(type) {
|
||||
default:
|
||||
assert(false);
|
||||
case Type::REPLAY: return config.replay_config.record_options;
|
||||
case Type::RECORD: return config.record_config.record_options;
|
||||
case Type::STREAM: return config.streaming_config.record_options;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<CheckBox> SettingsPage::create_led_indicator(const char *type) {
|
||||
char label_str[256];
|
||||
snprintf(label_str, sizeof(label_str), "Show %s status with scroll lock led", type);
|
||||
@@ -856,6 +1228,33 @@ namespace gsr {
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_low_power_mode() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
list->set_visible(gsr_info->gpu_info.vendor == GpuVendor::AMD);
|
||||
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record in low-power mode");
|
||||
low_power_mode_checkbox_ptr = checkbox.get();
|
||||
|
||||
list->add_widget(std::move(checkbox));
|
||||
|
||||
auto info = std::make_unique<Image>(&get_theme().question_mark_texture, low_power_mode_checkbox_ptr->get_size(), Image::ScaleBehavior::SCALE);
|
||||
info->set_tooltip_text(
|
||||
"Do not force the GPU to go into high performance mode when recording.\n"
|
||||
"May affect recording performance, especially when playing a video at the same time.\n"
|
||||
"If enabled then it's recommended to use sync to content frame rate mode to reduce power usage when idle."
|
||||
);
|
||||
Image *info_ptr = info.get();
|
||||
info->on_mouse_move = [info_ptr](bool inside) {
|
||||
if(inside)
|
||||
set_current_tooltip(info_ptr);
|
||||
else
|
||||
remove_as_current_tooltip(info_ptr);
|
||||
};
|
||||
list->add_widget(std::move(info));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -871,6 +1270,7 @@ namespace gsr {
|
||||
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());
|
||||
general_list->add_widget(create_low_power_mode());
|
||||
|
||||
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>("Replay indicator", create_indicator("replay"), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
@@ -894,7 +1294,7 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<CheckBox> SettingsPage::create_save_recording_in_game_folder() {
|
||||
char text[256];
|
||||
snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
|
||||
snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", supports_window_title ? "" : " (X11 applications only)");
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
|
||||
save_recording_in_game_folder_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
@@ -927,6 +1327,7 @@ namespace gsr {
|
||||
|
||||
auto general_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
general_list->add_widget(create_save_recording_in_game_folder());
|
||||
general_list->add_widget(create_low_power_mode());
|
||||
|
||||
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>("Recording indicator", create_indicator("recording"), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
@@ -947,6 +1348,7 @@ namespace gsr {
|
||||
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("Kick", "kick");
|
||||
streaming_service_box->add_item("Custom", "custom");
|
||||
streaming_service_box_ptr = streaming_service_box.get();
|
||||
return streaming_service_box;
|
||||
@@ -990,6 +1392,8 @@ namespace gsr {
|
||||
twitch_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
youtube_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
rumble_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
kick_stream_url_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
kick_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
|
||||
stream_key_list_ptr = stream_key_list.get();
|
||||
return stream_key_list;
|
||||
@@ -1056,7 +1460,7 @@ namespace gsr {
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("Streaming info", std::move(streaming_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_save_recording_in_game_folder());
|
||||
general_list->add_widget(create_low_power_mode());
|
||||
|
||||
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>("Streaming indicator", create_indicator("streaming"), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
@@ -1065,12 +1469,15 @@ namespace gsr {
|
||||
const bool twitch_option = id == "twitch";
|
||||
const bool youtube_option = id == "youtube";
|
||||
const bool rumble_option = id == "rumble";
|
||||
const bool kick_option = id == "kick";
|
||||
const bool custom_option = id == "custom";
|
||||
stream_key_list_ptr->set_visible(!custom_option);
|
||||
custom_stream_list_ptr->set_visible(custom_option);
|
||||
twitch_stream_key_entry_ptr->get_parent_widget()->set_visible(twitch_option);
|
||||
youtube_stream_key_entry_ptr->get_parent_widget()->set_visible(youtube_option);
|
||||
rumble_stream_key_entry_ptr->get_parent_widget()->set_visible(rumble_option);
|
||||
kick_stream_url_entry_ptr->get_parent_widget()->set_visible(kick_option);
|
||||
kick_stream_key_entry_ptr->get_parent_widget()->set_visible(kick_option);
|
||||
return true;
|
||||
};
|
||||
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");
|
||||
@@ -1181,6 +1588,8 @@ namespace gsr {
|
||||
auto audio_track_section = create_audio_track_section(audio_section_ptr);
|
||||
audio_track_section_list_ptr->add_widget(std::move(audio_track_section));
|
||||
}
|
||||
|
||||
update_application_audio_warning_visibility();
|
||||
}
|
||||
|
||||
void SettingsPage::load_common(RecordOptions &record_options) {
|
||||
@@ -1199,6 +1608,22 @@ namespace gsr {
|
||||
restore_portal_session_checkbox_ptr->set_checked(record_options.restore_portal_session);
|
||||
show_notification_checkbox_ptr->set_checked(record_options.show_notifications);
|
||||
led_indicator_checkbox_ptr->set_checked(record_options.use_led_indicator);
|
||||
low_power_mode_checkbox_ptr->set_checked(record_options.low_power_mode);
|
||||
|
||||
char webcam_setup_str[256];
|
||||
snprintf(webcam_setup_str, sizeof(webcam_setup_str), "%dx%d@%dhz", record_options.webcam_camera_width, record_options.webcam_camera_height, record_options.webcam_camera_fps);
|
||||
|
||||
webcam_sources_box_ptr->set_selected_item(record_options.webcam_source);
|
||||
flip_camera_horizontally_checkbox_ptr->set_checked(record_options.webcam_flip_horizontally);
|
||||
webcam_video_format_box_ptr->set_selected_item(record_options.webcam_video_format);
|
||||
webcam_video_setup_box_ptr->set_selected_item(webcam_setup_str);
|
||||
webcam_box_pos.x = ((float)record_options.webcam_x / 100.0f) * screen_inner_size.x;
|
||||
webcam_box_pos.y = ((float)record_options.webcam_y / 100.0f) * screen_inner_size.y;
|
||||
webcam_box_size.x = ((float)record_options.webcam_width / 100.0f * screen_inner_size.x);
|
||||
webcam_box_size.y = ((float)record_options.webcam_height / 100.0f * screen_inner_size.y);
|
||||
|
||||
if(selected_camera_setup.has_value())
|
||||
webcam_box_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
|
||||
if(record_options.record_area_width == 0)
|
||||
record_options.record_area_width = 1920;
|
||||
@@ -1268,6 +1693,8 @@ namespace gsr {
|
||||
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);
|
||||
kick_stream_url_entry_ptr->set_text(config.streaming_config.kick.stream_url);
|
||||
kick_stream_key_entry_ptr->set_text(config.streaming_config.kick.stream_key);
|
||||
stream_url_entry_ptr->set_text(config.streaming_config.custom.url);
|
||||
stream_key_entry_ptr->set_text(config.streaming_config.custom.key);
|
||||
container_box_ptr->set_selected_item(config.streaming_config.custom.container);
|
||||
@@ -1331,6 +1758,28 @@ namespace gsr {
|
||||
record_options.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked();
|
||||
record_options.show_notifications = show_notification_checkbox_ptr->is_checked();
|
||||
record_options.use_led_indicator = led_indicator_checkbox_ptr->is_checked();
|
||||
record_options.low_power_mode = low_power_mode_checkbox_ptr->is_checked();
|
||||
|
||||
// TODO: Set selected_camera_setup properly when updating and shit
|
||||
|
||||
if(selected_camera_setup.has_value())
|
||||
webcam_box_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
|
||||
int camera_width = 0;
|
||||
int camera_height = 0;
|
||||
int camera_fps = 0;
|
||||
sscanf(webcam_video_setup_box_ptr->get_selected_id().c_str(), "%dx%d@%dhz", &camera_width, &camera_height, &camera_fps);
|
||||
|
||||
record_options.webcam_source = webcam_sources_box_ptr->get_selected_id();
|
||||
record_options.webcam_flip_horizontally = flip_camera_horizontally_checkbox_ptr->is_checked();
|
||||
record_options.webcam_video_format = webcam_video_format_box_ptr->get_selected_id();
|
||||
record_options.webcam_camera_width = camera_width;
|
||||
record_options.webcam_camera_height = camera_height;
|
||||
record_options.webcam_camera_fps = camera_fps;
|
||||
record_options.webcam_x = std::round((webcam_box_pos.x / screen_inner_size.x) * 100.0f);
|
||||
record_options.webcam_y = std::round((webcam_box_pos.y / screen_inner_size.y) * 100.0f);
|
||||
record_options.webcam_width = std::round((webcam_box_size.x / screen_inner_size.x) * 100.0f);
|
||||
record_options.webcam_height = std::round((webcam_box_size.y / screen_inner_size.y) * 100.0f);
|
||||
|
||||
if(record_options.record_area_width == 0)
|
||||
record_options.record_area_width = 1920;
|
||||
@@ -1405,6 +1854,8 @@ namespace gsr {
|
||||
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.kick.stream_url = kick_stream_url_entry_ptr->get_text();
|
||||
config.streaming_config.kick.stream_key = kick_stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.custom.url = stream_url_entry_ptr->get_text();
|
||||
config.streaming_config.custom.key = stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.custom.container = container_box_ptr->get_selected_id();
|
||||
|
||||
67
src/gui/Tooltip.cpp
Normal file
67
src/gui/Tooltip.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "../../include/gui/Tooltip.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
|
||||
#include <mglpp/graphics/Rectangle.hpp>
|
||||
#include <mglpp/graphics/Sprite.hpp>
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static const float padding_top_scale = 0.008f;
|
||||
static const float padding_bottom_scale = 0.008f;
|
||||
static const float padding_left_scale = 0.008f;
|
||||
static const float padding_right_scale = 0.008f;
|
||||
static const float accent_scale = 0.0025f;
|
||||
|
||||
Tooltip::Tooltip(mgl::Font *font) : label("", *font) {}
|
||||
|
||||
bool Tooltip::on_event(mgl::Event&, mgl::Window&, mgl::vec2f) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Tooltip::draw(mgl::Window &window, mgl::vec2f offset) {
|
||||
if(!visible)
|
||||
return;
|
||||
|
||||
const mgl::vec2f draw_pos = (window.get_mouse_position().to_vec2f() + offset).floor();
|
||||
|
||||
const int padding_top = get_theme().window_height * padding_top_scale;
|
||||
const int padding_left = get_theme().window_height * padding_left_scale;
|
||||
const int accent_height = get_theme().window_height * accent_scale;
|
||||
const int icon_height = label.get_font()->get_character_size();
|
||||
|
||||
mgl::Rectangle background(get_size());
|
||||
background.set_position(draw_pos - mgl::vec2f(0.0f, background.get_size().y));
|
||||
background.set_color(mgl::Color(0, 0, 0));
|
||||
window.draw(background);
|
||||
|
||||
mgl::Rectangle accent(background.get_position(), mgl::vec2f(background.get_size().x, accent_height).floor());
|
||||
accent.set_color(get_color_theme().tint_color);
|
||||
window.draw(accent);
|
||||
|
||||
mgl::Sprite icon_sprite(&get_theme().info_texture, background.get_position() + mgl::vec2f(padding_left, accent_height + padding_top).floor());
|
||||
icon_sprite.set_height(icon_height);
|
||||
window.draw(icon_sprite);
|
||||
|
||||
label.set_position(background.get_position() + mgl::vec2f(padding_left, accent_height + padding_top + icon_sprite.get_size().y).floor());
|
||||
window.draw(label);
|
||||
}
|
||||
|
||||
mgl::vec2f Tooltip::get_size() {
|
||||
if(!visible)
|
||||
return {0.0f, 0.0f};
|
||||
|
||||
const int padding_top = get_theme().window_height * padding_top_scale;
|
||||
const int padding_bottom = get_theme().window_height * padding_bottom_scale;
|
||||
const int padding_left = get_theme().window_height * padding_left_scale;
|
||||
const int padding_right = get_theme().window_height * padding_right_scale;
|
||||
const int accent_height = get_theme().window_height * accent_scale;
|
||||
const mgl::vec2f text_size = label.get_bounds().size.floor();
|
||||
const int icon_height = label.get_font()->get_character_size();
|
||||
|
||||
return mgl::vec2f(padding_left + text_size.x + padding_right, accent_height + padding_top + icon_height + text_size.y + padding_bottom).floor();
|
||||
}
|
||||
|
||||
void Tooltip::set_text(std::string text) {
|
||||
label.set_string(std::move(text));
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,15 @@
|
||||
namespace gsr {
|
||||
static double frame_delta_seconds = 1.0;
|
||||
|
||||
static mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
return { std::min(a.x, b.x), std::min(a.y, b.y) };
|
||||
}
|
||||
|
||||
static mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
return { std::max(a.x, b.x), std::max(a.y, b.y) };
|
||||
}
|
||||
|
||||
static mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
|
||||
mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
|
||||
return min_vec2i(max, max_vec2i(value, min));
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace gsr {
|
||||
const mgl::vec2i pos = clamp_vec2i(child.position, parent.position, parent.position + parent.size);
|
||||
return mgl::Scissor{
|
||||
pos,
|
||||
min_vec2i(child.size, parent.position + parent.size - pos)
|
||||
max_vec2i(mgl::vec2i(0, 0), min_vec2i(child.position + child.size - pos, parent.position + parent.size - pos))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
#include "../../include/gui/Widget.hpp"
|
||||
#include "../../include/gui/Tooltip.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include <vector>
|
||||
|
||||
#include <mglpp/window/Event.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static std::vector<std::unique_ptr<Widget>> widgets_to_remove;
|
||||
static Widget *current_tooltip_widget = nullptr;
|
||||
static std::unique_ptr<Tooltip> tooltip;
|
||||
|
||||
static void set_current_tooltip_text(Widget *widget);
|
||||
|
||||
Widget::Widget() {
|
||||
|
||||
@@ -10,6 +18,7 @@ namespace gsr {
|
||||
|
||||
Widget::~Widget() {
|
||||
remove_widget_as_selected_in_parent();
|
||||
remove_as_current_tooltip(this);
|
||||
}
|
||||
|
||||
void Widget::set_position(mgl::vec2f position) {
|
||||
@@ -64,10 +73,34 @@ namespace gsr {
|
||||
this->visible = visible;
|
||||
}
|
||||
|
||||
bool Widget::is_visible() const {
|
||||
return visible;
|
||||
}
|
||||
|
||||
Widget* Widget::get_parent_widget() {
|
||||
return parent_widget;
|
||||
}
|
||||
|
||||
void Widget::set_tooltip_text(std::string text) {
|
||||
tooltip_text = std::move(text);
|
||||
if(current_tooltip_widget == this)
|
||||
set_current_tooltip_text(current_tooltip_widget);
|
||||
}
|
||||
|
||||
const std::string& Widget::get_tooltip_text() const {
|
||||
return tooltip_text;
|
||||
}
|
||||
|
||||
void Widget::handle_tooltip_event(mgl::Event &event, mgl::vec2f position, mgl::vec2f size) {
|
||||
if(event.type == mgl::Event::MouseMoved) {
|
||||
if(mgl::FloatRect(position, size).contains(mgl::vec2f(event.mouse_move.x, event.mouse_move.y))) {
|
||||
set_current_tooltip(this);
|
||||
} else {
|
||||
remove_as_current_tooltip(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add_widget_to_remove(std::unique_ptr<Widget> widget) {
|
||||
widgets_to_remove.push_back(std::move(widget));
|
||||
}
|
||||
@@ -78,4 +111,40 @@ namespace gsr {
|
||||
}
|
||||
widgets_to_remove.clear();
|
||||
}
|
||||
|
||||
void set_current_tooltip(Widget *widget) {
|
||||
if(current_tooltip_widget == widget)
|
||||
return;
|
||||
|
||||
set_current_tooltip_text(widget);
|
||||
}
|
||||
|
||||
void remove_as_current_tooltip(Widget *widget) {
|
||||
if(current_tooltip_widget == widget)
|
||||
set_current_tooltip_text(nullptr);
|
||||
}
|
||||
|
||||
void set_current_tooltip_text(Widget *widget) {
|
||||
if(widget && !widget->get_tooltip_text().empty()) {
|
||||
current_tooltip_widget = widget;
|
||||
if(!tooltip)
|
||||
tooltip = std::make_unique<Tooltip>(&get_theme().body_font);
|
||||
tooltip->set_text(current_tooltip_widget->get_tooltip_text());
|
||||
} else {
|
||||
current_tooltip_widget = nullptr;
|
||||
tooltip.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void draw_tooltip(mgl::Window &window) {
|
||||
if(!tooltip)
|
||||
return;
|
||||
|
||||
if(!current_tooltip_widget->is_visible()) {
|
||||
set_current_tooltip(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip->draw(window, mgl::vec2f(0.0f, 0.0f));
|
||||
}
|
||||
}
|
||||
69
src/main.cpp
69
src/main.cpp
@@ -3,6 +3,7 @@
|
||||
#include "../include/gui/Utils.hpp"
|
||||
#include "../include/Process.hpp"
|
||||
#include "../include/Rpc.hpp"
|
||||
#include "../include/Theme.hpp"
|
||||
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
@@ -23,8 +24,10 @@ extern "C" {
|
||||
}
|
||||
|
||||
static sig_atomic_t running = 1;
|
||||
static sig_atomic_t killed = 0;
|
||||
static void sigint_handler(int signal) {
|
||||
(void)signal;
|
||||
killed = 1;
|
||||
running = 0;
|
||||
}
|
||||
|
||||
@@ -53,7 +56,7 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
|
||||
rpc->add_handler("toggle-record", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record();
|
||||
overlay->toggle_record(gsr::RecordForceType::NONE);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-pause", [overlay](const std::string &name) {
|
||||
@@ -61,6 +64,16 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
overlay->toggle_pause();
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-record-region", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record(gsr::RecordForceType::REGION);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-record-window", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record(gsr::RecordForceType::WINDOW);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-stream", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_stream();
|
||||
@@ -162,9 +175,10 @@ static void set_display_server_environment_variables() {
|
||||
static void usage() {
|
||||
printf("usage: gsr-ui [action]\n");
|
||||
printf("OPTIONS:\n");
|
||||
printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\" or \"launch-daemon\". Optional, defaults to \"launch-hide\".\n");
|
||||
printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\", \"launch-hide-announce\" 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. The UI will be opened if the program is already running in another process.\n");
|
||||
printf(" If \"launch-hide-announce\" is used then the program starts but the UI is not opened until Alt+Z is pressed and a notification tells the user to press Alt+Z. 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);
|
||||
}
|
||||
@@ -172,6 +186,7 @@ static void usage() {
|
||||
enum class LaunchAction {
|
||||
LAUNCH_SHOW,
|
||||
LAUNCH_HIDE,
|
||||
LAUNCH_HIDE_ANNOUNCE,
|
||||
LAUNCH_DAEMON
|
||||
};
|
||||
|
||||
@@ -195,10 +210,12 @@ 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-hide-announce") == 0) {
|
||||
launch_action = LaunchAction::LAUNCH_HIDE_ANNOUNCE;
|
||||
} else if(strcmp(launch_action_opt, "launch-daemon") == 0) {
|
||||
launch_action = LaunchAction::LAUNCH_DAEMON;
|
||||
} else {
|
||||
printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\" or \"launch-daemon\".\n", launch_action_opt);
|
||||
printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\", \"launch-hide-announce\" or \"launch-daemon\".\n", launch_action_opt);
|
||||
usage();
|
||||
}
|
||||
} else {
|
||||
@@ -207,6 +224,19 @@ int main(int argc, char **argv) {
|
||||
|
||||
set_display_server_environment_variables();
|
||||
|
||||
std::string resources_path;
|
||||
if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) {
|
||||
resources_path = "./";
|
||||
} else {
|
||||
#ifdef GSR_UI_RESOURCES_PATH
|
||||
resources_path = GSR_UI_RESOURCES_PATH "/";
|
||||
#else
|
||||
resources_path = "/usr/share/gsr-ui/";
|
||||
#endif
|
||||
}
|
||||
|
||||
const std::string gsr_icon_path = resources_path + "images/gpu_screen_recorder_logo.png";
|
||||
|
||||
auto rpc = std::make_unique<gsr::Rpc>();
|
||||
const gsr::RpcOpenResult rpc_open_result = rpc->open("gsr-ui");
|
||||
|
||||
@@ -218,7 +248,10 @@ int main(int argc, char **argv) {
|
||||
fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n");
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n");
|
||||
const char *args[] = { "gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
|
||||
const char *args[] = {
|
||||
"gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.", "--timeout", "5.0",
|
||||
"--icon-color", "ffffff", "--icon", gsr_icon_path.c_str(), "--bg-color", "ff0000", nullptr
|
||||
};
|
||||
gsr::exec_program_daemonized(args);
|
||||
}
|
||||
return 1;
|
||||
@@ -228,7 +261,10 @@ int main(int argc, char **argv) {
|
||||
fprintf(stderr, "Error: Failed to create rpc\n");
|
||||
|
||||
if(gsr::pidof("gpu-screen-recorder", -1) != -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 };
|
||||
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", "ffffff", "--icon", gsr_icon_path.c_str(), "--bg-color", "ff0000", nullptr
|
||||
};
|
||||
gsr::exec_program_daemonized(args);
|
||||
}
|
||||
|
||||
@@ -282,17 +318,6 @@ int main(int argc, char **argv) {
|
||||
|
||||
gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info);
|
||||
|
||||
std::string resources_path;
|
||||
if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) {
|
||||
resources_path = "./";
|
||||
} else {
|
||||
#ifdef GSR_UI_RESOURCES_PATH
|
||||
resources_path = GSR_UI_RESOURCES_PATH "/";
|
||||
#else
|
||||
resources_path = "/usr/share/gsr-ui/";
|
||||
#endif
|
||||
}
|
||||
|
||||
mgl_context *context = mgl_get_context();
|
||||
|
||||
egl_functions egl_funcs;
|
||||
@@ -311,6 +336,8 @@ int main(int argc, char **argv) {
|
||||
auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs);
|
||||
if(launch_action == LaunchAction::LAUNCH_SHOW)
|
||||
overlay->show();
|
||||
else if(launch_action == LaunchAction::LAUNCH_HIDE_ANNOUNCE)
|
||||
overlay->show_notification("Press Alt+Z to open the GPU Screen Recorder UI", 5.0, mgl::Color(255, 255, 255), gsr::get_color_theme().tint_color, gsr::NotificationType::NOTICE, nullptr, gsr::NotificationLevel::ERROR);
|
||||
|
||||
rpc_add_commands(rpc.get(), overlay.get());
|
||||
|
||||
@@ -331,6 +358,8 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
const bool connected_to_display_server = mgl_is_connected_to_display_server();
|
||||
|
||||
fprintf(stderr, "Info: shutting down!\n");
|
||||
rpc.reset();
|
||||
overlay.reset();
|
||||
@@ -344,5 +373,11 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return mgl_is_connected_to_display_server() ? 0 : 1;
|
||||
if(killed)
|
||||
return 0;
|
||||
|
||||
if(connected_to_display_server)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -200,6 +200,9 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
return;
|
||||
}
|
||||
|
||||
if(extra_data->is_non_keyboard_device)
|
||||
return;
|
||||
|
||||
if(event.type == EV_SYN && event.code == SYN_DROPPED) {
|
||||
/* TODO: Don't do this on every SYN_DROPPED to prevent spamming this, instead wait until the next event or wait for timeout */
|
||||
keyboard_event_fetch_update_key_states(self, extra_data, fd);
|
||||
@@ -210,6 +213,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);
|
||||
//}
|
||||
|
||||
const bool prev_grabbed = extra_data->grabbed;
|
||||
|
||||
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);
|
||||
@@ -228,7 +233,7 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
}
|
||||
|
||||
if(extra_data->grabbed) {
|
||||
if(!self->check_grab_lock && (event.type == EV_KEY || event.type == EV_MSC)) {
|
||||
if(prev_grabbed && !self->check_grab_lock && (event.type == EV_KEY || event.type == EV_MSC)) {
|
||||
self->uinput_written_time_seconds = clock_get_monotonic_seconds();
|
||||
self->check_grab_lock = true;
|
||||
}
|
||||
@@ -238,17 +243,6 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
fprintf(stderr, "Error: failed to write event data to virtual keyboard for exclusively grabbed device\n");
|
||||
}
|
||||
|
||||
if(event.type == EV_LED) {
|
||||
write(fd, &event, sizeof(event));
|
||||
|
||||
const struct input_event syn_event = {
|
||||
.type = EV_SYN,
|
||||
.code = 0,
|
||||
.value = 0
|
||||
};
|
||||
write(fd, &syn_event, sizeof(syn_event));
|
||||
}
|
||||
|
||||
if(!extra_data->is_possibly_non_keyboard_device)
|
||||
return;
|
||||
|
||||
|
||||
95
tools/gsr-hyprland-helper/main.c
Normal file
95
tools/gsr-hyprland-helper/main.c
Normal file
@@ -0,0 +1,95 @@
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
static bool get_hyprland_socket_path(char *path, int path_len) {
|
||||
const char* xdg_runtime_dir = getenv("XDG_RUNTIME_DIR");
|
||||
const char* instance_sig = getenv("HYPRLAND_INSTANCE_SIGNATURE");
|
||||
if (!xdg_runtime_dir || !instance_sig) {
|
||||
fprintf(stderr, "Error: gsr-hypland-helper: environment variables not set\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (snprintf(path, path_len, "%s/hypr/%s/.socket2.sock", xdg_runtime_dir, instance_sig) >= path_len) {
|
||||
fprintf(stderr, "Error: gsr-hypland-helper: path to hyprland socket (%s/hypr/%s/.socket2.sock) is more than %d characters long\n", xdg_runtime_dir, instance_sig, path_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void print_window_title(const char *window_title) {
|
||||
if (window_title[0] == 0) {
|
||||
printf("Window title changed: %s\n", "Desktop");
|
||||
fflush(stdout);
|
||||
return;
|
||||
}
|
||||
printf("Window title changed: %s\n", window_title);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static int handle_ipc(void) {
|
||||
const int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (sock_fd == -1) {
|
||||
perror("Error: gsr-hyprland-helper: socket");
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct sockaddr_un addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
|
||||
if (!get_hyprland_socket_path(addr.sun_path, sizeof(addr.sun_path))) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
perror("Error: gsr-hyprland-helper: connect");
|
||||
close(sock_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Info: gsr-hyprland-helper: connected to Hyprland socket: %s\n", addr.sun_path);
|
||||
|
||||
FILE *sock_file = fdopen(sock_fd, "r");
|
||||
if (!sock_file) {
|
||||
perror("Error: gsr-hyprland-helper: fdopen");
|
||||
close(sock_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char buffer[1024];
|
||||
while (fgets(buffer, sizeof(buffer), sock_file)) {
|
||||
int line_len = strlen(buffer);
|
||||
if(line_len > 0 && buffer[line_len - 1] == '\n') {
|
||||
buffer[line_len - 1] = '\0';
|
||||
line_len -= 1;
|
||||
}
|
||||
|
||||
if(line_len >= 14 && memcmp(buffer, "activewindow>>", 14) == 0) {
|
||||
char *window_id = buffer + 14;
|
||||
char *window_title = strchr(buffer + 14, ',');
|
||||
if(!window_title)
|
||||
continue;
|
||||
|
||||
window_title[0] = '\0';
|
||||
window_title += 1;
|
||||
if(strcmp(window_id, "gsr-ui") == 0 || strcmp(window_id, "gsr-notify") == 0)
|
||||
continue;
|
||||
|
||||
print_window_title(window_title);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(sock_file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
return handle_ipc();
|
||||
}
|
||||
58
tools/gsr-kwin-helper/gsrkwinhelper.js
Normal file
58
tools/gsr-kwin-helper/gsrkwinhelper.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const DAEMON_DBUS_NAME = "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper";
|
||||
|
||||
// utils
|
||||
function sendNewActiveWindowTitle(title) {
|
||||
callDBus(
|
||||
DAEMON_DBUS_NAME, "/", DAEMON_DBUS_NAME,
|
||||
"setActiveWindowTitle", title
|
||||
);
|
||||
}
|
||||
|
||||
function sendNewActiveWindowFullscreen(isFullscreen) {
|
||||
callDBus(
|
||||
DAEMON_DBUS_NAME, "/", DAEMON_DBUS_NAME,
|
||||
"setActiveWindowFullscreen", isFullscreen
|
||||
);
|
||||
}
|
||||
|
||||
// track handlers to avoid duplicates
|
||||
const windowEventHandlers = new Map();
|
||||
|
||||
function subscribeToClient(client) {
|
||||
if (!client || windowEventHandlers.has(client)) return;
|
||||
|
||||
const emitActiveTitle = () => {
|
||||
if (workspace.activeWindow === client) {
|
||||
sendNewActiveWindowTitle(client.caption || "");
|
||||
}
|
||||
};
|
||||
|
||||
const emitActiveFullscreen = () => {
|
||||
if (workspace.activeWindow === client) {
|
||||
sendNewActiveWindowFullscreen(client.fullScreen);
|
||||
}
|
||||
};
|
||||
|
||||
windowEventHandlers.set(client, {
|
||||
title: emitActiveTitle,
|
||||
fs: emitActiveFullscreen,
|
||||
});
|
||||
|
||||
client.captionChanged.connect(emitActiveTitle);
|
||||
client.fullScreenChanged.connect(emitActiveFullscreen);
|
||||
}
|
||||
|
||||
function updateActiveWindow(client) {
|
||||
if (!client) return;
|
||||
sendNewActiveWindowTitle(client.caption || "");
|
||||
sendNewActiveWindowFullscreen(client.fullScreen);
|
||||
subscribeToClient(client);
|
||||
}
|
||||
|
||||
// handle window focus changes
|
||||
workspace.windowActivated.connect(updateActiveWindow);
|
||||
|
||||
// handle initial state
|
||||
if (workspace.activeWindow) {
|
||||
updateActiveWindow(workspace.activeWindow);
|
||||
}
|
||||
233
tools/gsr-kwin-helper/main.cpp
Normal file
233
tools/gsr-kwin-helper/main.cpp
Normal file
@@ -0,0 +1,233 @@
|
||||
#include <dbus/dbus.h>
|
||||
#include <unistd.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
static const char* INTROSPECTION_XML =
|
||||
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
|
||||
"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
|
||||
"<node>\n"
|
||||
" <interface name='com.dec05eba.gpu_screen_recorder.gsr_kwin_helper'>\n"
|
||||
" <method name='setActiveWindowTitle'>\n"
|
||||
" <arg type='s' name='title' direction='in'/>\n"
|
||||
" </method>\n"
|
||||
" </interface>\n"
|
||||
" <interface name='org.freedesktop.DBus.Introspectable'>\n"
|
||||
" <method name='Introspect'>\n"
|
||||
" <arg type='s' name='data' direction='out'/>\n"
|
||||
" </method>\n"
|
||||
" </interface>\n"
|
||||
"</node>\n";
|
||||
|
||||
|
||||
class GsrKwinHelper {
|
||||
public:
|
||||
std::string active_window_title;
|
||||
DBusConnection* connection = nullptr;
|
||||
DBusError err;
|
||||
|
||||
bool init() {
|
||||
dbus_error_init(&err);
|
||||
|
||||
connection = dbus_bus_get(DBUS_BUS_SESSION, &err);
|
||||
if (dbus_error_is_set(&err)) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to connect to session bus: " << err.message << "\n";
|
||||
dbus_error_free(&err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
std::cerr << "Error: gsr-kwin-helper: connection is null\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = dbus_bus_request_name(connection, "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper",
|
||||
DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
|
||||
if (dbus_error_is_set(&err)) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to request name: " << err.message << "\n";
|
||||
dbus_error_free(&err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
|
||||
std::cerr << "Error: gsr-kwin-helper: not primary owner of the name\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cerr << "Info: gsr-kwin-helper: DBus server initialized on com.dec05eba.gpu_screen_recorder.gsr_kwin_helper\n";
|
||||
|
||||
const bool inside_flatpak = access("/app/manifest.json", F_OK) == 0;
|
||||
|
||||
const char *helper_path =
|
||||
!inside_flatpak
|
||||
? KWIN_HELPER_SCRIPT_PATH
|
||||
: "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gsr-ui/gsrkwinhelper.js";
|
||||
|
||||
std::cerr << "Info: gsr-kwin-helper: KWin script path: " << helper_path << std::endl;
|
||||
|
||||
if (!load_kwin_script(connection, helper_path)) {
|
||||
std::cerr << "Warning: gsr-kwin-helper: failed to load KWin script\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void run() {
|
||||
while (true) {
|
||||
dbus_connection_read_write(connection, 100);
|
||||
DBusMessage* msg = dbus_connection_pop_message(connection);
|
||||
|
||||
if (!msg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Introspectable", "Introspect")) {
|
||||
handle_introspect(msg);
|
||||
} else if (dbus_message_is_method_call(msg, "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper", "setActiveWindowTitle")) {
|
||||
handle_set_title(msg);
|
||||
}
|
||||
|
||||
dbus_message_unref(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void handle_introspect(DBusMessage* msg) {
|
||||
DBusMessage* reply = dbus_message_new_method_return(msg);
|
||||
if (!reply) return;
|
||||
|
||||
DBusMessageIter args;
|
||||
dbus_message_iter_init_append(reply, &args);
|
||||
|
||||
if (!dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &INTROSPECTION_XML)) {
|
||||
dbus_message_unref(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
dbus_connection_send(connection, reply, nullptr);
|
||||
dbus_connection_flush(connection);
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
|
||||
void handle_set_title(DBusMessage* msg) {
|
||||
DBusMessageIter args;
|
||||
const char* title = nullptr;
|
||||
|
||||
if (!dbus_message_iter_init(msg, &args)) {
|
||||
send_error_reply(msg, "No arguments provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_STRING) {
|
||||
send_error_reply(msg, "Expected string argument");
|
||||
return;
|
||||
}
|
||||
|
||||
dbus_message_iter_get_basic(&args, &title);
|
||||
|
||||
if (title) {
|
||||
active_window_title = title;
|
||||
std::cout << "Active window title set to: " << active_window_title << "\n";
|
||||
std::cout.flush();
|
||||
send_success_reply(msg);
|
||||
} else {
|
||||
send_error_reply(msg, "Failed to read string");
|
||||
}
|
||||
}
|
||||
|
||||
void send_success_reply(DBusMessage* msg) {
|
||||
DBusMessage* reply = dbus_message_new_method_return(msg);
|
||||
if (reply) {
|
||||
dbus_connection_send(connection, reply, nullptr);
|
||||
dbus_connection_flush(connection);
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
}
|
||||
|
||||
void send_error_reply(DBusMessage* msg, const char* error_msg) {
|
||||
DBusMessage* reply = dbus_message_new_error(msg, "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper.Error", error_msg);
|
||||
if (reply) {
|
||||
dbus_connection_send(connection, reply, nullptr);
|
||||
dbus_connection_flush(connection);
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
}
|
||||
|
||||
bool call_kwin_method(DBusConnection* conn, const char* method,
|
||||
const char* arg1 = nullptr, const char* arg2 = nullptr) {
|
||||
DBusMessage* msg = dbus_message_new_method_call(
|
||||
"org.kde.KWin",
|
||||
"/Scripting",
|
||||
"org.kde.kwin.Scripting",
|
||||
method
|
||||
);
|
||||
|
||||
if (!msg) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to create message for " << method << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arg1) {
|
||||
dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_INVALID);
|
||||
if (arg2) {
|
||||
dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg2, DBUS_TYPE_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
DBusError err;
|
||||
dbus_error_init(&err);
|
||||
|
||||
// Send message and wait for reply (with 1 second timeout)
|
||||
DBusMessage* reply = dbus_connection_send_with_reply_and_block(conn, msg, 1000, &err);
|
||||
|
||||
dbus_message_unref(msg);
|
||||
|
||||
if (dbus_error_is_set(&err)) {
|
||||
std::cerr << "Error: gsr-kwin-helper: error calling " << method << ": " << err.message << "\n";
|
||||
dbus_error_free(&err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load_kwin_script(DBusConnection* conn, const char* script_path) {
|
||||
// Unload existing script
|
||||
call_kwin_method(conn, "unloadScript", "gsrkwinhelper");
|
||||
|
||||
if (!call_kwin_method(conn, "loadScript", script_path, "gsrkwinhelper")) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to load KWin script\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!call_kwin_method(conn, "start")) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to start KWin script\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cerr << "Info: gsr-kwin-helper: KWin script loaded and started successfully\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
~GsrKwinHelper() {
|
||||
if (connection) {
|
||||
dbus_bus_release_name(connection, "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper", nullptr);
|
||||
dbus_connection_unref(connection);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
GsrKwinHelper helper;
|
||||
|
||||
if (!helper.init()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
helper.run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -52,6 +52,10 @@ static void usage(void) {
|
||||
printf(" Start/stop recording.\n");
|
||||
printf(" toggle-pause\n");
|
||||
printf(" Pause/unpause recording. Only applies to regular recording.\n");
|
||||
printf(" toggle-record-region\n");
|
||||
printf(" Start/stop recording a region.\n");
|
||||
printf(" toggle-record-window\n");
|
||||
printf(" Start/stop recording a window (or desktop portal on Wayland).\n");
|
||||
printf(" toggle-stream\n");
|
||||
printf(" Start/stop streaming.\n");
|
||||
printf(" toggle-replay\n");
|
||||
@@ -80,6 +84,8 @@ static bool is_valid_command(const char *command) {
|
||||
"toggle-show",
|
||||
"toggle-record",
|
||||
"toggle-pause",
|
||||
"toggle-record-region",
|
||||
"toggle-record-window",
|
||||
"toggle-stream",
|
||||
"toggle-replay",
|
||||
"replay-save",
|
||||
|
||||
Reference in New Issue
Block a user