提交 ddfe6483 编写于 作者: P Palana

UI: Add OBS_PROPERTY_FRAME_RATE implementation

上级 1b078f57
......@@ -155,6 +155,7 @@ set(obs_HEADERS
window-projector.hpp
window-remux.hpp
properties-view.hpp
properties-view.moc.hpp
display-helpers.hpp
double-slider.hpp
focus-list.hpp
......
......@@ -173,6 +173,11 @@ Basic.PropertiesWindow.AddEditableListFiles="Add files to '%1'"
Basic.PropertiesWindow.AddEditableListEntry="Add entry to '%1'"
Basic.PropertiesWindow.EditEditableListEntry="Edit entry from '%1'"
# properties view
Basic.PropertiesView.FPS.Simple="Simple FPS Values"
Basic.PropertiesView.FPS.Rational="Rational FPS Values"
Basic.PropertiesView.FPS.ValidFPSRanges="Valid FPS Ranges:"
# interaction window
Basic.InteractionWindow="Interacting with '%1'"
......
......@@ -17,10 +17,15 @@
#include <QPlainTextEdit>
#include <QDialogButtonBox>
#include <QMenu>
#include <QStackedWidget>
#include "double-slider.hpp"
#include "qt-wrappers.hpp"
#include "properties-view.hpp"
#include "properties-view.moc.hpp"
#include "obs-app.hpp"
#include <cstdlib>
#include <initializer_list>
#include <string>
using namespace std;
......@@ -46,6 +51,41 @@ static inline long long color_to_int(QColor color)
shift(color.alpha(), 24);
}
namespace {
struct frame_rate_tag {
enum tag_type {
SIMPLE,
RATIONAL,
USER,
} type = SIMPLE;
const char *val = nullptr;
frame_rate_tag() = default;
explicit frame_rate_tag(tag_type type)
: type(type)
{}
explicit frame_rate_tag(const char *val)
: type(USER),
val(val)
{}
static frame_rate_tag simple() { return frame_rate_tag{SIMPLE}; }
static frame_rate_tag rational() { return frame_rate_tag{RATIONAL}; }
};
struct common_frame_rate {
const char *fps_name;
media_frames_per_second fps;
};
}
Q_DECLARE_METATYPE(frame_rate_tag);
Q_DECLARE_METATYPE(media_frames_per_second);
void OBSPropertiesView::ReloadProperties()
{
if (obj) {
......@@ -634,6 +674,589 @@ void OBSPropertiesView::AddFont(obs_property_t *prop, QFormLayout *layout,
obs_data_release(font_obj);
}
namespace std {
template <>
struct default_delete<obs_data_t> {
void operator()(obs_data_t *data)
{
obs_data_release(data);
}
};
template <>
struct default_delete<obs_data_item_t> {
void operator()(obs_data_item_t *item)
{
obs_data_item_release(&item);
}
};
}
template <typename T>
static double make_epsilon(T val)
{
return val * 0.00001;
}
static bool matches_range(media_frames_per_second &match,
media_frames_per_second fps,
const frame_rate_range_t &pair)
{
auto val = media_frames_per_second_to_frame_interval(fps);
auto max_ = media_frames_per_second_to_frame_interval(pair.first);
auto min_ = media_frames_per_second_to_frame_interval(pair.second);
if (min_ <= val && val <= max_) {
match = fps;
return true;
}
return false;
}
static bool matches_ranges(media_frames_per_second &best_match,
media_frames_per_second fps,
const frame_rate_ranges_t &fps_ranges, bool exact=false)
{
auto convert_fn = media_frames_per_second_to_frame_interval;
auto val = convert_fn(fps);
auto epsilon = make_epsilon(val);
bool match = false;
auto best_dist = numeric_limits<double>::max();
for (auto &pair : fps_ranges) {
auto max_ = convert_fn(pair.first);
auto min_ = convert_fn(pair.second);
/*blog(LOG_INFO, "%lg ≤ %lg ≤ %lg? %s %s %s",
min_, val, max_,
fabsl(min_ - val) < epsilon ? "true" : "false",
min_ <= val && val <= max_ ? "true" : "false",
fabsl(min_ - val) < epsilon ? "true" :
"false");*/
if (matches_range(best_match, fps, pair))
return true;
if (exact)
continue;
auto min_dist = fabsl(min_ - val);
auto max_dist = fabsl(max_ - val);
if (min_dist < epsilon && min_dist < best_dist) {
best_match = pair.first;
match = true;
continue;
}
if (max_dist < epsilon && max_dist < best_dist) {
best_match = pair.second;
match = true;
continue;
}
}
return match;
}
static media_frames_per_second make_fps(uint32_t num, uint32_t den)
{
media_frames_per_second fps{};
fps.numerator = num;
fps.denominator = den;
return fps;
}
static const common_frame_rate common_fps[] = {
{"60", {60, 1}},
{"59.94", {60000, 1001}},
{"50", {50, 1}},
{"48", {48, 1}},
{"30", {30, 1}},
{"29.97", {30000, 1001}},
{"25", {25, 1}},
{"24", {24, 1}},
{"23.976", {24000, 1001}},
};
static void UpdateSimpleFPSSelection(OBSFrameRatePropertyWidget *fpsProps,
const media_frames_per_second *current_fps)
{
if (!current_fps || !media_frames_per_second_is_valid(*current_fps)) {
fpsProps->simpleFPS->setCurrentIndex(0);
return;
}
auto combo = fpsProps->simpleFPS;
auto num = combo->count();
for (int i = 0; i < num; i++) {
auto variant = combo->itemData(i);
if (!variant.canConvert<media_frames_per_second>())
continue;
auto fps = variant.value<media_frames_per_second>();
if (fps != *current_fps)
continue;
combo->setCurrentIndex(i);
return;
}
combo->setCurrentIndex(0);
}
static void AddFPSRanges(vector<common_frame_rate> &items,
const frame_rate_ranges_t &ranges)
{
auto InsertFPS = [&](media_frames_per_second fps)
{
auto fps_val = media_frames_per_second_to_fps(fps);
auto end_ = end(items);
auto i = begin(items);
for (; i != end_; i++) {
auto i_fps_val = media_frames_per_second_to_fps(i->fps);
if (fabsl(i_fps_val - fps_val) < 0.01)
return;
if (i_fps_val > fps_val)
continue;
break;
}
items.insert(i, {nullptr, fps});
};
for (auto &range : ranges) {
InsertFPS(range.first);
InsertFPS(range.second);
}
}
static QWidget *CreateSimpleFPSValues(OBSFrameRatePropertyWidget *fpsProps,
bool &selected, const media_frames_per_second *current_fps)
{
auto widget = new QWidget{};
widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
auto layout = new QVBoxLayout{};
layout->setContentsMargins(0, 0, 0, 0);
auto items = vector<common_frame_rate>{};
items.reserve(sizeof(common_fps)/sizeof(common_frame_rate));
auto combo = fpsProps->simpleFPS = new QComboBox{};
combo->addItem("", QVariant::fromValue(make_fps(0, 0)));
for (const auto &fps : common_fps) {
media_frames_per_second best_match{};
if (!matches_ranges(best_match, fps.fps, fpsProps->fps_ranges))
continue;
items.push_back({fps.fps_name, best_match});
}
AddFPSRanges(items, fpsProps->fps_ranges);
for (const auto &item : items) {
auto var = QVariant::fromValue(item.fps);
auto name = item.fps_name ?
QString(item.fps_name) :
QString("%1")
.arg(media_frames_per_second_to_fps(item.fps));
combo->addItem(name, var);
bool select = current_fps && *current_fps == item.fps;
if (select) {
combo->setCurrentIndex(combo->count() - 1);
selected = true;
}
}
layout->addWidget(combo, 0, Qt::AlignTop);
widget->setLayout(layout);
return widget;
}
static void UpdateRationalFPSWidgets(OBSFrameRatePropertyWidget *fpsProps,
const media_frames_per_second *current_fps)
{
if (!current_fps || !media_frames_per_second_is_valid(*current_fps)) {
fpsProps->numEdit->setValue(0);
fpsProps->denEdit->setValue(0);
return;
}
auto combo = fpsProps->fpsRange;
auto num = combo->count();
for (int i = 0; i < num; i++) {
auto variant = combo->itemData(i);
if (!variant.canConvert<size_t>())
continue;
auto idx = variant.value<size_t>();
if (fpsProps->fps_ranges.size() < idx)
continue;
media_frames_per_second match{};
if (!matches_range(match, *current_fps,
fpsProps->fps_ranges[idx]))
continue;
combo->setCurrentIndex(i);
break;
}
fpsProps->numEdit->setValue(current_fps->numerator);
fpsProps->denEdit->setValue(current_fps->denominator);
}
static QWidget *CreateRationalFPS(OBSFrameRatePropertyWidget *fpsProps,
bool &selected, const media_frames_per_second *current_fps)
{
auto widget = new QWidget{};
widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
auto layout = new QFormLayout{};
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(4);
auto str = QTStr("Basic.PropertiesView.FPS.ValidFPSRanges");
auto rlabel = new QLabel{str};
auto combo = fpsProps->fpsRange = new QComboBox{};
auto convert_fps = media_frames_per_second_to_fps;
//auto convert_fi = media_frames_per_second_to_frame_interval;
for (size_t i = 0; i < fpsProps->fps_ranges.size(); i++) {
auto &pair = fpsProps->fps_ranges[i];
combo->addItem(QString{"%1 - %2"}
.arg(convert_fps(pair.first))
.arg(convert_fps(pair.second)),
QVariant::fromValue(i));
media_frames_per_second match;
if (!current_fps || !matches_range(match, *current_fps, pair))
continue;
combo->setCurrentIndex(combo->count() - 1);
selected = true;
}
layout->addRow(rlabel, combo);
auto num_edit = fpsProps->numEdit = new QSpinBox{};
auto den_edit = fpsProps->denEdit = new QSpinBox{};
num_edit->setRange(0, INT_MAX);
den_edit->setRange(0, INT_MAX);
if (current_fps) {
num_edit->setValue(current_fps->numerator);
den_edit->setValue(current_fps->denominator);
}
layout->addRow(QTStr("Basic.Settings.Video.Numerator"), num_edit);
layout->addRow(QTStr("Basic.Settings.Video.Denominator"), den_edit);
widget->setLayout(layout);
return widget;
}
static OBSFrameRatePropertyWidget *CreateFrameRateWidget(obs_property_t *prop,
bool &warning, const char *option,
media_frames_per_second *current_fps,
frame_rate_ranges_t &fps_ranges)
{
auto widget = new OBSFrameRatePropertyWidget{};
auto hlayout = new QHBoxLayout{};
hlayout->setContentsMargins(0, 0, 0, 0);
swap(widget->fps_ranges, fps_ranges);
auto combo = widget->modeSelect = new QComboBox{};
combo->addItem(QTStr("Basic.PropertiesView.FPS.Simple"),
QVariant::fromValue(frame_rate_tag::simple()));
combo->addItem(QTStr("Basic.PropertiesView.FPS.Rational"),
QVariant::fromValue(frame_rate_tag::rational()));
auto num = obs_property_frame_rate_options_count(prop);
if (num)
combo->insertSeparator(combo->count());
bool option_found = false;
for (size_t i = 0; i < num; i++) {
auto name = obs_property_frame_rate_option_name(prop, i);
auto desc = obs_property_frame_rate_option_description(prop, i);
combo->addItem(desc, QVariant::fromValue(frame_rate_tag{name}));
if (!name || !option || string(name) != option)
continue;
option_found = true;
combo->setCurrentIndex(combo->count() - 1);
}
hlayout->addWidget(combo, 0, Qt::AlignTop);
auto stack = widget->modeDisplay = new QStackedWidget{};
bool match_found = option_found;
auto AddWidget = [&](decltype(CreateRationalFPS) func)
{
bool selected = false;
stack->addWidget(func(widget, selected, current_fps));
if (match_found || !selected)
return;
match_found = true;
stack->setCurrentIndex(stack->count() - 1);
combo->setCurrentIndex(stack->count() - 1);
};
AddWidget(CreateSimpleFPSValues);
AddWidget(CreateRationalFPS);
stack->addWidget(new QWidget{});
if (option_found)
stack->setCurrentIndex(stack->count() - 1);
else if (!match_found) {
int idx = current_fps ? 1 : 0; // Rational for "unsupported"
// Simple as default
stack->setCurrentIndex(idx);
combo->setCurrentIndex(idx);
warning = true;
}
hlayout->addWidget(stack, 0, Qt::AlignTop);
auto label_area = widget->labels = new QWidget{};
label_area->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
auto vlayout = new QVBoxLayout{};
vlayout->setContentsMargins(0, 0, 0, 0);
auto fps_label = widget->currentFPS = new QLabel{"FPS: 22"};
auto time_label = widget->timePerFrame =
new QLabel{"Frame Interval: 0.123 ms"};
auto min_label = widget->minLabel = new QLabel{"Min FPS: 1/1"};
auto max_label = widget->maxLabel = new QLabel{"Max FPS: 2/1"};
min_label->setHidden(true);
max_label->setHidden(true);
auto flags = Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard;
min_label->setTextInteractionFlags(flags);
max_label->setTextInteractionFlags(flags);
vlayout->addWidget(fps_label);
vlayout->addWidget(time_label);
vlayout->addWidget(min_label);
vlayout->addWidget(max_label);
label_area->setLayout(vlayout);
hlayout->addWidget(label_area, 0, Qt::AlignTop);
widget->setLayout(hlayout);
return widget;
}
static void UpdateMinMaxLabels(OBSFrameRatePropertyWidget *w)
{
auto Hide = [&](bool hide)
{
w->minLabel->setHidden(hide);
w->maxLabel->setHidden(hide);
};
auto variant = w->modeSelect->currentData();
if (!variant.canConvert<frame_rate_tag>() ||
variant.value<frame_rate_tag>().type !=
frame_rate_tag::RATIONAL) {
Hide(true);
return;
}
variant = w->fpsRange->currentData();
if (!variant.canConvert<size_t>()) {
Hide(true);
return;
}
auto idx = variant.value<size_t>();
if (idx >= w->fps_ranges.size()) {
Hide(true);
return;
}
Hide(false);
auto min = w->fps_ranges[idx].first;
auto max = w->fps_ranges[idx].second;
w->minLabel->setText(QString("Min FPS: %1/%2")
.arg(min.numerator)
.arg(min.denominator));
w->maxLabel->setText(QString("Max FPS: %1/%2")
.arg(max.numerator)
.arg(max.denominator));
}
static void UpdateFPSLabels(OBSFrameRatePropertyWidget *w)
{
UpdateMinMaxLabels(w);
unique_ptr<obs_data_item_t> obj{
obs_data_item_byname(w->settings, w->name)};
media_frames_per_second fps{};
media_frames_per_second *valid_fps = nullptr;
if (obs_data_item_get_autoselect_frames_per_second(obj.get(), &fps,
nullptr) ||
obs_data_item_get_frames_per_second(obj.get(), &fps,
nullptr))
valid_fps = &fps;
const char *option = nullptr;
obs_data_item_get_frames_per_second(obj.get(), nullptr, &option);
if (!valid_fps) {
w->currentFPS->setHidden(true);
w->timePerFrame->setHidden(true);
if (!option)
w->warningLabel->setStyleSheet(
"QLabel { color: red; }");
return;
}
w->currentFPS->setHidden(false);
w->timePerFrame->setHidden(false);
media_frames_per_second match{};
if (!option && !matches_ranges(match, *valid_fps, w->fps_ranges, true))
w->warningLabel->setStyleSheet("QLabel { color: red; }");
else
w->warningLabel->setStyleSheet("");
auto convert_to_fps = media_frames_per_second_to_fps;
auto convert_to_frame_interval =
media_frames_per_second_to_frame_interval;
w->currentFPS->setText(QString("FPS: %1")
.arg(convert_to_fps(*valid_fps)));
w->timePerFrame->setText(QString("Frame Interval: %1 ms")
.arg(convert_to_frame_interval(*valid_fps) * 1000));
}
void OBSPropertiesView::AddFrameRate(obs_property_t *prop, bool &warning,
QFormLayout *layout, QLabel *&label)
{
const char *name = obs_property_name(prop);
bool enabled = obs_property_enabled(prop);
unique_ptr<obs_data_item_t> obj{obs_data_item_byname(settings, name)};
const char *option = nullptr;
obs_data_item_get_frames_per_second(obj.get(), nullptr, &option);
media_frames_per_second fps{};
media_frames_per_second *valid_fps = nullptr;
if (obs_data_item_get_frames_per_second(obj.get(), &fps, nullptr))
valid_fps = &fps;
frame_rate_ranges_t fps_ranges;
size_t num = obs_property_frame_rate_fps_ranges_count(prop);
fps_ranges.reserve(num);
for (size_t i = 0; i < num; i++)
fps_ranges.emplace_back(
obs_property_frame_rate_fps_range_min(prop, i),
obs_property_frame_rate_fps_range_max(prop, i));
auto widget = CreateFrameRateWidget(prop, warning, option, valid_fps,
fps_ranges);
auto info = new WidgetInfo(this, prop, widget);
widget->name = name;
widget->settings = settings;
widget->modeSelect->setEnabled(enabled);
widget->simpleFPS->setEnabled(enabled);
widget->fpsRange->setEnabled(enabled);
widget->numEdit->setEnabled(enabled);
widget->denEdit->setEnabled(enabled);
label = widget->warningLabel =
new QLabel{obs_property_description(prop)};
layout->addRow(label, widget);
children.emplace_back(info);
UpdateFPSLabels(widget);
auto stack = widget->modeDisplay;
auto combo = widget->modeSelect;
auto comboIndexChanged = static_cast<void (QComboBox::*)(int)>(
&QComboBox::currentIndexChanged);
connect(combo, comboIndexChanged, stack,
[=](int index)
{
bool out_of_bounds = index >= stack->count();
auto idx = out_of_bounds ? stack->count() - 1 : index;
stack->setCurrentIndex(idx);
if (widget->updating)
return;
UpdateFPSLabels(widget);
emit info->ControlChanged();
});
connect(widget->simpleFPS, comboIndexChanged, [=](int)
{
if (widget->updating)
return;
emit info->ControlChanged();
});
connect(widget->fpsRange, comboIndexChanged, [=](int)
{
if (widget->updating)
return;
UpdateFPSLabels(widget);
});
auto sbValueChanged = static_cast<void (QSpinBox::*)(int)>(
&QSpinBox::valueChanged);
connect(widget->numEdit, sbValueChanged, [=](int)
{
if (widget->updating)
return;
emit info->ControlChanged();
});
connect(widget->denEdit, sbValueChanged, [=](int)
{
if (widget->updating)
return;
emit info->ControlChanged();
});
}
void OBSPropertiesView::AddProperty(obs_property_t *property,
QFormLayout *layout)
{
......@@ -680,6 +1303,9 @@ void OBSPropertiesView::AddProperty(obs_property_t *property,
case OBS_PROPERTY_EDITABLE_LIST:
AddEditableList(property, layout, label);
break;
case OBS_PROPERTY_FRAME_RATE:
AddFrameRate(property, warning, layout, label);
break;
}
if (widget && !obs_property_enabled(property))
......@@ -713,6 +1339,112 @@ void OBSPropertiesView::SignalChanged()
emit Changed();
}
static bool FrameRateChangedVariant(const QVariant &variant,
media_frames_per_second &fps, obs_data_item_t *&obj,
const media_frames_per_second *valid_fps)
{
if (!variant.canConvert<media_frames_per_second>())
return false;
fps = variant.value<media_frames_per_second>();
if (valid_fps && fps == *valid_fps)
return false;
obs_data_item_set_frames_per_second(&obj, fps, nullptr);
return true;
}
static bool FrameRateChangedCommon(OBSFrameRatePropertyWidget *w,
obs_data_item_t *&obj, const media_frames_per_second *valid_fps)
{
media_frames_per_second fps{};
if (!FrameRateChangedVariant(w->simpleFPS->currentData(), fps, obj,
valid_fps))
return false;
UpdateRationalFPSWidgets(w, &fps);
return true;
}
static bool FrameRateChangedRational(OBSFrameRatePropertyWidget *w,
obs_data_item_t *&obj, const media_frames_per_second *valid_fps)
{
auto num = w->numEdit->value();
auto den = w->denEdit->value();
auto fps = make_fps(num, den);
if (valid_fps && media_frames_per_second_is_valid(fps) &&
fps == *valid_fps)
return false;
obs_data_item_set_frames_per_second(&obj, fps, nullptr);
UpdateSimpleFPSSelection(w, &fps);
return true;
}
static bool FrameRateChanged(QWidget *widget, const char *name,
OBSData &settings)
{
auto w = qobject_cast<OBSFrameRatePropertyWidget*>(widget);
if (!w)
return false;
auto variant = w->modeSelect->currentData();
if (!variant.canConvert<frame_rate_tag>())
return false;
auto StopUpdating = [&](void*)
{
w->updating = false;
};
unique_ptr<void, decltype(StopUpdating)> signalGuard(
static_cast<void*>(w), StopUpdating);
w->updating = true;
if (!obs_data_has_user_value(settings, name))
obs_data_set_obj(settings, name, nullptr);
unique_ptr<obs_data_item_t> obj{obs_data_item_byname(settings, name)};
auto obj_ptr = obj.get();
auto CheckObj = [&]()
{
if (!obj_ptr)
obj.release();
};
const char *option = nullptr;
obs_data_item_get_frames_per_second(obj.get(), nullptr, &option);
media_frames_per_second fps{};
media_frames_per_second *valid_fps = nullptr;
if (obs_data_item_get_frames_per_second(obj.get(), &fps, nullptr))
valid_fps = &fps;
auto tag = variant.value<frame_rate_tag>();
switch (tag.type) {
case frame_rate_tag::SIMPLE:
if (!FrameRateChangedCommon(w, obj_ptr, valid_fps))
return false;
break;
case frame_rate_tag::RATIONAL:
if (!FrameRateChangedRational(w, obj_ptr, valid_fps))
return false;
break;
case frame_rate_tag::USER:
if (tag.val && option && strcmp(tag.val, option) == 0)
return false;
obs_data_item_set_frames_per_second(&obj_ptr, {}, tag.val);
break;
}
UpdateFPSLabels(w);
CheckObj();
return true;
}
void WidgetInfo::BoolChanged(const char *setting)
{
QCheckBox *checkbox = static_cast<QCheckBox*>(widget);
......@@ -938,6 +1670,10 @@ void WidgetInfo::ControlChanged()
return;
break;
case OBS_PROPERTY_EDITABLE_LIST: return;
case OBS_PROPERTY_FRAME_RATE:
if (!FrameRateChanged(widget, setting, view->settings))
return;
break;
}
if (view->callback && !view->deferUpdate)
......
......@@ -99,6 +99,8 @@ private:
QWidget *AddButton(obs_property_t *prop);
void AddColor(obs_property_t *prop, QFormLayout *layout, QLabel *&label);
void AddFont(obs_property_t *prop, QFormLayout *layout, QLabel *&label);
void AddFrameRate(obs_property_t *prop, bool &warning,
QFormLayout *layout, QLabel *&label);
void AddProperty(obs_property_t *property, QFormLayout *layout);
......
#pragma once
#include <QComboBox>
#include <QLabel>
#include <QSpinBox>
#include <QStackedWidget>
#include <QWidget>
#include <media-io/frame-rate.h>
#include <vector>
static bool operator!=(const media_frames_per_second &a,
const media_frames_per_second &b)
{
return a.numerator != b.numerator || a.denominator != b.denominator;
}
static bool operator==(const media_frames_per_second &a,
const media_frames_per_second &b)
{
return !(a != b);
}
using frame_rate_range_t =
std::pair<media_frames_per_second, media_frames_per_second>;
using frame_rate_ranges_t = std::vector<frame_rate_range_t>;
class OBSFrameRatePropertyWidget : public QWidget {
Q_OBJECT
public:
frame_rate_ranges_t fps_ranges;
QComboBox *modeSelect = nullptr;
QStackedWidget *modeDisplay = nullptr;
QWidget *labels = nullptr;
QLabel *currentFPS = nullptr;
QLabel *timePerFrame = nullptr;
QLabel *minLabel = nullptr;
QLabel *maxLabel = nullptr;
QComboBox *simpleFPS = nullptr;
QComboBox *fpsRange = nullptr;
QSpinBox *numEdit = nullptr;
QSpinBox *denEdit = nullptr;
bool updating = false;
const char *name = nullptr;
obs_data_t *settings = nullptr;
QLabel *warningLabel = nullptr;
OBSFrameRatePropertyWidget() = default;
};
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册