Add webcam support

This commit is contained in:
dec05eba
2025-12-25 08:19:07 +01:00
parent 1971d4a288
commit 13562d2aa1
19 changed files with 531 additions and 26 deletions

2
TODO
View File

@@ -253,3 +253,5 @@ Sometimes when opening gpu screen recorder ui gsr-global-hotkeys incorrectly det
When running replay for a long time and then stopping it it takes a while. Improve this.
When adding webcamera make replay auto start wait for camera to be available (when /dev/video device exists and can be initialized), just like audio device.
Make it possible to resize webcam box from top left, top right and bottom left as well.

View File

@@ -61,6 +61,14 @@ namespace gsr {
bool record_cursor = true;
bool restore_portal_session = true;
std::string webcam_source = "";
bool webcam_flip_horizontally = false;
std::string webcam_video_format = "auto";
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;
};

View File

@@ -25,11 +25,22 @@ namespace gsr {
bool png = false;
};
struct SupportedCameraPixelFormats {
bool yuyv = false;
bool mjpeg = false;
};
struct GsrMonitor {
std::string name;
mgl::vec2i size;
};
struct GsrCamera {
std::string path;
mgl::vec2i size;
SupportedCameraPixelFormats supported_pixel_formats;
};
struct GsrVersion {
uint8_t major = 0;
uint8_t minor = 0;
@@ -51,6 +62,7 @@ namespace gsr {
bool focused = false;
bool portal = false;
std::vector<GsrMonitor> monitors;
std::vector<GsrCamera> cameras;
};
enum class DisplayServer {
@@ -103,4 +115,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();
}

View File

@@ -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;

View File

@@ -19,6 +19,8 @@ namespace gsr {
void draw(mgl::Window &window, mgl::vec2f offset) override;
void add_item(const std::string &text, const std::string &id);
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);

View File

@@ -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,6 +66,9 @@ 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_format();
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);
@@ -140,6 +151,8 @@ namespace gsr {
void save_stream();
void view_changed(bool advanced_view);
RecordOptions& get_current_record_options();
private:
Type type;
Config &config;
@@ -197,7 +210,29 @@ 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_format_box_ptr = nullptr;
List *webcam_body_list_ptr = nullptr;
CheckBox *flip_camera_horizontally_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;
};
}

View File

@@ -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();

View File

@@ -199,6 +199,13 @@ 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.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_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},
@@ -231,6 +238,13 @@ 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.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_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},
@@ -260,6 +274,13 @@ 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.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_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},

View File

@@ -288,6 +288,56 @@ 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 SupportedCameraPixelFormats parse_supported_camera_pixel_formats(std::string_view line) {
SupportedCameraPixelFormats result;
string_split_char(line, ',', [&](std::string_view column) {
if(column == "yuyv")
result.yuyv = true;
else if(column == "mjpeg")
result.mjpeg = true;
return true;
});
return result;
}
static std::optional<GsrCamera> capture_option_line_to_camera(std::string_view line) {
std::optional<GsrCamera> camera;
const std::optional<KeyValue3> key_value3 = parse_3(line);
if(!key_value3)
return camera;
mgl::vec2i size;
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", &size.x, &size.y) != 2)
return camera;
camera = GsrCamera{std::string(key_value3->value1), size, parse_supported_camera_pixel_formats(key_value3->value3)};
return camera;
}
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);
@@ -305,15 +355,19 @@ namespace gsr {
}
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] == '/') {
std::optional<GsrCamera> camera = capture_option_line_to_camera(line);
if(camera)
capture_options.cameras.push_back(std::move(camera.value()));
} else {
std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line);
if(monitor)
capture_options.monitors.push_back(std::move(monitor.value()));
@@ -348,4 +402,24 @@ 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) {
std::optional<GsrCamera> camera = capture_option_line_to_camera(line);
if(camera)
cameras.push_back(std::move(camera.value()));
return true;
});
return cameras;
}
}

View File

@@ -2220,6 +2220,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;
@@ -2245,7 +2257,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);
@@ -2262,7 +2274,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);
@@ -2274,7 +2286,7 @@ 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);
}
@@ -2671,6 +2683,66 @@ 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;
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;
@@ -2760,8 +2832,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",
@@ -2982,8 +3056,10 @@ namespace gsr {
if(config.record_config.record_options.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",
@@ -3170,8 +3246,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",
@@ -3301,7 +3379,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) {

View File

@@ -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;
}

View File

@@ -90,6 +90,13 @@ namespace gsr {
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];

View File

@@ -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() {

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -4,11 +4,16 @@
#include "../../include/gui/PageStack.hpp"
#include "../../include/gui/FileChooser.hpp"
#include "../../include/gui/Subsection.hpp"
#include "../../include/gui/Image.hpp"
#include "../../include/gui/CustomRendererWidget.hpp"
#include "../../include/gui/Utils.hpp"
#include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
#include "mglpp/window/Window.hpp"
#include "mglpp/window/Event.hpp"
#include <algorithm>
#include <cmath>
#include <string.h>
namespace gsr {
@@ -87,7 +92,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 +192,214 @@ 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;
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->supported_pixel_formats.yuyv)
webcam_video_format_box_ptr->add_item("YUYV", "yuyv");
if(it->supported_pixel_formats.mjpeg)
webcam_video_format_box_ptr->add_item("Motion-JPEG", "mjpeg");
webcam_video_format_box_ptr->set_selected_item(get_current_record_options().webcam_video_format);
selected_camera = *it;
};
ll->add_widget(std::move(combobox));
return ll;
}
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;
};
ll->add_widget(std::move(combobox));
return ll;
}
std::unique_ptr<Widget> SettingsPage::create_webcam_section() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
ll->add_widget(create_webcam_sources());
auto body_list = std::make_unique<List>(List::Orientation::VERTICAL);
body_list->set_visible(false);
webcam_body_list_ptr = body_list.get();
{
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())
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->size.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->size.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->size.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->size.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;
};
body_list->add_widget(std::move(camera_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));
{
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();
body_list->add_widget(std::move(flip_camera_horizontally_checkbox));
}
body_list->add_widget(create_webcam_video_format());
ll->add_widget(std::move(body_list));
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");
}
@@ -618,6 +831,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();
@@ -830,6 +1044,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);
@@ -1200,6 +1424,17 @@ namespace gsr {
show_notification_checkbox_ptr->set_checked(record_options.show_notifications);
led_indicator_checkbox_ptr->set_checked(record_options.use_led_indicator);
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_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.has_value())
webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
if(record_options.record_area_width == 0)
record_options.record_area_width = 1920;
@@ -1332,6 +1567,17 @@ namespace gsr {
record_options.show_notifications = show_notification_checkbox_ptr->is_checked();
record_options.use_led_indicator = led_indicator_checkbox_ptr->is_checked();
if(selected_camera.has_value())
webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
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_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;

View File

@@ -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))
};
}
}

View File

@@ -195,11 +195,14 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
}
if(extra_data->gsr_ui_virtual_keyboard) {
if(event.type == EV_KEY || event.type == EV_MSC)
if(event.type == EV_KEY)
self->check_grab_lock = false;
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) {
self->uinput_written_time_seconds = clock_get_monotonic_seconds();
self->check_grab_lock = true;
}