diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index 93994c4ad..3f4a6b223 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -712,6 +712,8 @@ Basic.Settings.Stream.IgnoreRecommended.Warn.Title="Override Recommended Setting
Basic.Settings.Stream.IgnoreRecommended.Warn.Text="Warning: Ignoring the service's limitations may result in degraded stream quality or prevent you from streaming.\n\nContinue?"
Basic.Settings.Stream.Recommended.MaxVideoBitrate="Maximum Video Bitrate: %1 kbps"
Basic.Settings.Stream.Recommended.MaxAudioBitrate="Maximum Audio Bitrate: %1 kbps"
+Basic.Settings.Stream.Recommended.MaxResolution="Maximum Resolution: %1"
+Basic.Settings.Stream.Recommended.MaxFPS="Maximum FPS: %1"
# basic mode 'output' settings
Basic.Settings.Output="Output"
@@ -751,6 +753,10 @@ Basic.Settings.Output.Simple.Encoder.Hardware.QSV="Hardware (QSV)"
Basic.Settings.Output.Simple.Encoder.Hardware.AMD="Hardware (AMD)"
Basic.Settings.Output.Simple.Encoder.Hardware.NVENC="Hardware (NVENC)"
Basic.Settings.Output.Simple.Encoder.SoftwareLowCPU="Software (x264 low CPU usage preset, increases file size)"
+Basic.Settings.Output.Warn.EnforceResolutionFPS.Title="Incompatible Resolution/Framerate"
+Basic.Settings.Output.Warn.EnforceResolutionFPS.Msg="This streaming service does not support your current output resolution and/or framerate. They will be changed to the closest compatible value:\n\n%1\n\nDo you want to continue?"
+Basic.Settings.Output.Warn.EnforceResolutionFPS.Resolution="Resolution: %1"
+Basic.Settings.Output.Warn.EnforceResolutionFPS.FPS="FPS: %1"
Basic.Settings.Output.VideoBitrate="Video Bitrate"
Basic.Settings.Output.AudioBitrate="Audio Bitrate"
Basic.Settings.Output.Reconnect="Automatically Reconnect"
diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui
index 8aa0be24b..d48cd69f9 100644
--- a/UI/forms/OBSBasicSettings.ui
+++ b/UI/forms/OBSBasicSettings.ui
@@ -4304,7 +4304,7 @@
-
-
+
Basic.Settings.Video.ScaledResolution
@@ -4552,7 +4552,7 @@
-
-
+
6
@@ -4592,8 +4592,8 @@
0
0
- 98
- 28
+ 818
+ 675
@@ -4639,7 +4639,7 @@
0
0
- 599
+ 803
781
diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp
index 870f946e5..90de78b82 100644
--- a/UI/window-basic-settings-stream.cpp
+++ b/UI/window-basic-settings-stream.cpp
@@ -75,10 +75,14 @@ void OBSBasicSettings::InitStreamPage()
SLOT(UpdateVodTrackSetting()));
connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
SLOT(UpdateServiceRecommendations()));
+ connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(UpdateResFPSLimits()));
connect(ui->customServer, SIGNAL(textChanged(const QString &)), this,
SLOT(UpdateKeyLink()));
connect(ui->ignoreRecommended, SIGNAL(clicked(bool)), this,
SLOT(DisplayEnforceWarning(bool)));
+ connect(ui->ignoreRecommended, SIGNAL(toggled(bool)), this,
+ SLOT(UpdateResFPSLimits()));
connect(ui->customServer, SIGNAL(editingFinished(const QString &)),
this, SLOT(UpdateKeyLink()));
connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
@@ -159,6 +163,9 @@ void OBSBasicSettings::LoadStream1Settings()
ui->ignoreRecommended->setChecked(ignoreRecommended);
loading = false;
+
+ QMetaObject::invokeMethod(this, "UpdateResFPSLimits",
+ Qt::QueuedConnection);
}
void OBSBasicSettings::SaveStream1Settings()
@@ -725,3 +732,263 @@ void OBSBasicSettings::DisplayEnforceWarning(bool checked)
SimpleRecordingEncoderChanged();
}
+
+bool OBSBasicSettings::ResFPSValid(obs_service_resolution *res_list,
+ size_t res_count, int max_fps)
+{
+ if (!res_count && !max_fps)
+ return true;
+
+ if (res_count) {
+ QString res = ui->outputResolution->currentText();
+ bool found_res = false;
+
+ int cx, cy;
+ if (sscanf(QT_TO_UTF8(res), "%dx%d", &cx, &cy) != 2)
+ return false;
+
+ for (size_t i = 0; i < res_count; i++) {
+ if (res_list[i].cx == cx && res_list[i].cy == cy) {
+ found_res = true;
+ break;
+ }
+ }
+
+ if (!found_res)
+ return false;
+ }
+
+ if (max_fps) {
+ int fpsType = ui->fpsType->currentIndex();
+ if (fpsType != 0)
+ return false;
+
+ std::string fps_str = QT_TO_UTF8(ui->fpsCommon->currentText());
+ float fps;
+ sscanf(fps_str.c_str(), "%f", &fps);
+ if (fps > (float)max_fps)
+ return false;
+ }
+
+ return true;
+}
+
+extern void set_closest_res(int &cx, int &cy,
+ struct obs_service_resolution *res_list,
+ size_t count);
+
+/* Checks for and updates the resolution and FPS limits of a service, if any.
+ *
+ * If the service has a resolution and/or FPS limit, this will enforce those
+ * limitations in the UI itself, preventing the user from selecting a
+ * resolution or FPS that's not supported.
+ *
+ * This is an unpleasant thing to have to do to users, but there is no other
+ * way to ensure that a service's restricted resolution/framerate values are
+ * properly enforced, otherwise users will just be confused when things aren't
+ * working correctly. The user can turn it off if they're partner (or if they
+ * want to risk getting in trouble with their service) by selecting the "Ignore
+ * recommended settings" option in the stream section of settings.
+ *
+ * This only affects services that have a resolution and/or framerate limit, of
+ * which as of this writing, and hopefully for the foreseeable future, there is
+ * only one.
+ */
+void OBSBasicSettings::UpdateResFPSLimits()
+{
+ if (loading)
+ return;
+
+ int idx = ui->service->currentIndex();
+ if (idx == -1)
+ return;
+
+ bool ignoreRecommended = ui->ignoreRecommended->isChecked();
+ BPtr res_list;
+ size_t res_count = 0;
+ int max_fps = 0;
+
+ if (!IsCustomService() && !ignoreRecommended) {
+ OBSService service = GetStream1Service();
+ obs_service_get_supported_resolutions(service, &res_list,
+ &res_count);
+ obs_service_get_max_fps(service, &max_fps);
+ }
+
+ /* ------------------------------------ */
+ /* Check for enforced res/FPS */
+
+ QString res = ui->outputResolution->currentText();
+ QString fps_str;
+ int cx = 0, cy = 0;
+ double max_fpsd = (double)max_fps;
+ int closest_fps_index = -1;
+ double fpsd;
+
+ sscanf(QT_TO_UTF8(res), "%dx%d", &cx, &cy);
+
+ if (res_count)
+ set_closest_res(cx, cy, res_list, res_count);
+
+ if (max_fps) {
+ int fpsType = ui->fpsType->currentIndex();
+
+ if (fpsType == 1) { //Integer
+ fpsd = (double)ui->fpsInteger->value();
+ } else if (fpsType == 2) { //Fractional
+ fpsd = (double)ui->fpsNumerator->value() /
+ (double)ui->fpsDenominator->value();
+ } else { //Common
+ sscanf(QT_TO_UTF8(ui->fpsCommon->currentText()), "%lf",
+ &fpsd);
+ }
+
+ double closest_diff = 1000000000000.0;
+
+ for (int i = 0; i < ui->fpsCommon->count(); i++) {
+ double com_fpsd;
+ sscanf(QT_TO_UTF8(ui->fpsCommon->itemText(i)), "%lf",
+ &com_fpsd);
+
+ if (com_fpsd > max_fpsd) {
+ continue;
+ }
+
+ double diff = fabs(com_fpsd - fpsd);
+ if (diff < closest_diff) {
+ closest_diff = diff;
+ closest_fps_index = i;
+ fps_str = ui->fpsCommon->itemText(i);
+ }
+ }
+ }
+
+ QString res_str =
+ QString("%1x%2").arg(QString::number(cx), QString::number(cy));
+
+ /* ------------------------------------ */
+ /* Display message box if res/FPS bad */
+
+ bool valid = ResFPSValid(res_list, res_count, max_fps);
+
+ if (!valid) {
+ /* if the user was already on facebook with an incompatible
+ * resolution, assume it's an upgrade */
+ if (lastServiceIdx == -1 && lastIgnoreRecommended == -1) {
+ ui->ignoreRecommended->setChecked(true);
+ ui->ignoreRecommended->setProperty("changed", true);
+ stream1Changed = true;
+ EnableApplyButton(true);
+ UpdateResFPSLimits();
+ return;
+ }
+
+ QMessageBox::StandardButton button;
+
+#define WARNING_VAL(x) \
+ QTStr("Basic.Settings.Output.Warn.EnforceResolutionFPS." x)
+
+ QString str;
+ if (res_count)
+ str += WARNING_VAL("Resolution").arg(res_str);
+ if (max_fps) {
+ if (!str.isEmpty())
+ str += "\n";
+ str += WARNING_VAL("FPS").arg(fps_str);
+ }
+
+ button = OBSMessageBox::question(this, WARNING_VAL("Title"),
+ WARNING_VAL("Msg").arg(str));
+#undef WARNING_VAL
+
+ if (button == QMessageBox::No) {
+ if (idx != lastServiceIdx)
+ QMetaObject::invokeMethod(
+ ui->service, "setCurrentIndex",
+ Qt::QueuedConnection,
+ Q_ARG(int, lastServiceIdx));
+ else
+ QMetaObject::invokeMethod(ui->ignoreRecommended,
+ "setChecked",
+ Qt::QueuedConnection,
+ Q_ARG(bool, true));
+ return;
+ }
+ }
+
+ /* ------------------------------------ */
+ /* Update widgets/values if switching */
+ /* to/from enforced resolution/FPS */
+
+ ui->outputResolution->blockSignals(true);
+ if (res_count) {
+ ui->outputResolution->clear();
+ ui->outputResolution->setEditable(false);
+
+ int new_res_index = -1;
+
+ for (size_t i = 0; i < res_count; i++) {
+ obs_service_resolution val = res_list[i];
+ QString str =
+ QString("%1x%2").arg(QString::number(val.cx),
+ QString::number(val.cy));
+ ui->outputResolution->addItem(str);
+
+ if (val.cx == cx && val.cy == cy)
+ new_res_index = (int)i;
+ }
+
+ ui->outputResolution->setCurrentIndex(new_res_index);
+ if (!valid) {
+ ui->outputResolution->setProperty("changed", true);
+ videoChanged = true;
+ EnableApplyButton(true);
+ }
+ } else {
+ QString baseRes = ui->baseResolution->currentText();
+ int baseCX, baseCY;
+ sscanf(QT_TO_UTF8(baseRes), "%dx%d", &baseCX, &baseCY);
+
+ if (!ui->outputResolution->isEditable()) {
+ RecreateOutputResolutionWidget();
+ ui->outputResolution->blockSignals(true);
+ ResetDownscales((uint32_t)baseCX, (uint32_t)baseCY,
+ true);
+ ui->outputResolution->setCurrentText(res);
+ }
+ }
+ ui->outputResolution->blockSignals(false);
+
+ if (max_fps) {
+ for (int i = 0; i < ui->fpsCommon->count(); i++) {
+ double com_fpsd;
+ sscanf(QT_TO_UTF8(ui->fpsCommon->itemText(i)), "%lf",
+ &com_fpsd);
+
+ if (com_fpsd > max_fpsd) {
+ SetComboItemEnabled(ui->fpsCommon, i, false);
+ continue;
+ }
+ }
+
+ ui->fpsType->setCurrentIndex(0);
+ ui->fpsCommon->setCurrentIndex(closest_fps_index);
+ if (!valid) {
+ ui->fpsType->setProperty("changed", true);
+ ui->fpsCommon->setProperty("changed", true);
+ videoChanged = true;
+ EnableApplyButton(true);
+ }
+ } else {
+ for (int i = 0; i < ui->fpsCommon->count(); i++)
+ SetComboItemEnabled(ui->fpsCommon, i, true);
+ }
+
+ SetComboItemEnabled(ui->fpsType, 1, !max_fps);
+ SetComboItemEnabled(ui->fpsType, 2, !max_fps);
+
+ /* ------------------------------------ */
+
+ lastIgnoreRecommended = (int)ignoreRecommended;
+ lastServiceIdx = idx;
+}
diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp
index aca23e9a4..0858e1d4f 100644
--- a/UI/window-basic-settings.cpp
+++ b/UI/window-basic-settings.cpp
@@ -1365,14 +1365,17 @@ void OBSBasicSettings::ResetDownscales(uint32_t cx, uint32_t cy,
advRecRescale = ui->advOutRecRescale->lineEdit()->text();
advFFRescale = ui->advOutFFRescale->lineEdit()->text();
- ui->outputResolution->blockSignals(true);
+ bool lockedOutputRes = !ui->outputResolution->isEditable();
+ if (!lockedOutputRes) {
+ ui->outputResolution->blockSignals(true);
+ ui->outputResolution->clear();
+ }
if (ignoreAllSignals) {
ui->advOutRescale->blockSignals(true);
ui->advOutRecRescale->blockSignals(true);
ui->advOutFFRescale->blockSignals(true);
}
- ui->outputResolution->clear();
ui->advOutRescale->clear();
ui->advOutRecRescale->clear();
ui->advOutFFRescale->clear();
@@ -1399,7 +1402,8 @@ void OBSBasicSettings::ResetDownscales(uint32_t cx, uint32_t cy,
string res = ResString(downscaleCX, downscaleCY);
string outRes = ResString(outDownscaleCX, outDownscaleCY);
- ui->outputResolution->addItem(res.c_str());
+ if (!lockedOutputRes)
+ ui->outputResolution->addItem(res.c_str());
ui->advOutRescale->addItem(outRes.c_str());
ui->advOutRecRescale->addItem(outRes.c_str());
ui->advOutFFRescale->addItem(outRes.c_str());
@@ -1418,23 +1422,27 @@ void OBSBasicSettings::ResetDownscales(uint32_t cx, uint32_t cy,
string res = ResString(cx, cy);
- float baseAspect = float(cx) / float(cy);
- float outputAspect = float(out_cx) / float(out_cy);
+ if (!lockedOutputRes) {
+ float baseAspect = float(cx) / float(cy);
+ float outputAspect = float(out_cx) / float(out_cy);
+ bool closeAspect = close_float(baseAspect, outputAspect, 0.01f);
- bool closeAspect = close_float(baseAspect, outputAspect, 0.01f);
- if (closeAspect) {
- ui->outputResolution->lineEdit()->setText(oldOutputRes);
- on_outputResolution_editTextChanged(oldOutputRes);
- } else {
- ui->outputResolution->lineEdit()->setText(bestScale.c_str());
- on_outputResolution_editTextChanged(bestScale.c_str());
- }
+ if (closeAspect) {
+ ui->outputResolution->lineEdit()->setText(oldOutputRes);
+ on_outputResolution_editTextChanged(oldOutputRes);
+ } else {
+ ui->outputResolution->lineEdit()->setText(
+ bestScale.c_str());
+ on_outputResolution_editTextChanged(bestScale.c_str());
+ }
- ui->outputResolution->blockSignals(false);
+ ui->outputResolution->blockSignals(false);
- if (!closeAspect) {
- ui->outputResolution->setProperty("changed", QVariant(true));
- videoChanged = true;
+ if (!closeAspect) {
+ ui->outputResolution->setProperty("changed",
+ QVariant(true));
+ videoChanged = true;
+ }
}
if (advRescale.isEmpty())
@@ -3884,16 +3892,22 @@ void OBSBasicSettings::on_colorFormat_currentIndexChanged(const QString &text)
static bool ValidResolutions(Ui::OBSBasicSettings *ui)
{
QString baseRes = ui->baseResolution->lineEdit()->text();
- QString outputRes = ui->outputResolution->lineEdit()->text();
uint32_t cx, cy;
- if (!ConvertResText(QT_TO_UTF8(baseRes), cx, cy) ||
- !ConvertResText(QT_TO_UTF8(outputRes), cx, cy)) {
-
+ if (!ConvertResText(QT_TO_UTF8(baseRes), cx, cy)) {
ui->videoMsg->setText(QTStr(INVALID_RES_STR));
return false;
}
+ bool lockedOutRes = !ui->outputResolution->isEditable();
+ if (!lockedOutRes) {
+ QString outRes = ui->outputResolution->lineEdit()->text();
+ if (!ConvertResText(QT_TO_UTF8(outRes), cx, cy)) {
+ ui->videoMsg->setText(QTStr(INVALID_RES_STR));
+ return false;
+ }
+ }
+
ui->videoMsg->setText("");
return true;
}
@@ -4865,3 +4879,31 @@ int OBSBasicSettings::CurrentFLVTrack()
return 0;
}
+
+/* Using setEditable(true) on a QComboBox when there's a custom style in use
+ * does not work properly, so instead completely recreate the widget, which
+ * seems to work fine. */
+void OBSBasicSettings::RecreateOutputResolutionWidget()
+{
+ QSizePolicy sizePolicy = ui->outputResolution->sizePolicy();
+ delete ui->outputResolution;
+ ui->outputResolution = new QComboBox(ui->videoPage);
+ ui->outputResolution->setObjectName(
+ QString::fromUtf8("outputResolution"));
+ ui->outputResolution->setSizePolicy(sizePolicy);
+ ui->outputResolution->setEditable(true);
+ ui->outputResLabel->setBuddy(ui->outputResolution);
+
+ ui->outputResLayout->insertWidget(0, ui->outputResolution);
+
+ QWidget::setTabOrder(ui->baseResolution, ui->outputResolution);
+ QWidget::setTabOrder(ui->outputResolution, ui->downscaleFilter);
+
+ HookWidget(ui->outputResolution, CBEDIT_CHANGED, VIDEO_RES);
+
+ connect(ui->outputResolution, &QComboBox::editTextChanged, this,
+ &OBSBasicSettings::on_outputResolution_editTextChanged);
+
+ ui->outputResolution->lineEdit()->setValidator(
+ ui->baseResolution->lineEdit()->validator());
+}
diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp
index 6f97505fb..1387a2046 100644
--- a/UI/window-basic-settings.hpp
+++ b/UI/window-basic-settings.hpp
@@ -122,6 +122,8 @@ private:
int channelIndex = 0;
int lastSimpleRecQualityIdx = 0;
+ int lastServiceIdx = -1;
+ int lastIgnoreRecommended = -1;
int lastChannelSetupIdx = 0;
OBSFFFormatDesc formats;
@@ -175,6 +177,11 @@ private:
void SaveEncoder(QComboBox *combo, const char *section,
const char *value);
+ bool ResFPSValid(obs_service_resolution *res_list, size_t res_count,
+ int max_fps);
+ void ClosestResFPS(obs_service_resolution *res_list, size_t res_count,
+ int max_fps, int &new_cx, int &new_cy, int &new_fps);
+
inline bool Changed() const
{
return generalChanged || outputsChanged || stream1Changed ||
@@ -246,6 +253,8 @@ private slots:
void UpdateKeyLink();
void UpdateVodTrackSetting();
void UpdateServiceRecommendations();
+ void RecreateOutputResolutionWidget();
+ void UpdateResFPSLimits();
void UpdateMoreInfoLink();
void DisplayEnforceWarning(bool checked);
void on_show_clicked();