diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 862797b83..55f2e65a1 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -135,8 +135,10 @@ set(obs_SOURCES window-basic-settings.cpp window-basic-interaction.cpp window-basic-properties.cpp + window-basic-auto-config.cpp window-basic-main-outputs.cpp window-basic-source-select.cpp + window-basic-auto-config-test.cpp window-basic-main-scene-collections.cpp window-basic-main-transitions.cpp window-basic-main-dropfiles.cpp @@ -181,6 +183,7 @@ set(obs_HEADERS window-basic-settings.hpp window-basic-interaction.hpp window-basic-properties.hpp + window-basic-auto-config.hpp window-basic-main-outputs.hpp window-basic-source-select.hpp window-license-agreement.hpp @@ -217,6 +220,10 @@ set(obs_HEADERS set(obs_UI forms/NameDialog.ui + forms/AutoConfigStartPage.ui + forms/AutoConfigVideoPage.ui + forms/AutoConfigStreamPage.ui + forms/AutoConfigTestPage.ui forms/OBSLicenseAgreement.ui forms/OBSLogReply.ui forms/OBSBasic.ui diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index b894fe446..f41ec5d46 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -74,6 +74,63 @@ RemuxRecordings="Remux Recordings" Copy.Filters="Copy Filters" Paste.Filters="Paste Filters" +# bandwidth test +BandwidthTest.Region="Region" +BandwidthTest.Region.US="United States" +BandwidthTest.Region.EU="Europe" +BandwidthTest.Region.Asia="Asia" +BandwidthTest.Region.Other="Other" + +# first time startup +Basic.FirstStartup.RunWizard="Would you like to run the auto-configuration wizard? You can also manually configure your settings by clicking the Settings button in the main window." +Basic.FirstStartup.RunWizard.BetaWarning="(Note: The auto-configuration wizard is currently in beta)" +Basic.FirstStartup.RunWizard.NoClicked="If you change your mind, you can run the auto-configuration wizard any time again from the Tools menu." + +# auto config wizard +Basic.AutoConfig="Auto-Configuration Wizard" +Basic.AutoConfig.Beta="Auto-Configuration Wizard (Beta)" +Basic.AutoConfig.ApplySettings="Apply Settings" +Basic.AutoConfig.StartPage="Usage Information" +Basic.AutoConfig.StartPage.SubTitle="Specify what you want to to use the program for" +Basic.AutoConfig.StartPage.PrioritizeStreaming="Optimize for streaming, recording is secondary" +Basic.AutoConfig.StartPage.PrioritizeRecording="Optimize just for recording, I will not be streaming" +Basic.AutoConfig.VideoPage="Video Settings" +Basic.AutoConfig.VideoPage.SubTitle="Specify the desired video settings you would like to use" +Basic.AutoConfig.VideoPage.BaseResolution.UseCurrent="Use Current (%1x%2)" +Basic.AutoConfig.VideoPage.BaseResolution.Display="Display %1 (%2x%3)" +Basic.AutoConfig.VideoPage.FPS.UseCurrent="Use Current (%1)" +Basic.AutoConfig.VideoPage.FPS.PreferHighFPS="Either 60 or 30, but prefer 60 when possible" +Basic.AutoConfig.VideoPage.FPS.PreferHighRes="Either 60 or 30, but prefer high resolution" +Basic.AutoConfig.VideoPage.CanvasExplanation="Note: The canvas (base) resolution is not necessarily the same as the resolution you will stream or record with. Your actual stream/recording resolution may be scaled down from the canvas resolution to reduce resource usage or bitrate requirements." +Basic.AutoConfig.StreamPage="Stream Information" +Basic.AutoConfig.StreamPage.SubTitle="Please enter your stream information" +Basic.AutoConfig.StreamPage.Service="Service" +Basic.AutoConfig.StreamPage.Service.ShowAll="Show All..." +Basic.AutoConfig.StreamPage.Server="Server" +Basic.AutoConfig.StreamPage.StreamKey="Stream Key" +Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(Link)" +Basic.AutoConfig.StreamPage.PerformBandwidthTest="Estimate bitrate with bandwidth test (may take a few minutes)" +Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Prefer hardware encoding" +Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="Hardware Encoding eliminates most CPU usage, but may require more bitrate to obtain the same level of quality." +Basic.AutoConfig.StreamPage.StreamWarning.Title="Stream warning" +Basic.AutoConfig.StreamPage.StreamWarning.Text="The bandwidth test is about to stream randomized video data without audio to your channel. If you're able, it's recommended to temporarily turn off saving videos of streams and set the stream to private until after the test has completed. Continue?" +Basic.AutoConfig.TestPage="Final Results" +Basic.AutoConfig.TestPage.SubTitle.Testing="The program is now executing a set of tests to estimate the most ideal settings" +Basic.AutoConfig.TestPage.SubTitle.Complete="Testing complete" +Basic.AutoConfig.TestPage.TestingBandwidth="Performing bandwidth test, this may take a few minutes..." +Basic.AutoConfig.TestPage.TestingBandwidth.Connecting="Connecting to: %1..." +Basic.AutoConfig.TestPage.TestingBandwidth.ConnectFailed="Failed to connect to any servers, please check your internet connection and try again." +Basic.AutoConfig.TestPage.TestingBandwidth.Server="Testing bandwidth for: %1" +Basic.AutoConfig.TestPage.TestingStreamEncoder="Testing stream encoder, this may take a minute..." +Basic.AutoConfig.TestPage.TestingRecordingEncoder="Testing recording encoder, this may take a minute..." +Basic.AutoConfig.TestPage.TestingRes="Testing resolutions, this may take a few minutes..." +Basic.AutoConfig.TestPage.TestingRes.Fail="Failed to start up encoder" +Basic.AutoConfig.TestPage.TestingRes.Resolution="Testing %1x%2 %3 FPS..." +Basic.AutoConfig.TestPage.Result.StreamingEncoder="Streaming Encoder" +Basic.AutoConfig.TestPage.Result.RecordingEncoder="Recording Encoder" +Basic.AutoConfig.TestPage.Result.Header="The program has determined that these estimated settings are the most ideal for you:" +Basic.AutoConfig.TestPage.Result.Footer="To use these settings, click Apply Settings. To reconfigure the wizard and try again, click Back. To manually configure settings yourself, click Cancel and open Settings." + # updater Updater.Title="New update available" Updater.Text="There is a new update available:" diff --git a/UI/forms/AutoConfigFinishPage.ui b/UI/forms/AutoConfigFinishPage.ui new file mode 100644 index 000000000..ae3ddc781 --- /dev/null +++ b/UI/forms/AutoConfigFinishPage.ui @@ -0,0 +1,41 @@ + + + AutoConfigFinishPage + + + + 0 + 0 + 400 + 300 + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/UI/forms/AutoConfigStartPage.ui b/UI/forms/AutoConfigStartPage.ui new file mode 100644 index 000000000..e8730435f --- /dev/null +++ b/UI/forms/AutoConfigStartPage.ui @@ -0,0 +1,51 @@ + + + AutoConfigStartPage + + + + 0 + 0 + 400 + 300 + + + + + + + + + + Basic.AutoConfig.StartPage.PrioritizeStreaming + + + true + + + + + + + Basic.AutoConfig.StartPage.PrioritizeRecording + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/UI/forms/AutoConfigStreamPage.ui b/UI/forms/AutoConfigStreamPage.ui new file mode 100644 index 000000000..16571e686 --- /dev/null +++ b/UI/forms/AutoConfigStreamPage.ui @@ -0,0 +1,252 @@ + + + AutoConfigStreamPage + + + + 0 + 0 + 566 + 335 + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + Basic.AutoConfig.StreamPage.Service + + + service + + + + + + + + + + Basic.AutoConfig.StreamPage.StreamKey + + + true + + + key + + + + + + + + + + + + + + + QLineEdit::Password + + + + + + + Show + + + + + + + + + Basic.AutoConfig.StreamPage.PerformBandwidthTest + + + true + + + + + + + Basic.AutoConfig.StreamPage.PreferHardwareEncoding + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 90 + 20 + + + + + + + + Basic.AutoConfig.StreamPage.Server + + + + + + + Basic.Settings.Stream.StreamType + + + + + + + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + BandwidthTest.Region + + + + + + BandwidthTest.Region.Asia + + + + + + + BandwidthTest.Region.US + + + + + + + BandwidthTest.Region.EU + + + + + + + BandwidthTest.Region.Other + + + + + + + + + + + + + 500 + + + 10000 + + + 2500 + + + + + + + Basic.Settings.Output.VideoBitrate + + + bitrate + + + + + + + streamType + service + server + customServer + key + show + preferHardware + doBandwidthTest + regionUS + regionEU + regionAsia + regionOther + + + + diff --git a/UI/forms/AutoConfigTestPage.ui b/UI/forms/AutoConfigTestPage.ui new file mode 100644 index 000000000..95c2d4415 --- /dev/null +++ b/UI/forms/AutoConfigTestPage.ui @@ -0,0 +1,141 @@ + + + AutoConfigTestPage + + + + 0 + 0 + 400 + 300 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 2 + + + + + + + + + + + + + + + + + + + + + 24 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + 0 + + + + Basic.AutoConfig.TestPage.Result.Header + + + true + + + + + + + Basic.AutoConfig.TestPage.Result.Footer + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + + + + diff --git a/UI/forms/AutoConfigVideoPage.ui b/UI/forms/AutoConfigVideoPage.ui new file mode 100644 index 000000000..b941cc8c7 --- /dev/null +++ b/UI/forms/AutoConfigVideoPage.ui @@ -0,0 +1,96 @@ + + + AutoConfigVideoPage + + + + 0 + 0 + 470 + 300 + + + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Basic.Settings.Video.BaseResolution + + + canvasRes + + + + + + + + + + Basic.Settings.Video.FPS + + + fps + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 87 + 17 + + + + + + + + + + Basic.AutoConfig.VideoPage.CanvasExplanation + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index f4a128f26..0e284964e 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -983,12 +983,11 @@ - - false - Basic.MainMenu.Tools + + @@ -1462,6 +1461,16 @@ PasteDuplicate + + + Basic.AutoConfig + + + + + Basic.AutoConfig.Beta + + diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp new file mode 100644 index 000000000..724ae5c86 --- /dev/null +++ b/UI/window-basic-auto-config-test.cpp @@ -0,0 +1,1069 @@ +#include + +#include + +#include +#include +#include +#include +#include + +#include "window-basic-auto-config.hpp" +#include "window-basic-main.hpp" +#include "qt-wrappers.hpp" +#include "obs-app.hpp" + +#include "ui_AutoConfigTestPage.h" + +#define wiz reinterpret_cast(wizard()) + +using namespace std; + +/* ------------------------------------------------------------------------- */ + +class TestMode { + obs_video_info ovi; + OBSSource source[6]; + + static void render_rand(void *, uint32_t cx, uint32_t cy) + { + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + gs_eparam_t *randomvals[3] = { + gs_effect_get_param_by_name(solid, "randomvals1"), + gs_effect_get_param_by_name(solid, "randomvals2"), + gs_effect_get_param_by_name(solid, "randomvals3") + }; + + struct vec4 r; + + for (int i = 0; i < 3; i++) { + vec4_set(&r, + rand_float(true) * 100.0f, + rand_float(true) * 100.0f, + rand_float(true) * 50000.0f + 10000.0f, + 0.0f); + gs_effect_set_vec4(randomvals[i], &r); + } + + while (gs_effect_loop(solid, "Random")) + gs_draw_sprite(nullptr, 0, cx, cy); + } + +public: + inline TestMode() + { + obs_get_video_info(&ovi); + obs_add_main_render_callback(render_rand, this); + + for (uint32_t i = 0; i < 6; i++) { + source[i] = obs_get_output_source(i); + obs_source_release(source[i]); + obs_set_output_source(i, nullptr); + } + } + + inline ~TestMode() + { + for (uint32_t i = 0; i < 6; i++) + obs_set_output_source(i, source[i]); + + obs_remove_main_render_callback(render_rand, this); + obs_reset_video(&ovi); + } + + inline void SetVideo(int cx, int cy, int fps_num, int fps_den) + { + obs_video_info newOVI = ovi; + + newOVI.output_width = (uint32_t)cx; + newOVI.output_height = (uint32_t)cy; + newOVI.fps_num = (uint32_t)fps_num; + newOVI.fps_den = (uint32_t)fps_den; + + obs_reset_video(&newOVI); + } +}; + +/* ------------------------------------------------------------------------- */ + +#define TEST_STR(x) "Basic.AutoConfig.TestPage." x +#define SUBTITLE_TESTING TEST_STR("Subtitle.Testing") +#define SUBTITLE_COMPLETE TEST_STR("Subtitle.Complete") +#define TEST_BW TEST_STR("TestingBandwidth") +#define TEST_BW_CONNECTING TEST_STR("TestingBandwidth.Connecting") +#define TEST_BW_CONNECT_FAIL TEST_STR("TestingBandwidth.ConnectFailed") +#define TEST_BW_SERVER TEST_STR("TestingBandwidth.Server") +#define TEST_RES TEST_STR("TestingRes") +#define TEST_RES_VAL TEST_STR("TestingRes.Resolution") +#define TEST_RES_FAIL TEST_STR("TestingRes.Fail") +#define TEST_SE TEST_STR("TestingStreamEncoder") +#define TEST_RE TEST_STR("TestingRecordingEncoder") +#define TEST_RESULT_SE TEST_STR("Result.StreamingEncoder") +#define TEST_RESULT_RE TEST_STR("Result.RecordingEncoder") + +void AutoConfigTestPage::StartBandwidthStage() +{ + ui->progressLabel->setText(QTStr(TEST_BW)); + testThread = std::thread([this] () {TestBandwidthThread();}); +} + +void AutoConfigTestPage::StartStreamEncoderStage() +{ + ui->progressLabel->setText(QTStr(TEST_SE)); + testThread = std::thread([this] () {TestStreamEncoderThread();}); +} + +void AutoConfigTestPage::StartRecordingEncoderStage() +{ + ui->progressLabel->setText(QTStr(TEST_RE)); + testThread = std::thread([this] () {TestRecordingEncoderThread();}); +} + +void AutoConfigTestPage::GetServers(std::vector &servers) +{ + OBSData settings = obs_data_create(); + obs_data_release(settings); + obs_data_set_string(settings, "service", wiz->serviceName.c_str()); + + obs_properties_t *ppts = obs_get_service_properties("rtmp_common"); + obs_property_t *p = obs_properties_get(ppts, "service"); + obs_property_modified(p, settings); + + p = obs_properties_get(ppts, "server"); + size_t count = obs_property_list_item_count(p); + servers.reserve(count); + + for (size_t i = 0; i < count; i++) { + const char *name = obs_property_list_item_name(p, i); + const char *server = obs_property_list_item_string(p, i); + + if (wiz->CanTestServer(name)) { + ServerInfo info(name, server); + servers.push_back(info); + } + } + + obs_properties_destroy(ppts); +} + +void AutoConfigTestPage::TestBandwidthThread() +{ + bool connected = false; + bool stopped = false; + + TestMode testMode; + testMode.SetVideo(128, 128, 60, 1); + + QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, 0)); + + /* + * create encoders + * create output + * test for 10 seconds + */ + + QMetaObject::invokeMethod(this, "UpdateMessage", + Q_ARG(QString, QStringLiteral(""))); + + /* -----------------------------------*/ + /* create obs objects */ + + const char *serverType = wiz->customServer + ? "rtmp_custom" + : "rtmp_common"; + + OBSEncoder vencoder = obs_video_encoder_create("obs_x264", + "test_x264", nullptr, nullptr); + OBSEncoder aencoder = obs_audio_encoder_create("ffmpeg_aac", + "test_aac", nullptr, 0, nullptr); + OBSService service = obs_service_create(serverType, + "test_service", nullptr, nullptr); + OBSOutput output = obs_output_create("rtmp_output", + "test_stream", nullptr, nullptr); + obs_output_release(output); + obs_encoder_release(vencoder); + obs_encoder_release(aencoder); + obs_service_release(service); + + /* -----------------------------------*/ + /* configure settings */ + + // service: "service", "server", "key" + // vencoder: "bitrate", "rate_control", + // obs_service_apply_encoder_settings + // aencoder: "bitrate" + // output: "bind_ip" via main config -> "Output", "BindIP" + // obs_output_set_service + + OBSData service_settings = obs_data_create(); + OBSData vencoder_settings = obs_data_create(); + OBSData aencoder_settings = obs_data_create(); + OBSData output_settings = obs_data_create(); + obs_data_release(service_settings); + obs_data_release(vencoder_settings); + obs_data_release(aencoder_settings); + obs_data_release(output_settings); + + std::string key = wiz->key; + if (wiz->service == AutoConfig::Service::Twitch) + key += "?bandwidthtest"; + + obs_data_set_string(service_settings, "service", + wiz->serviceName.c_str()); + obs_data_set_string(service_settings, "key", key.c_str()); + + obs_data_set_int(vencoder_settings, "bitrate", wiz->startingBitrate); + obs_data_set_string(vencoder_settings, "rate_control", "CBR"); + obs_data_set_string(vencoder_settings, "preset", "veryfast"); + obs_data_set_int(vencoder_settings, "keyint_sec", 2); + + obs_data_set_int(aencoder_settings, "bitrate", 32); + + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + const char *bind_ip = config_get_string(main->Config(), "Output", + "BindIP"); + obs_data_set_string(output_settings, "bind_ip", bind_ip); + + /* -----------------------------------*/ + /* determine which servers to test */ + + std::vector servers; + if (wiz->customServer) + servers.emplace_back(wiz->server.c_str(), wiz->server.c_str()); + else + GetServers(servers); + + /* just use the first server if it only has one alternate server */ + if (servers.size() < 3) + servers.resize(1); + + /* -----------------------------------*/ + /* apply settings */ + + obs_service_apply_encoder_settings(service, + vencoder_settings, aencoder_settings); + + obs_encoder_update(vencoder, vencoder_settings); + obs_encoder_update(aencoder, aencoder_settings); + obs_service_update(service, service_settings); + obs_output_update(output, output_settings); + + /* -----------------------------------*/ + /* connect encoders/services/outputs */ + + obs_encoder_set_video(vencoder, obs_get_video()); + obs_encoder_set_audio(aencoder, obs_get_audio()); + + obs_output_set_video_encoder(output, vencoder); + obs_output_set_audio_encoder(output, aencoder, 0); + + obs_output_set_service(output, service); + + /* -----------------------------------*/ + /* connect signals */ + + auto on_started = [&] () + { + unique_lock lock(m); + connected = true; + stopped = false; + cv.notify_one(); + }; + + auto on_stopped = [&] () + { + unique_lock lock(m); + connected = false; + stopped = true; + cv.notify_one(); + }; + + using on_started_t = decltype(on_started); + using on_stopped_t = decltype(on_stopped); + + auto pre_on_started = [] (void *data, calldata_t *) + { + on_started_t &on_started = + *reinterpret_cast(data); + on_started(); + }; + + auto pre_on_stopped = [] (void *data, calldata_t *) + { + on_stopped_t &on_stopped = + *reinterpret_cast(data); + on_stopped(); + }; + + signal_handler *sh = obs_output_get_signal_handler(output); + signal_handler_connect(sh, "start", pre_on_started, &on_started); + signal_handler_connect(sh, "stop", pre_on_stopped, &on_stopped); + + /* -----------------------------------*/ + /* test servers */ + + bool success = false; + + for (size_t i = 0; i < servers.size(); i++) { + auto &server = servers[i]; + + connected = false; + stopped = false; + + int per = int((i + 1) * 100 / servers.size()); + QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per)); + QMetaObject::invokeMethod(this, "UpdateMessage", + Q_ARG(QString, QTStr(TEST_BW_CONNECTING) + .arg(server.name.c_str()))); + + obs_data_set_string(service_settings, "server", + server.address.c_str()); + obs_service_update(service, service_settings); + + if (!obs_output_start(output)) + continue; + + unique_lock ul(m); + if (cancel) { + obs_output_force_stop(output); + return; + } + if (!stopped && !connected) + cv.wait(ul); + if (cancel) { + obs_output_force_stop(output); + return; + } + if (!connected) + continue; + + QMetaObject::invokeMethod(this, "UpdateMessage", + Q_ARG(QString, QTStr(TEST_BW_SERVER) + .arg(server.name.c_str()))); + + uint64_t t_start = os_gettime_ns(); + + cv.wait_for(ul, chrono::seconds(10)); + if (stopped) + continue; + if (cancel) { + obs_output_force_stop(output); + return; + } + + obs_output_stop(output); + cv.wait(ul); + + uint64_t total_time = os_gettime_ns() - t_start; + + int total_bytes = (int)obs_output_get_total_bytes(output); + uint64_t bitrate = (uint64_t)total_bytes * 8 + * 1000000000 / total_time / 1000; + + if (obs_output_get_frames_dropped(output) || + (int)bitrate < (wiz->startingBitrate * 75 / 100)) { + server.bitrate = (int)bitrate * 70 / 100; + } else { + server.bitrate = wiz->startingBitrate; + } + + server.ms = obs_output_get_connect_time_ms(output); + success = true; + } + + if (!success) { + QMetaObject::invokeMethod(this, "Failure", + Q_ARG(QString, QTStr(TEST_BW_CONNECT_FAIL))); + return; + } + + int bestBitrate = 0; + int bestMS = 0x7FFFFFFF; + string bestServer; + string bestServerName; + + for (auto &server : servers) { + bool close = abs(server.bitrate - bestBitrate) < 400; + + if ((!close && server.bitrate > bestBitrate) || + (close && server.ms < bestMS)) { + bestServer = server.address; + bestServerName = server.name; + bestBitrate = server.bitrate; + bestMS = server.ms; + } + } + + wiz->server = bestServer; + wiz->serverName = bestServerName; + wiz->idealBitrate = bestBitrate; + + QMetaObject::invokeMethod(this, "NextStage"); +} + +/* this is used to estimate the lower bitrate limit for a given + * resolution/fps. yes, it is a totally arbitrary equation that gets + * the closest to the expected values */ +static long double EstimateBitrateVal(int cx, int cy, int fps_num, int fps_den) +{ + long fps = (long double)fps_num / (long double)fps_den; + long double areaVal = pow((long double)(cx * cy), 0.85l); + return areaVal * sqrt(pow(fps, 1.1l)); +} + +static long double EstimateMinBitrate(int cx, int cy, int fps_num, int fps_den) +{ + long double val = EstimateBitrateVal(1920, 1080, 60, 1) / 5800.0l; + return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val; +} + +static long double EstimateUpperBitrate(int cx, int cy, int fps_num, int fps_den) +{ + long double val = EstimateBitrateVal(1280, 720, 30, 1) / 3000.0l; + return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val; +} + +struct Result { + int cx; + int cy; + int fps_num; + int fps_den; + + inline Result(int cx_, int cy_, int fps_num_, int fps_den_) + : cx(cx_), cy(cy_), fps_num(fps_num_), fps_den(fps_den_) + { + } +}; + +static void CalcBaseRes(int &baseCX, int &baseCY) +{ + const int maxBaseArea = 1920 * 1200; + const int clipResArea = 1920 * 1080; + + /* if base resolution unusually high, recalculate to a more reasonable + * value to start the downscaling at, based upon 1920x1080's area. + * + * for 16:9 resolutions this will always change the starting value to + * 1920x1080 */ + if ((baseCX * baseCY) > maxBaseArea) { + long double xyAspect = + (long double)baseCX / (long double)baseCY; + baseCY = (int)sqrt((long double)clipResArea / xyAspect); + baseCX = (int)((long double)baseCY * xyAspect); + } +} + +bool AutoConfigTestPage::TestSoftwareEncoding() +{ + TestMode testMode; + QMetaObject::invokeMethod(this, "UpdateMessage", + Q_ARG(QString, QStringLiteral(""))); + + /* -----------------------------------*/ + /* create obs objects */ + + OBSEncoder vencoder = obs_video_encoder_create("obs_x264", + "test_x264", nullptr, nullptr); + OBSEncoder aencoder = obs_audio_encoder_create("ffmpeg_aac", + "test_aac", nullptr, 0, nullptr); + OBSOutput output = obs_output_create("null_output", + "null", nullptr, nullptr); + obs_output_release(output); + obs_encoder_release(vencoder); + obs_encoder_release(aencoder); + + /* -----------------------------------*/ + /* configure settings */ + + OBSData aencoder_settings = obs_data_create(); + OBSData vencoder_settings = obs_data_create(); + obs_data_release(aencoder_settings); + obs_data_release(vencoder_settings); + obs_data_set_int(aencoder_settings, "bitrate", 32); + + if (wiz->type != AutoConfig::Type::Recording) { + obs_data_set_int(vencoder_settings, "keyint_sec", 2); + obs_data_set_int(vencoder_settings, "bitrate", + wiz->idealBitrate); + obs_data_set_string(vencoder_settings, "rate_control", "CBR"); + obs_data_set_string(vencoder_settings, "profile", "main"); + obs_data_set_string(vencoder_settings, "preset", "veryfast"); + } else { + obs_data_set_int(vencoder_settings, "crf", 20); + obs_data_set_string(vencoder_settings, "rate_control", "CRF"); + obs_data_set_string(vencoder_settings, "profile", "high"); + obs_data_set_string(vencoder_settings, "preset", "veryfast"); + } + + /* -----------------------------------*/ + /* apply settings */ + + obs_encoder_update(vencoder, vencoder_settings); + obs_encoder_update(aencoder, aencoder_settings); + + /* -----------------------------------*/ + /* connect encoders/services/outputs */ + + obs_output_set_video_encoder(output, vencoder); + obs_output_set_audio_encoder(output, aencoder, 0); + + /* -----------------------------------*/ + /* connect signals */ + + auto on_stopped = [&] () + { + unique_lock lock(m); + cv.notify_one(); + }; + + using on_stopped_t = decltype(on_stopped); + + auto pre_on_stopped = [] (void *data, calldata_t *) + { + on_stopped_t &on_stopped = + *reinterpret_cast(data); + on_stopped(); + }; + + signal_handler *sh = obs_output_get_signal_handler(output); + signal_handler_connect(sh, "deactivate", pre_on_stopped, &on_stopped); + + /* -----------------------------------*/ + /* calculate starting resolution */ + + int baseCX = wiz->baseResolutionCX; + int baseCY = wiz->baseResolutionCY; + CalcBaseRes(baseCX, baseCY); + + /* -----------------------------------*/ + /* calculate starting test rates */ + + int pcores = os_get_physical_cores(); + int lcores = os_get_logical_cores(); + int maxDataRate; + if (lcores > 8 || pcores > 4) { + /* superb */ + maxDataRate = 1920 * 1200 * 60 + 1000; + + } else if (lcores > 4 && pcores == 4) { + /* great */ + maxDataRate = 1920 * 1080 * 60 + 1000; + + } else if (pcores == 4) { + /* okay */ + maxDataRate = 1920 * 1080 * 30 + 1000; + + } else { + /* toaster */ + maxDataRate = 960 * 540 * 30 + 1000; + } + + /* -----------------------------------*/ + /* perform tests */ + + vector results; + int i = 0; + int count = 1; + + auto testRes = [&] (long double div, int fps_num, int fps_den, + bool force) + { + int per = ++i * 100 / count; + QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per)); + + /* no need for more than 3 tests max */ + if (results.size() >= 3) + return true; + + if (!fps_num || !fps_den) { + fps_num = wiz->specificFPSNum; + fps_den = wiz->specificFPSDen; + } + + long double fps = ((long double)fps_num / (long double)fps_den); + + int cx = int((long double)baseCX / div); + int cy = int((long double)baseCY / div); + + if (!force && wiz->type != AutoConfig::Type::Recording) { + int est = EstimateMinBitrate(cx, cy, fps_num, fps_den); + if (est > wiz->idealBitrate) + return true; + } + + long double rate = (long double)cx * (long double)cy * fps; + if (!force && rate > maxDataRate) + return true; + + testMode.SetVideo(cx, cy, fps_num, fps_den); + + obs_encoder_set_video(vencoder, obs_get_video()); + obs_encoder_set_audio(aencoder, obs_get_audio()); + obs_encoder_update(vencoder, vencoder_settings); + + obs_output_set_media(output, obs_get_video(), obs_get_audio()); + + QString cxStr = QString::number(cx); + QString cyStr = QString::number(cy); + + QString fpsStr = (fps_den > 1) + ? QString::number(fps, 'f', 2) + : QString::number(fps, 'g', 2); + + QMetaObject::invokeMethod(this, "UpdateMessage", + Q_ARG(QString, QTStr(TEST_RES_VAL) + .arg(cxStr, cyStr, fpsStr))); + + unique_lock ul(m); + if (cancel) + return false; + + if (!obs_output_start(output)) { + QMetaObject::invokeMethod(this, "Failure", + Q_ARG(QString, QTStr(TEST_RES_FAIL))); + return false; + } + + cv.wait_for(ul, chrono::seconds(5)); + + obs_output_stop(output); + cv.wait(ul); + + int skipped = (int)video_output_get_skipped_frames( + obs_get_video()); + if (force || skipped <= 10) + results.emplace_back(cx, cy, fps_num, fps_den); + + return !cancel; + }; + + if (wiz->specificFPSNum && wiz->specificFPSDen) { + count = 5; + if (!testRes(1.0, 0, 0, false)) return false; + if (!testRes(1.5, 0, 0, false)) return false; + if (!testRes(1.0 / 0.6, 0, 0, false)) return false; + if (!testRes(2.0, 0, 0, false)) return false; + if (!testRes(2.25, 0, 0, true)) return false; + } else { + count = 10; + if (!testRes(1.0, 60, 1, false)) return false; + if (!testRes(1.0, 30, 1, false)) return false; + if (!testRes(1.5, 60, 1, false)) return false; + if (!testRes(1.5, 30, 1, false)) return false; + if (!testRes(1.0 / 0.6, 60, 1, false)) return false; + if (!testRes(1.0 / 0.6, 30, 1, false)) return false; + if (!testRes(2.0, 60, 1, false)) return false; + if (!testRes(2.0, 30, 1, false)) return false; + if (!testRes(2.25, 60, 1, false)) return false; + if (!testRes(2.25, 30, 1, true)) return false; + } + + /* -----------------------------------*/ + /* find preferred settings */ + + int minArea = 960 * 540 + 1000; + + if (!wiz->specificFPSNum && wiz->preferHighFPS && results.size() > 1) { + Result &result1 = results[0]; + Result &result2 = results[1]; + + if (result1.fps_num == 30 && result2.fps_num == 60) { + int nextArea = result2.cx * result2.cy; + if (nextArea >= minArea) + results.erase(results.begin()); + } + } + + Result result = results.front(); + wiz->idealResolutionCX = result.cx; + wiz->idealResolutionCY = result.cy; + wiz->idealFPSNum = result.fps_num; + wiz->idealFPSDen = result.fps_den; + + long double fUpperBitrate = EstimateUpperBitrate( + result.cx, result.cy, result.fps_num, result.fps_den); + + int upperBitrate = int(floor(fUpperBitrate / 50.0l) * 50.0l); + + if (wiz->streamingEncoder != AutoConfig::Encoder::x264) { + upperBitrate *= 114; + upperBitrate /= 100; + } + + if (wiz->idealBitrate > upperBitrate) + wiz->idealBitrate = upperBitrate; + + softwareTested = true; + return true; +} + +void AutoConfigTestPage::FindIdealHardwareResolution() +{ + int baseCX = wiz->baseResolutionCX; + int baseCY = wiz->baseResolutionCY; + CalcBaseRes(baseCX, baseCY); + + vector results; + + int pcores = os_get_physical_cores(); + int maxDataRate; + if (pcores >= 4) { + maxDataRate = 1920 * 1200 * 60 + 1000; + } else { + maxDataRate = 1280 * 720 * 30 + 1000; + } + + auto testRes = [&] (long double div, int fps_num, int fps_den, + bool force) + { + if (results.size() >= 3) + return; + + if (!fps_num || !fps_den) { + fps_num = wiz->specificFPSNum; + fps_den = wiz->specificFPSDen; + } + + long double fps = ((long double)fps_num / (long double)fps_den); + + int cx = int((long double)baseCX / div); + int cy = int((long double)baseCY / div); + + long double rate = (long double)cx * (long double)cy * fps; + if (!force && rate > maxDataRate) + return; + + int minBitrate = EstimateMinBitrate(cx, cy, fps_num, fps_den) + * 114 / 100; + if (wiz->type == AutoConfig::Type::Recording) + force = true; + if (force || wiz->idealBitrate >= minBitrate) + results.emplace_back(cx, cy, fps_num, fps_den); + }; + + if (wiz->specificFPSNum && wiz->specificFPSDen) { + testRes(1.0, 0, 0, false); + testRes(1.5, 0, 0, false); + testRes(1.0 / 0.6, 0, 0, false); + testRes(2.0, 0, 0, false); + testRes(2.25, 0, 0, true); + } else { + testRes(1.0, 60, 1, false); + testRes(1.0, 30, 1, false); + testRes(1.5, 60, 1, false); + testRes(1.5, 30, 1, false); + testRes(1.0 / 0.6, 60, 1, false); + testRes(1.0 / 0.6, 30, 1, false); + testRes(2.0, 60, 1, false); + testRes(2.0, 30, 1, false); + testRes(2.25, 60, 1, false); + testRes(2.25, 30, 1, true); + } + + int minArea = 960 * 540 + 1000; + + if (!wiz->specificFPSNum && wiz->preferHighFPS && results.size() > 1) { + Result &result1 = results[0]; + Result &result2 = results[1]; + + if (result1.fps_num == 30 && result2.fps_num == 60) { + int nextArea = result2.cx * result2.cy; + if (nextArea >= minArea) + results.erase(results.begin()); + } + } + + Result result = results.front(); + wiz->idealResolutionCX = result.cx; + wiz->idealResolutionCY = result.cy; + wiz->idealFPSNum = result.fps_num; + wiz->idealFPSDen = result.fps_den; +} + +void AutoConfigTestPage::TestStreamEncoderThread() +{ + bool preferHardware = wiz->preferHardware; + if (!softwareTested) { + if (!preferHardware || !wiz->hardwareEncodingAvailable) { + if (!TestSoftwareEncoding()) { + return; + } + } + } + + if (preferHardware && !softwareTested && wiz->hardwareEncodingAvailable) + FindIdealHardwareResolution(); + + if (!softwareTested) { + if (wiz->nvencAvailable) + wiz->streamingEncoder = AutoConfig::Encoder::NVENC; + else if (wiz->qsvAvailable) + wiz->streamingEncoder = AutoConfig::Encoder::QSV; + else + wiz->streamingEncoder = AutoConfig::Encoder::AMD; + } else { + wiz->streamingEncoder = AutoConfig::Encoder::x264; + } + + QMetaObject::invokeMethod(this, "NextStage"); +} + +void AutoConfigTestPage::TestRecordingEncoderThread() +{ + if (!wiz->hardwareEncodingAvailable && !softwareTested) { + if (!TestSoftwareEncoding()) { + return; + } + } + + if (wiz->type == AutoConfig::Type::Recording && + wiz->hardwareEncodingAvailable) + FindIdealHardwareResolution(); + + wiz->recordingQuality = AutoConfig::Quality::High; + + bool recordingOnly = wiz->type == AutoConfig::Type::Recording; + + if (wiz->hardwareEncodingAvailable) { + if (wiz->nvencAvailable) + wiz->recordingEncoder = AutoConfig::Encoder::NVENC; + else if (wiz->qsvAvailable) + wiz->recordingEncoder = AutoConfig::Encoder::QSV; + else + wiz->recordingEncoder = AutoConfig::Encoder::AMD; + } else { + wiz->recordingEncoder = AutoConfig::Encoder::x264; + } + + if (wiz->recordingEncoder != AutoConfig::Encoder::NVENC) { + if (!recordingOnly) { + wiz->recordingEncoder = AutoConfig::Encoder::Stream; + wiz->recordingQuality = AutoConfig::Quality::Stream; + } + } + + QMetaObject::invokeMethod(this, "NextStage"); +} + +#define ENCODER_TEXT(x) "Basic.Settings.Output.Simple.Encoder." x +#define ENCODER_SOFTWARE ENCODER_TEXT("Software") +#define ENCODER_NVENC ENCODER_TEXT("Hardware.NVENC") +#define ENCODER_QSV ENCODER_TEXT("Hardware.QSV") +#define ENCODER_AMD ENCODER_TEXT("Hardware.AMD") + +#define QUALITY_SAME "Basic.Settings.Output.Simple.RecordingQuality.Stream" +#define QUALITY_HIGH "Basic.Settings.Output.Simple.RecordingQuality.Small" + +void AutoConfigTestPage::FinalizeResults() +{ + ui->stackedWidget->setCurrentIndex(1); + setSubTitle(QTStr(SUBTITLE_COMPLETE)); + + QFormLayout *form = results; + + auto encName = [] (AutoConfig::Encoder enc) -> QString + { + switch (enc) { + case AutoConfig::Encoder::x264: + return QTStr(ENCODER_SOFTWARE); + case AutoConfig::Encoder::NVENC: + return QTStr(ENCODER_NVENC); + case AutoConfig::Encoder::QSV: + return QTStr(ENCODER_QSV); + case AutoConfig::Encoder::AMD: + return QTStr(ENCODER_AMD); + case AutoConfig::Encoder::Stream: + return QTStr(QUALITY_SAME); + } + + return QTStr(ENCODER_SOFTWARE); + }; + + auto newLabel = [this] (const char *str) -> QLabel * + { + return new QLabel(QTStr(str), this); + }; + + if (wiz->type != AutoConfig::Type::Recording) { + if (!wiz->customServer) + form->addRow( + newLabel("Basic.AutoConfig.StreamPage.Service"), + new QLabel(wiz->serviceName.c_str(), + ui->finishPage)); + form->addRow(newLabel("Basic.AutoConfig.StreamPage.Server"), + new QLabel(wiz->serverName.c_str(), ui->finishPage)); + form->addRow(newLabel("Basic.Settings.Output.VideoBitrate"), + new QLabel(QString::number(wiz->idealBitrate), + ui->finishPage)); + form->addRow(newLabel(TEST_RESULT_SE), + new QLabel(encName(wiz->streamingEncoder), + ui->finishPage)); + } + + QString baseRes = QString("%1x%2").arg( + QString::number(wiz->baseResolutionCX), + QString::number(wiz->baseResolutionCY)); + QString scaleRes = QString("%1x%2").arg( + QString::number(wiz->idealResolutionCX), + QString::number(wiz->idealResolutionCY)); + + if (wiz->recordingEncoder != AutoConfig::Encoder::Stream || + wiz->recordingQuality != AutoConfig::Quality::Stream) + form->addRow(newLabel(TEST_RESULT_RE), + new QLabel(encName(wiz->recordingEncoder), + ui->finishPage)); + + QString recQuality; + + switch (wiz->recordingQuality) { + case AutoConfig::Quality::High: + recQuality = QTStr(QUALITY_HIGH); + break; + case AutoConfig::Quality::Stream: + recQuality = QTStr(QUALITY_SAME); + break; + } + + form->addRow(newLabel("Basic.Settings.Output.Simple.RecordingQuality"), + new QLabel(recQuality, ui->finishPage)); + + long double fps = + (long double)wiz->idealFPSNum / (long double)wiz->idealFPSDen; + + QString fpsStr = (wiz->idealFPSDen > 1) + ? QString::number(fps, 'f', 2) + : QString::number(fps, 'g', 2); + + form->addRow(newLabel("Basic.Settings.Video.BaseResolution"), + new QLabel(baseRes, ui->finishPage)); + form->addRow(newLabel("Basic.Settings.Video.ScaledResolution"), + new QLabel(scaleRes, ui->finishPage)); + form->addRow(newLabel("Basic.Settings.Video.FPS"), + new QLabel(fpsStr, ui->finishPage)); +} + +#define STARTING_SEPARATOR \ + "\n==== Auto-config wizard testing commencing ======\n" +#define STOPPING_SEPARATOR \ + "\n==== Auto-config wizard testing stopping ========\n" + +void AutoConfigTestPage::NextStage() +{ + if (testThread.joinable()) + testThread.join(); + if (cancel) + return; + + ui->subProgressLabel->setText(QString()); + + /* make it skip to bandwidth stage if only set to config recording */ + if (stage == Stage::Starting) { + if (!started) { + blog(LOG_INFO, STARTING_SEPARATOR); + started = true; + } + + if (wiz->type == AutoConfig::Type::Recording) { + stage = Stage::StreamEncoder; + } else if (!wiz->bandwidthTest) { + stage = Stage::BandwidthTest; + } + } + + if (stage == Stage::Starting) { + stage = Stage::BandwidthTest; + StartBandwidthStage(); + + } else if (stage == Stage::BandwidthTest) { + stage = Stage::StreamEncoder; + StartStreamEncoderStage(); + + } else if (stage == Stage::StreamEncoder) { + stage = Stage::RecordingEncoder; + StartRecordingEncoderStage(); + + } else { + stage = Stage::Finished; + FinalizeResults(); + emit completeChanged(); + } +} + +void AutoConfigTestPage::UpdateMessage(QString message) +{ + ui->subProgressLabel->setText(message); +} + +void AutoConfigTestPage::Failure(QString message) +{ + ui->errorLabel->setText(message); + ui->stackedWidget->setCurrentIndex(2); +} + +void AutoConfigTestPage::Progress(int percentage) +{ + ui->progressBar->setValue(percentage); +} + +AutoConfigTestPage::AutoConfigTestPage(QWidget *parent) + : QWizardPage (parent), + ui (new Ui_AutoConfigTestPage) +{ + ui->setupUi(this); + setTitle(QTStr("Basic.AutoConfig.TestPage")); + setSubTitle(QTStr(SUBTITLE_TESTING)); + setCommitPage(true); +} + +AutoConfigTestPage::~AutoConfigTestPage() +{ + delete ui; + + if (testThread.joinable()) { + { + unique_lock ul(m); + cancel = true; + cv.notify_one(); + } + testThread.join(); + } + + if (started) + blog(LOG_INFO, STOPPING_SEPARATOR); +} + +void AutoConfigTestPage::initializePage() +{ + setSubTitle(QTStr(SUBTITLE_TESTING)); + stage = Stage::Starting; + softwareTested = false; + cancel = false; + DeleteLayout(results); + results = new QFormLayout(); + results->setContentsMargins(0, 0, 0, 0); + ui->finishPageLayout->insertLayout(1, results); + ui->stackedWidget->setCurrentIndex(0); + NextStage(); +} + +void AutoConfigTestPage::cleanupPage() +{ + if (testThread.joinable()) { + { + unique_lock ul(m); + cancel = true; + cv.notify_one(); + } + testThread.join(); + } +} + +bool AutoConfigTestPage::isComplete() const +{ + return stage == Stage::Finished; +} + +int AutoConfigTestPage::nextId() const +{ + return -1; +} diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp new file mode 100644 index 000000000..f04c80775 --- /dev/null +++ b/UI/window-basic-auto-config.cpp @@ -0,0 +1,795 @@ +#include "window-basic-auto-config.hpp" +#include "window-basic-main.hpp" +#include "qt-wrappers.hpp" +#include "obs-app.hpp" + +#include +#include + +#include + +#include "ui_AutoConfigStartPage.h" +#include "ui_AutoConfigVideoPage.h" +#include "ui_AutoConfigStreamPage.h" + +#define wiz reinterpret_cast(wizard()) + +/* ------------------------------------------------------------------------- */ + +#define SERVICE_PATH "service.json" + +static OBSData OpenServiceSettings(std::string &type) +{ + char serviceJsonPath[512]; + int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), + SERVICE_PATH); + if (ret <= 0) + return OBSData(); + + OBSData data = obs_data_create_from_json_file_safe(serviceJsonPath, + "bak"); + obs_data_release(data); + + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSData settings = obs_data_get_obj(data, "settings"); + obs_data_release(settings); + + return settings; +} + +static void GetServiceInfo(std::string &type, std::string &service, + std::string &server, std::string &key) +{ + OBSData settings = OpenServiceSettings(type); + + service = obs_data_get_string(settings, "service"); + server = obs_data_get_string(settings, "server"); + key = obs_data_get_string(settings, "key"); +} + +/* ------------------------------------------------------------------------- */ + +AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) + : QWizardPage (parent), + ui (new Ui_AutoConfigStartPage) +{ + ui->setupUi(this); + setTitle(QTStr("Basic.AutoConfig.StartPage")); + setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle")); +} + +AutoConfigStartPage::~AutoConfigStartPage() +{ + delete ui; +} + +int AutoConfigStartPage::nextId() const +{ + return AutoConfig::VideoPage; +} + +void AutoConfigStartPage::on_prioritizeStreaming_clicked() +{ + wiz->type = AutoConfig::Type::Streaming; +} + +void AutoConfigStartPage::on_prioritizeRecording_clicked() +{ + wiz->type = AutoConfig::Type::Recording; +} + +/* ------------------------------------------------------------------------- */ + +#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x +#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent") +#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display") +#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent") +#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS") +#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes") + +AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) + : QWizardPage (parent), + ui (new Ui_AutoConfigVideoPage) +{ + ui->setupUi(this); + + setTitle(QTStr("Basic.AutoConfig.VideoPage")); + setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle")); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + long double fpsVal = + (long double)ovi.fps_num / (long double)ovi.fps_den; + + QString fpsStr = (ovi.fps_den > 1) + ? QString::number(fpsVal, 'f', 2) + : QString::number(fpsVal, 'g', 2); + + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), + (int)AutoConfig::FPSType::PreferHighFPS); + ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), + (int)AutoConfig::FPSType::PreferHighRes); + ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), + (int)AutoConfig::FPSType::UseCurrent); + ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30); + ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60); + ui->fps->setCurrentIndex(0); + + QString cxStr = QString::number(ovi.base_width); + QString cyStr = QString::number(ovi.base_height); + + int encRes = int(ovi.base_width << 16) | int(ovi.base_height); + ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), + (int)encRes); + + QList screens = QGuiApplication::screens(); + for (int i = 0; i < screens.size(); i++) { + QScreen *screen = screens[i]; + QSize as = screen->size(); + + encRes = int(as.width() << 16) | int(as.height()); + + QString str = QTStr(RES_USE_DISPLAY) + .arg(QString::number(i + 1), + QString::number(as.width()), + QString::number(as.height())); + + ui->canvasRes->addItem(str, encRes); + } + + auto addRes = [&] (int cx, int cy) + { + encRes = (cx << 16) | cy; + QString str = QString("%1x%2").arg( + QString::number(cx), + QString::number(cy)); + ui->canvasRes->addItem(str, encRes); + }; + + addRes(1920, 1080); + addRes(1280, 720); + + ui->canvasRes->setCurrentIndex(0); +} + +AutoConfigVideoPage::~AutoConfigVideoPage() +{ + delete ui; +} + +int AutoConfigVideoPage::nextId() const +{ + return wiz->type == AutoConfig::Type::Recording + ? AutoConfig::TestPage + : AutoConfig::StreamPage; +} + +bool AutoConfigVideoPage::validatePage() +{ + int encRes = ui->canvasRes->currentData().toInt(); + wiz->baseResolutionCX = encRes >> 16; + wiz->baseResolutionCY = encRes & 0xFFFF; + wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt(); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + switch (wiz->fpsType) { + case AutoConfig::FPSType::PreferHighFPS: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = true; + break; + case AutoConfig::FPSType::PreferHighRes: + wiz->specificFPSNum = 0; + wiz->specificFPSDen = 0; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::UseCurrent: + wiz->specificFPSNum = ovi.fps_num; + wiz->specificFPSDen = ovi.fps_den; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps30: + wiz->specificFPSNum = 30; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + case AutoConfig::FPSType::fps60: + wiz->specificFPSNum = 60; + wiz->specificFPSDen = 1; + wiz->preferHighFPS = false; + break; + } + + return true; +} + +/* ------------------------------------------------------------------------- */ + +AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) + : QWizardPage (parent), + ui (new Ui_AutoConfigStreamPage) +{ + ui->setupUi(this); + ui->bitrateLabel->setVisible(false); + ui->bitrate->setVisible(false); + + ui->streamType->addItem(obs_service_get_display_name("rtmp_common")); + ui->streamType->addItem(obs_service_get_display_name("rtmp_custom")); + + setTitle(QTStr("Basic.AutoConfig.StreamPage")); + setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle")); + + LoadServices(false); + + connect(ui->streamType, SIGNAL(currentIndexChanged(int)), + this, SLOT(ServiceChanged())); + connect(ui->service, SIGNAL(currentIndexChanged(int)), + this, SLOT(ServiceChanged())); + connect(ui->customServer, SIGNAL(textChanged(const QString &)), + this, SLOT(ServiceChanged())); + connect(ui->doBandwidthTest, SIGNAL(toggled(bool)), + this, SLOT(ServiceChanged())); + + connect(ui->service, SIGNAL(currentIndexChanged(int)), + this, SLOT(UpdateServerList())); + + connect(ui->streamType, SIGNAL(currentIndexChanged(int)), + this, SLOT(UpdateKeyLink())); + connect(ui->service, SIGNAL(currentIndexChanged(int)), + this, SLOT(UpdateKeyLink())); + + connect(ui->key, SIGNAL(textChanged(const QString &)), + this, SLOT(UpdateCompleted())); + connect(ui->regionUS, SIGNAL(toggled(bool)), + this, SLOT(UpdateCompleted())); + connect(ui->regionEU, SIGNAL(toggled(bool)), + this, SLOT(UpdateCompleted())); + connect(ui->regionAsia, SIGNAL(toggled(bool)), + this, SLOT(UpdateCompleted())); + connect(ui->regionOther, SIGNAL(toggled(bool)), + this, SLOT(UpdateCompleted())); +} + +AutoConfigStreamPage::~AutoConfigStreamPage() +{ + delete ui; +} + +bool AutoConfigStreamPage::isComplete() const +{ + return ready; +} + +int AutoConfigStreamPage::nextId() const +{ + return AutoConfig::TestPage; +} + +bool AutoConfigStreamPage::validatePage() +{ + OBSData service_settings = obs_data_create(); + obs_data_release(service_settings); + obs_data_set_string(service_settings, "service", + QT_TO_UTF8(ui->service->currentText())); + + OBSService service = obs_service_create("rtmp_common", "temp_service", + service_settings, nullptr); + obs_service_release(service); + + wiz->customServer = ui->streamType->currentIndex() == 1; + + int bitrate = 6000; + if (!ui->doBandwidthTest->isChecked()) { + bitrate = ui->bitrate->value(); + wiz->idealBitrate = bitrate; + } + + OBSData settings = obs_data_create(); + obs_data_release(settings); + obs_data_set_int(settings, "bitrate", bitrate); + obs_service_apply_encoder_settings(service, settings, nullptr); + + if (wiz->customServer) { + QString server = ui->customServer->text(); + wiz->server = wiz->serverName = QT_TO_UTF8(server); + } else { + wiz->serverName = QT_TO_UTF8(ui->server->currentText()); + wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); + } + + wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); + wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); + wiz->idealBitrate = wiz->startingBitrate; + wiz->regionUS = ui->regionUS->isChecked(); + wiz->regionEU = ui->regionEU->isChecked(); + wiz->regionAsia = ui->regionAsia->isChecked(); + wiz->regionOther = ui->regionOther->isChecked(); + wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); + if (ui->preferHardware) + wiz->preferHardware = ui->preferHardware->isChecked(); + wiz->key = QT_TO_UTF8(ui->key->text()); + + if (!wiz->customServer) { + if (wiz->serviceName == "Twitch") + wiz->service = AutoConfig::Service::Twitch; + else if (wiz->serviceName == "hitbox.tv") + wiz->service = AutoConfig::Service::Hitbox; + else if (wiz->serviceName == "beam.pro") + wiz->service = AutoConfig::Service::Beam; + else + wiz->service = AutoConfig::Service::Other; + } else { + wiz->service = AutoConfig::Service::Other; + } + + if (wiz->service != AutoConfig::Service::Twitch && wiz->bandwidthTest) { + QMessageBox::StandardButton button; +#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) + button = QMessageBox::question(this, + WARNING_TEXT("Title"), + WARNING_TEXT("Text")); +#undef WARNING_TEXT + + if (button == QMessageBox::No) + return false; + } + + return true; +} + +void AutoConfigStreamPage::on_show_clicked() +{ + if (ui->key->echoMode() == QLineEdit::Password) { + ui->key->setEchoMode(QLineEdit::Normal); + ui->show->setText(QTStr("Hide")); + } else { + ui->key->setEchoMode(QLineEdit::Password); + ui->show->setText(QTStr("Show")); + } +} + +void AutoConfigStreamPage::ServiceChanged() +{ + bool showMore = ui->service->currentData().toBool(); + if (showMore) + return; + + std::string service = QT_TO_UTF8(ui->service->currentText()); + bool regionBased = service == "Twitch" || + service == "hitbox.tv" || + service == "beam.pro"; + bool testBandwidth = ui->doBandwidthTest->isChecked(); + bool custom = ui->streamType->currentIndex() == 1; + + ui->service->setVisible(!custom); + ui->serviceLabel->setVisible(!custom); + + ui->formLayout->removeWidget(ui->serviceLabel); + ui->formLayout->removeWidget(ui->service); + + ui->formLayout->removeWidget(ui->serverLabel); + ui->formLayout->removeWidget(ui->serverStackedWidget); + + if (custom) { + ui->formLayout->insertRow(1, ui->serverLabel, + ui->serverStackedWidget); + + ui->region->setVisible(false); + ui->serverStackedWidget->setCurrentIndex(1); + ui->serverStackedWidget->setVisible(true); + ui->serverLabel->setVisible(true); + } else { + ui->formLayout->insertRow(1, ui->serviceLabel, ui->service); + + if (!testBandwidth) + ui->formLayout->insertRow(2, ui->serverLabel, + ui->serverStackedWidget); + + ui->region->setVisible(regionBased && testBandwidth); + ui->serverStackedWidget->setCurrentIndex(0); + ui->serverStackedWidget->setHidden(testBandwidth); + ui->serverLabel->setHidden(testBandwidth); + } + + wiz->testRegions = regionBased && testBandwidth; + + ui->bitrateLabel->setHidden(testBandwidth); + ui->bitrate->setHidden(testBandwidth); + + UpdateCompleted(); +} + +void AutoConfigStreamPage::UpdateKeyLink() +{ + bool custom = ui->streamType->currentIndex() == 1; + QString serviceName = ui->service->currentText(); + + if (custom) + serviceName = ""; + + QString text = QTStr("Basic.AutoConfig.StreamPage.StreamKey"); + if (serviceName == "Twitch") { + text += " "; + text += QTStr("Basic.AutoConfig.StreamPage.StreamKey.LinkToSite"); + text += ""; + } else if (serviceName == "YouTube / YouTube Gaming") { + text += " "; + text += QTStr("Basic.AutoConfig.StreamPage.StreamKey.LinkToSite"); + text += ""; + } + + ui->streamKeyLabel->setText(text); +} + +void AutoConfigStreamPage::LoadServices(bool showAll) +{ + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + + OBSData settings = obs_data_create(); + obs_data_release(settings); + + obs_data_set_bool(settings, "show_all", showAll); + + obs_property_t *prop = obs_properties_get(props, "show_all"); + obs_property_modified(prop, settings); + + ui->service->blockSignals(true); + ui->service->clear(); + + QStringList names; + + obs_property_t *services = obs_properties_get(props, "service"); + size_t services_count = obs_property_list_item_count(services); + for (size_t i = 0; i < services_count; i++) { + const char *name = obs_property_list_item_string(services, i); + names.push_back(name); + } + + if (showAll) + names.sort(); + + for (QString &name : names) + ui->service->addItem(name); + + if (!lastService.isEmpty()) { + int idx = ui->service->findText(lastService); + if (idx != -1) + ui->service->setCurrentIndex(idx); + } + + if (!showAll) { + ui->service->addItem( + QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), + QVariant(true)); + } + + obs_properties_destroy(props); + + ui->service->blockSignals(false); +} + +void AutoConfigStreamPage::UpdateServerList() +{ + QString serviceName = ui->service->currentText(); + bool showMore = ui->service->currentData().toBool(); + + if (showMore) { + LoadServices(true); + ui->service->showPopup(); + return; + } else { + lastService = serviceName; + } + + obs_properties_t *props = obs_get_service_properties("rtmp_common"); + obs_property_t *services = obs_properties_get(props, "service"); + + OBSData settings = obs_data_create(); + obs_data_release(settings); + + obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); + obs_property_modified(services, settings); + + obs_property_t *servers = obs_properties_get(props, "server"); + + ui->server->clear(); + + size_t servers_count = obs_property_list_item_count(servers); + for (size_t i = 0; i < servers_count; i++) { + const char *name = obs_property_list_item_name(servers, i); + const char *server = obs_property_list_item_string(servers, i); + ui->server->addItem(name, server); + } + + obs_properties_destroy(props); +} + +void AutoConfigStreamPage::UpdateCompleted() +{ + if (ui->key->text().isEmpty()) { + ready = false; + } else { + bool custom = ui->streamType->currentIndex() == 1; + if (custom) { + ready = !ui->customServer->text().isEmpty(); + } else { + ready = !wiz->testRegions || + ui->regionUS->isChecked() || + ui->regionEU->isChecked() || + ui->regionAsia->isChecked() || + ui->regionOther->isChecked(); + } + } + emit completeChanged(); +} + +/* ------------------------------------------------------------------------- */ + +AutoConfig::AutoConfig(QWidget *parent) + : QWizard(parent) +{ + OBSBasic *main = reinterpret_cast(parent); + main->EnableOutputs(false); + + installEventFilter(CreateShortcutFilter()); + + std::string serviceType; + GetServiceInfo(serviceType, serviceName, server, key); +#ifdef _WIN32 + setWizardStyle(QWizard::ModernStyle); +#endif + AutoConfigStreamPage *streamPage = new AutoConfigStreamPage(); + + setPage(StartPage, new AutoConfigStartPage()); + setPage(VideoPage, new AutoConfigVideoPage()); + setPage(StreamPage, streamPage); + setPage(TestPage, new AutoConfigTestPage()); + setWindowTitle(QTStr("Basic.AutoConfig.Beta")); + + obs_video_info ovi; + obs_get_video_info(&ovi); + + baseResolutionCX = ovi.base_width; + baseResolutionCY = ovi.base_height; + + /* ----------------------------------------- */ + /* load service/servers */ + + customServer = serviceType == "rtmp_custom"; + + QComboBox *serviceList = streamPage->ui->service; + + if (!serviceName.empty()) { + serviceList->blockSignals(true); + + int count = serviceList->count(); + bool found = false; + + for (int i = 0; i < count; i++) { + QString name = serviceList->itemText(i); + + if (name == serviceName.c_str()) { + serviceList->setCurrentIndex(i); + found = true; + break; + } + } + + if (!found) { + serviceList->insertItem(0, serviceName.c_str()); + serviceList->setCurrentIndex(0); + } + + serviceList->blockSignals(false); + } + + streamPage->UpdateServerList(); + streamPage->UpdateKeyLink(); + + if (!customServer) { + QComboBox *serverList = streamPage->ui->server; + int idx = serverList->findData(QString(server.c_str())); + if (idx == -1) + idx = 0; + + serverList->setCurrentIndex(idx); + } else { + streamPage->ui->customServer->setText(server.c_str()); + streamPage->ui->streamType->setCurrentIndex(1); + } + + if (!key.empty()) + streamPage->ui->key->setText(key.c_str()); + + int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); + streamPage->ui->bitrate->setValue(bitrate); + streamPage->ServiceChanged(); + + streamPage->ui->preferHardware->setChecked(os_get_physical_cores() <= 4); + + TestHardwareEncoding(); + if (!hardwareEncodingAvailable) { + delete streamPage->ui->preferHardware; + streamPage->ui->preferHardware = nullptr; + } + + setOptions(0); + setButtonText(QWizard::FinishButton, + QTStr("Basic.AutoConfig.ApplySettings")); +} + +AutoConfig::~AutoConfig() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + main->EnableOutputs(true); +} + +void AutoConfig::TestHardwareEncoding() +{ + size_t idx = 0; + const char *id; + while (obs_enum_encoder_types(idx++, &id)) { + if (strcmp(id, "ffmpeg_nvenc") == 0) + hardwareEncodingAvailable = nvencAvailable = true; + else if (strcmp(id, "obs_qsv11") == 0) + hardwareEncodingAvailable = qsvAvailable = true; + else if (strcmp(id, "amd_amf_h264") == 0) + hardwareEncodingAvailable = vceAvailable = true; + } +} + +bool AutoConfig::CanTestServer(const char *server) +{ + if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) + return true; + + if (service == Service::Twitch) { + if (astrcmp_n(server, "US West:", 8) == 0 || + astrcmp_n(server, "US East:", 8) == 0 || + astrcmp_n(server, "US Central:", 11) == 0) { + return regionUS; + } else if (astrcmp_n(server, "EU:", 3) == 0) { + return regionEU; + } else if (astrcmp_n(server, "Asia:", 5) == 0) { + return regionAsia; + } else if (regionOther) { + return true; + } + } else if (service == Service::Hitbox) { + if (strcmp(server, "Default") == 0) { + return true; + } else if (astrcmp_n(server, "US-West:", 8) == 0 || + astrcmp_n(server, "US-East:", 8) == 0) { + return regionUS; + } else if (astrcmp_n(server, "EU-", 3) == 0) { + return regionEU; + } else if (astrcmp_n(server, "South Korea:", 12) == 0 || + astrcmp_n(server, "Asia:", 5) == 0 || + astrcmp_n(server, "China:", 6) == 0) { + return regionAsia; + } else if (regionOther) { + return true; + } + } else if (service == Service::Beam) { + if (astrcmp_n(server, "US:", 3) == 0) { + return regionUS; + } else if (astrcmp_n(server, "EU:", 3) == 0) { + return regionEU; + } else if (astrcmp_n(server, "South Korea:", 12) == 0 || + astrcmp_n(server, "Asia:", 5) == 0) { + return regionAsia; + } else if (regionOther) { + return true; + } + } else { + return true; + } + + return false; +} + +void AutoConfig::done(int result) +{ + QWizard::done(result); + + if (result == QDialog::Accepted) { + if (type == Type::Streaming) + SaveStreamSettings(); + SaveSettings(); + } +} + +inline const char *AutoConfig::GetEncoderId(Encoder enc) +{ + switch (enc) { + case Encoder::NVENC: + return SIMPLE_ENCODER_NVENC; + case Encoder::QSV: + return SIMPLE_ENCODER_QSV; + case Encoder::AMD: + return SIMPLE_ENCODER_AMD; + default: + return SIMPLE_ENCODER_X264; + } +}; + +void AutoConfig::SaveStreamSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + /* ---------------------------------- */ + /* save service */ + + const char *service_id = customServer + ? "rtmp_custom" + : "rtmp_common"; + + obs_service_t *oldService = main->GetService(); + OBSData hotkeyData = obs_hotkeys_save_service(oldService); + obs_data_release(hotkeyData); + + OBSData settings = obs_data_create(); + obs_data_release(settings); + + if (!customServer) + obs_data_set_string(settings, "service", serviceName.c_str()); + obs_data_set_string(settings, "server", server.c_str()); + obs_data_set_string(settings, "key", key.c_str()); + + OBSService newService = obs_service_create(service_id, + "default_service", settings, hotkeyData); + obs_service_release(newService); + + if (!newService) + return; + + main->SetService(newService); + main->SaveService(); + + /* ---------------------------------- */ + /* save stream settings */ + + config_set_int(main->Config(), "SimpleOutput", "VBitrate", + idealBitrate); + config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", + GetEncoderId(streamingEncoder)); +} + +void AutoConfig::SaveSettings() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + if (recordingEncoder != Encoder::Stream) + config_set_string(main->Config(), "SimpleOutput", "RecEncoder", + GetEncoderId(recordingEncoder)); + + const char *quality = recordingQuality == Quality::High + ? "Small" + : "Stream"; + + config_set_string(main->Config(), "Output", "Mode", "Simple"); + config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality); + config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX); + config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY); + config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX); + config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY); + + if (fpsType != FPSType::UseCurrent) { + config_set_uint(main->Config(), "Video", "FPSType", 0); + config_set_string(main->Config(), "Video", "FPSCommon", + std::to_string(idealFPSNum).c_str()); + } + + main->ResetVideo(); + main->ResetOutputs(); + config_save_safe(main->Config(), "tmp", nullptr); +} diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp new file mode 100644 index 000000000..ee856c4c3 --- /dev/null +++ b/UI/window-basic-auto-config.hpp @@ -0,0 +1,248 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +class Ui_AutoConfigStartPage; +class Ui_AutoConfigVideoPage; +class Ui_AutoConfigStreamPage; +class Ui_AutoConfigTestPage; + +class AutoConfig : public QWizard { + Q_OBJECT + + friend class AutoConfigStartPage; + friend class AutoConfigVideoPage; + friend class AutoConfigStreamPage; + friend class AutoConfigTestPage; + + enum class Type { + Invalid, + Streaming, + Recording + }; + + enum class Service { + Twitch, + Hitbox, + Beam, + Other + }; + + enum class Encoder { + x264, + NVENC, + QSV, + AMD, + Stream + }; + + enum class Quality { + Stream, + High + }; + + enum class FPSType : int { + PreferHighFPS, + PreferHighRes, + UseCurrent, + fps30, + fps60 + }; + + static inline const char *GetEncoderId(Encoder enc); + + Service service = Service::Other; + Quality recordingQuality = Quality::Stream; + Encoder recordingEncoder = Encoder::Stream; + Encoder streamingEncoder = Encoder::x264; + Type type = Type::Invalid; + FPSType fpsType = FPSType::PreferHighFPS; + int idealBitrate = 2500; + int baseResolutionCX = 1920; + int baseResolutionCY = 1080; + int idealResolutionCX = 1280; + int idealResolutionCY = 720; + int idealFPSNum = 60; + int idealFPSDen = 1; + std::string serviceName; + std::string serverName; + std::string server; + std::string key; + + bool hardwareEncodingAvailable = false; + bool nvencAvailable = false; + bool qsvAvailable = false; + bool vceAvailable = false; + + int startingBitrate = 2500; + bool customServer = false; + bool bandwidthTest = false; + bool testRegions = true; + bool regionUS = true; + bool regionEU = true; + bool regionAsia = true; + bool regionOther = true; + bool preferHighFPS = false; + bool preferHardware = false; + int specificFPSNum = 0; + int specificFPSDen = 0; + + void TestHardwareEncoding(); + bool CanTestServer(const char *server); + + virtual void done(int result) override; + + void SaveStreamSettings(); + void SaveSettings(); + +public: + AutoConfig(QWidget *parent); + ~AutoConfig(); + + enum Page { + StartPage, + VideoPage, + StreamPage, + TestPage + }; +}; + +class AutoConfigStartPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + Ui_AutoConfigStartPage *ui; + +public: + AutoConfigStartPage(QWidget *parent = nullptr); + ~AutoConfigStartPage(); + + virtual int nextId() const override; + +public slots: + void on_prioritizeStreaming_clicked(); + void on_prioritizeRecording_clicked(); +}; + +class AutoConfigVideoPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + Ui_AutoConfigVideoPage *ui; + +public: + AutoConfigVideoPage(QWidget *parent = nullptr); + ~AutoConfigVideoPage(); + + virtual int nextId() const override; + virtual bool validatePage() override; +}; + +class AutoConfigStreamPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + Ui_AutoConfigStreamPage *ui; + QString lastService; + bool ready = false; + + void LoadServices(bool showAll); + +public: + AutoConfigStreamPage(QWidget *parent = nullptr); + ~AutoConfigStreamPage(); + + virtual bool isComplete() const override; + virtual int nextId() const override; + virtual bool validatePage() override; + +public slots: + void on_show_clicked(); + void ServiceChanged(); + void UpdateKeyLink(); + void UpdateServerList(); + void UpdateCompleted(); +}; + +class AutoConfigTestPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + QPointer results; + + Ui_AutoConfigTestPage *ui; + std::thread testThread; + std::condition_variable cv; + std::mutex m; + bool cancel = false; + bool started = false; + + enum class Stage { + Starting, + BandwidthTest, + StreamEncoder, + RecordingEncoder, + Finished + }; + + Stage stage = Stage::Starting; + bool softwareTested = false; + + void StartBandwidthStage(); + void StartStreamEncoderStage(); + void StartRecordingEncoderStage(); + + void FindIdealHardwareResolution(); + bool TestSoftwareEncoding(); + + void TestBandwidthThread(); + void TestStreamEncoderThread(); + void TestRecordingEncoderThread(); + + void FinalizeResults(); + + struct ServerInfo { + std::string name; + std::string address; + int bitrate = 0; + int ms = -1; + + inline ServerInfo() {} + + inline ServerInfo(const char *name_, const char *address_) + : name(name_), address(address_) + { + } + }; + + void GetServers(std::vector &servers); + +public: + AutoConfigTestPage(QWidget *parent = nullptr); + ~AutoConfigTestPage(); + + virtual void initializePage() override; + virtual void cleanupPage() override; + virtual bool isComplete() const override; + virtual int nextId() const override; + +public slots: + void NextStage(); + void UpdateMessage(QString message); + void Failure(QString message); + void Progress(int percentage); +}; diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 3264e94ca..1229d1e48 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -40,6 +40,7 @@ #include "item-widget-helpers.hpp" #include "window-basic-settings.hpp" #include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" #include "window-basic-source-select.hpp" #include "window-basic-main.hpp" #include "window-basic-main-outputs.hpp" @@ -1422,6 +1423,36 @@ void OBSBasic::OBSInit() SystemTray(true); OpenSavedProjectors(); + + bool has_last_version = config_has_user_value(App()->GlobalConfig(), + "General", "LastVersion"); + bool first_run = config_get_bool(App()->GlobalConfig(), "General", + "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GlobalConfig(), "General", "FirstRun", + true); + config_save_safe(App()->GlobalConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) { + QString msg; + msg = QTStr("Basic.FirstStartup.RunWizard"); + msg += "\n\n"; + msg += QTStr("Basic.FirstStartup.RunWizard.BetaWarning"); + + QMessageBox::StandardButton button = + QMessageBox::question(this, QTStr("Basic.AutoConfig"), + msg); + + if (button == QMessageBox::Yes) { + on_autoConfigure_triggered(); + } else { + msg = QTStr("Basic.FirstStartup.RunWizard.NoClicked"); + QMessageBox::information(this, + QTStr("Basic.AutoConfig"), msg); + } + } } void OBSBasic::InitHotkeys() @@ -3976,6 +4007,7 @@ inline void OBSBasic::OnActivate() { if (ui->profileMenu->isEnabled()) { ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); App()->IncrementSleepInhibition(); UpdateProcessPriority(); @@ -3988,6 +4020,7 @@ inline void OBSBasic::OnDeactivate() { if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); App()->DecrementSleepInhibition(); ClearProcessPriority(); @@ -5445,3 +5478,11 @@ void OBSBasic::on_actionPasteFilters_triggered() obs_source_copy_filters(dstSource, source); } + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 7177f59bf..808056529 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -613,6 +613,8 @@ private slots: void on_modeSwitch_clicked(); + void on_autoConfigure_triggered(); + void logUploadFinished(const QString &text, const QString &error); void updateCheckFinished();