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