diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index df4d264500419c..ba08f87f565fe3 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -11,6 +11,7 @@ if(BROWSER_AVAILABLE_INTERNAL) add_definitions(-DBROWSER_AVAILABLE) endif() +add_subdirectory(pre-stream-wizard) add_subdirectory(obs-frontend-api) # ---------------------------------------------------------------------------- @@ -69,6 +70,7 @@ endif() include_directories(${FFMPEG_INCLUDE_DIRS}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(SYSTEM "obs-frontend-api") +include_directories(SYSTEM "pre-stream-wizard") include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs") include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/deps/libff") include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/deps/json11") @@ -188,6 +190,16 @@ set(obs_SOURCES ${obs_PLATFORM_SOURCES} ${obs_libffutil_SOURCES} ../deps/json11/json11.cpp + pre-stream-wizard/pre-stream-current-settings.cpp + pre-stream-wizard/pre-stream-wizard.cpp + pre-stream-wizard/page-input-display.cpp + pre-stream-wizard/page-start-prompt.cpp + pre-stream-wizard/page-select-settings.cpp + pre-stream-wizard/page-loading.cpp + pre-stream-wizard/page-completed.cpp + pre-stream-wizard/page-error.cpp + pre-stream-wizard/setting-selection-row.cpp + pre-stream-wizard/encoder-settings-provider-facebook.cpp obs-app.cpp window-dock.cpp api-interface.cpp @@ -220,6 +232,8 @@ set(obs_SOURCES window-log-reply.cpp window-projector.cpp window-remux.cpp + common-settings.cpp + streaming-settings-util.cpp auth-base.cpp source-tree.cpp scene-tree.cpp @@ -258,6 +272,16 @@ set(obs_HEADERS ${obs_PLATFORM_HEADERS} ${obs_libffutil_HEADERS} ../deps/json11/json11.hpp + pre-stream-wizard/pre-stream-current-settings.hpp + pre-stream-wizard/pre-stream-wizard.hpp + pre-stream-wizard/page-input-display.hpp + pre-stream-wizard/page-start-prompt.hpp + pre-stream-wizard/page-select-settings.hpp + pre-stream-wizard/page-loading.hpp + pre-stream-wizard/page-completed.hpp + pre-stream-wizard/page-error.hpp + pre-stream-wizard/setting-selection-row.hpp + pre-stream-wizard/encoder-settings-provider-facebook.hpp obs-app.hpp platform.hpp window-dock.hpp @@ -282,6 +306,8 @@ set(obs_HEADERS window-log-reply.hpp window-projector.hpp window-remux.hpp + common-settings.hpp + streaming-settings-util.hpp auth-base.hpp source-tree.hpp scene-tree.hpp diff --git a/UI/common-settings.cpp b/UI/common-settings.cpp new file mode 100644 index 00000000000000..444107c1fc5cc4 --- /dev/null +++ b/UI/common-settings.cpp @@ -0,0 +1,184 @@ +#include "common-settings.hpp" + +#include "audio-encoders.hpp" + +bool CommonSettings::IsAdvancedMode(config_t *config) +{ + const char *outputMode = config_get_string(config, "Output", "Mode"); + return (strcmp(outputMode, "Advanced") == 0); +} + +OBSData CommonSettings::GetDataFromJsonFile(const char *jsonFile) +{ + char fullPath[512]; + obs_data_t *data = nullptr; + + int ret = GetProfilePath(fullPath, sizeof(fullPath), jsonFile); + if (ret > 0) { + BPtr jsonData = os_quick_read_utf8_file(fullPath); + if (jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) + data = obs_data_create(); + + OBSData dataRet(data); + obs_data_release(data); + return dataRet; +} + +void CommonSettings::GetConfigFPS(config_t *config, uint32_t &num, + uint32_t &den) +{ + uint32_t type = config_get_uint(config, videoSection, "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(config, num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(config, num, den); + else if (false) //"Nanoseconds", currently not implemented + GetFPSNanoseconds(config, num, den); + else + GetFPSCommon(config, num, den); +} + +double CommonSettings::GetConfigFPSDouble(config_t *config) +{ + uint32_t num = 0; + uint32_t den = 0; + CommonSettings::GetConfigFPS(config, num, den); + return (double)num / (double)den; +} + +void CommonSettings::GetFPSCommon(config_t *config, uint32_t &num, + uint32_t &den) +{ + const char *val = config_get_string(config, videoSection, "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void CommonSettings::GetFPSInteger(config_t *config, uint32_t &num, + uint32_t &den) +{ + num = (uint32_t)config_get_uint(config, videoSection, "FPSInt"); + den = 1; +} + +void CommonSettings::GetFPSFraction(config_t *config, uint32_t &num, + uint32_t &den) +{ + num = (uint32_t)config_get_uint(config, videoSection, "FPSNum"); + den = (uint32_t)config_get_uint(config, videoSection, "FPSDen"); +} + +void CommonSettings::GetFPSNanoseconds(config_t *config, uint32_t &num, + uint32_t &den) +{ + num = 1000000000; + den = (uint32_t)config_get_uint(config, videoSection, "FPSNS"); +} + +int CommonSettings::GetAudioChannelCount(config_t *config) +{ + const char *channelSetup = + config_get_string(config, "Audio", "ChannelSetup"); + + if (strcmp(channelSetup, "Mono") == 0) + return 1; + if (strcmp(channelSetup, "Stereo") == 0) + return 2; + if (strcmp(channelSetup, "2.1") == 0) + return 3; + if (strcmp(channelSetup, "4.0") == 0) + return 4; + if (strcmp(channelSetup, "4.1") == 0) + return 5; + if (strcmp(channelSetup, "5.1") == 0) + return 6; + if (strcmp(channelSetup, "7.1") == 0) + return 8; + + return 2; +} + +int CommonSettings::GetStreamingAudioBitrate(config_t *config) +{ + if (IsAdvancedMode(config)) { + return GetAdvancedAudioBitrate(config); + } + return GetSimpleAudioBitrate(config); +} + +int CommonSettings::GetSimpleAudioBitrate(config_t *config) +{ + int bitrate = config_get_uint(config, "SimpleOutput", "ABitrate"); + return FindClosestAvailableAACBitrate(bitrate); +} + +int CommonSettings::GetAdvancedAudioBitrate(config_t *config) +{ + int track = config_get_int(config, "AdvOut", "TrackIndex"); + return GetAdvancedAudioBitrateForTrack(config, track - 1); +} + +int CommonSettings::GetAdvancedAudioBitrateForTrack(config_t *config, + int trackIndex) +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", + "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + + // Sanity check for out of bounds, clamp to bounds + if (trackIndex > 5) { + trackIndex = 5; + } else if (trackIndex < 0) { + trackIndex = 0; + } + + int bitrate = (int)config_get_uint(config, "AdvOut", names[trackIndex]); + return FindClosestAvailableAACBitrate(bitrate); +} + +int CommonSettings::GetVideoBitrateInUse(config_t *config) +{ + if (!IsAdvancedMode(config)) { + return config_get_int(config, "SimpleOutput", "VBitrate"); + } + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + return obs_data_get_int(streamEncSettings, "bitrate"); +} diff --git a/UI/common-settings.hpp b/UI/common-settings.hpp new file mode 100644 index 00000000000000..beeecf5514ed37 --- /dev/null +++ b/UI/common-settings.hpp @@ -0,0 +1,49 @@ +/* + Helper to get common video and audio setting that are accessed from more than + one file. +*/ +#pragma once + +#include +#include "obs-app.hpp" + +class CommonSettings { + +public: + static bool IsAdvancedMode(config_t *config); + /* Shared Utility Functions --------------------------*/ + static OBSData GetDataFromJsonFile(const char *jsonFile); + + /* Framerate ----------------------------------------*/ + static void GetConfigFPS(config_t *config, uint32_t &num, + uint32_t &den); + static double GetConfigFPSDouble(config_t *config); + + /* Audio Data ---------------------------------------*/ + // Returns int of audio, sub (the .1 in 2.1) is a channel i.e., 2.1 -> 3ch + static int GetAudioChannelCount(config_t *config); + // Gets streaming track's bitrate, simple or advanced mode + static int GetStreamingAudioBitrate(config_t *config); + static int GetSimpleAudioBitrate(config_t *config); + static int GetAdvancedAudioBitrate(config_t *config); + // Advanced setting's streaming bitrate for track number (starts at 1) + static int GetAdvancedAudioBitrateForTrack(config_t *config, + int trackIndex); + + /* Stream Encoder ——————————————————————————————————*/ + static int GetVideoBitrateInUse(config_t *config); + static void SetAllVideoBitrates(config_t *config, int newBitrate); + +private: + // Reused Strings + static constexpr const char *videoSection = "Video"; + + static void GetFPSCommon(config_t *config, uint32_t &num, + uint32_t &den); + static void GetFPSInteger(config_t *config, uint32_t &num, + uint32_t &den); + static void GetFPSFraction(config_t *config, uint32_t &num, + uint32_t &den); + static void GetFPSNanoseconds(config_t *config, uint32_t &num, + uint32_t &den); +}; diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 1095f5f5335b8a..b86a8a3ddd4d38 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -99,6 +99,7 @@ LogViewer="Log Viewer" ShowOnStartup="Show on startup" OpenFile="Open file" AddValue="Add %1" +Loading="Loading" # warning if program already open AlreadyRunning.Title="OBS is already running" @@ -707,6 +708,43 @@ Basic.Settings.Stream.MissingUrlAndApiKey="URL and Stream Key are missing.\n\nOp Basic.Settings.Stream.MissingUrl="Stream URL is missing.\n\nOpen settings to enter the URL in the 'Stream' tab." Basic.Settings.Stream.MissingStreamKey="Stream key is missing.\n\nOpen settings to enter the stream key in the 'Stream' tab." +## Pre-Live Wizard and Settings +Basic.Settings.Stream.PreLiveWizard.RunNow="Check stream settings" +PreLiveWizard.Title="Verify Stream Settings" +PreLiveWizard.Prompt.Title="Verify stream settings before stream" +PreLiveWizard.Prompt.Subtitle.Default="The streaming wizard suggests the most up-to-date settings to improve your stream's reliability, quality, and latency. \n\nSelect the resolution to stream to your account:" +PreLiveWizard.Prompt.Subtitle.FB="Your destination is set to Facebook Live." +PreLiveWizard.Prompt.Explainer="The Streaming wizard suggests the most up-to-date settings to improve your stream's reliability, quality, and latency." +PreLiveWizard.Prompt.ResSelectTitle="Select a resolution to stream:" +PreLiveWizard.Prompt.ResolutionHelp.FB="If you're unsure, click Open Facebook Live, then click on the \"Stream Health\" tab and scroll down to find your maximum resolution." +PreLiveWizard.Prompt.ResolutionHelpButton.FB="Open Facebook Live" +PreLiveWizard.Prompt.ResolutionHelpButton.FB.ToolTip="Open Facebook Live in your default web browser" +PreLiveWizard.Prompt.Resolution.720="720p (Recommended for general purpose)" +PreLiveWizard.Prompt.Resolution.1080="1080p (Recommended for gaming)" +PreLiveWizard.Prompt.Resolution.Current="%1x%2 (Current setting)" +PreLiveWizard.Loading.Title="Fetching updated settings" +PreLiveWizard.Configure.ServiceNotAvailable.Title="Service not supported" +PreLiveWizard.Configure.ServiceNotAvailable.Description="The service selected in stream settings does not yet have an encoder configuration service." +PreLiveWizard.Configure.Error.Url="Settings setup failed. Query: " +PreLiveWizard.Configure.Error.JsonParse="Problem with server response" +PreLiveWizard.Configure.Error.JsonParse.Description="Wizard is unavailable at the moment." +PreLiveWizard.Configure.Error.NoData="No new settings recieved." +PreLiveWizard.Configure.Error.NoData.Description=" " +PreLiveWizard.Selection.Title="Suggested Settings" +PreLiveWizard.Selection.Description="Suggested video settings for your best stream:" +PreLiveWizard.Completed.Title="Encoder configured" +PreLiveWizard.Completed.BodyText="Applied suggested settings and encoder is setup to stream. You can now close the wizard." +PreLiveWizard.Completed.FacebookOnly="To finish going live go to Facebook Live:" +PreLiveWizard.Error.Title="There was an issue verifying settings" +PreLiveWizard.Error.Subtitle="This doesn't block you from starting your stream." +PreLiveWizard.Error.Generic.Headline="Encoder service is not working right now." +PreLiveWizard.Error.Generic.BodyText="You can exit the wizard and try again." +PreLiveWizard.Error.NetworkTimeout="Check your network connection before starting a stream." +PreLiveWizard.Output.Mode.CodecProfile="Profile" +PreLiveWizard.Output.Mode.CodecLevel="Level" +PreLiveWizard.Output.Mode.RateControl="Rate Control" +PreLiveWizard.Output.Mode.StreamBuffer="Stream Buffer" + # basic mode 'output' settings Basic.Settings.Output="Output" Basic.Settings.Output.Format="Recording Format" @@ -797,6 +835,10 @@ Basic.Settings.Output.Adv.FFmpeg.AEncoder="Audio Encoder" Basic.Settings.Output.Adv.FFmpeg.AEncoderSettings="Audio Encoder Settings (if any)" Basic.Settings.Output.Adv.FFmpeg.MuxerSettings="Muxer Settings (if any)" Basic.Settings.Output.Adv.FFmpeg.GOPSize="Keyframe interval (frames)" +Basic.Settings.Output.Adv.FFmpeg.GOPType="GOP Type" +Basic.Settings.Output.Adv.FFmpeg.GOPClosed="Closed GOP" +Basic.Settings.Output.Adv.FFmpeg.BFrames="B-Frames" +Basic.Settings.Output.Adv.FFmpeg.ReferenceFrames="Reference frames" Basic.Settings.Output.Adv.FFmpeg.IgnoreCodecCompat="Show all codecs (even if potentially incompatible)" # Screenshot diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index f2b91fc4ceec5e..5c7d55cbefc12e 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -151,8 +151,8 @@ 0 0 - 806 - 1254 + 810 + 1087 @@ -1177,6 +1177,19 @@ + + + + Basic.Settings.Stream.TTVAddon + + + twitchAddonDropdown + + + + + + @@ -1232,18 +1245,29 @@ - - - - - - - Basic.Settings.Stream.TTVAddon - - - twitchAddonDropdown - - + + + + + + Basic.Settings.Stream.PreLiveWizard.RunNow + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + @@ -1281,8 +1305,8 @@ 0 0 - 813 - 761 + 740 + 767 @@ -2413,7 +2437,7 @@ 9 0 - 236 + 250 25 @@ -3776,8 +3800,8 @@ 0 0 - 767 - 582 + 691 + 601 @@ -4632,8 +4656,8 @@ 0 0 - 791 - 970 + 810 + 930 diff --git a/UI/pre-stream-wizard/CMakeLists.txt b/UI/pre-stream-wizard/CMakeLists.txt new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/UI/pre-stream-wizard/encoder-settings-provider-facebook.cpp b/UI/pre-stream-wizard/encoder-settings-provider-facebook.cpp new file mode 100644 index 00000000000000..34b29e6ff9293c --- /dev/null +++ b/UI/pre-stream-wizard/encoder-settings-provider-facebook.cpp @@ -0,0 +1,289 @@ +#include "encoder-settings-provider-facebook.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "obs-config.h" + +#include "remote-text.hpp" +#include + +namespace StreamWizard { + +FacebookEncoderSettingsProvider::FacebookEncoderSettingsProvider(QObject *parent) + : QObject(parent) +{ + currentSettings_ = nullptr; +} + +void FacebookEncoderSettingsProvider::setEncoderRequest( + QSharedPointer request) +{ + currentSettings_ = request; +} + +void FacebookEncoderSettingsProvider::run() +{ + // Base URL for request + QUrl requestUrl( + "https://graph.facebook.com/v8.0/video_encoder_settings"); + QUrlQuery inputVideoSettingsQuery = + inputVideoQueryFromCurrentSettings(); + requestUrl.setQuery(inputVideoSettingsQuery); + + if (requestUrl.isValid()) { + makeRequest(requestUrl); + } else { + emit returnErrorDescription( + QTStr("PreLiveWizard.Configure.UrlError"), + requestUrl.toDisplayString()); + } +} + +void FacebookEncoderSettingsProvider::makeRequest(QUrl &url) +{ + blog(LOG_INFO, "FacebookEncoderSettingsProvider sending request"); + + bool requestSuccess = false; + + std::string urlString = url.toString().toStdString(); + std::string reply; + std::string error; + long responseCode = 0; + const char *contentType = "application/json"; + const char *postData = nullptr; + std::vector extraHeaders = std::vector(); + int timeout = 3; // seconds + + auto apiRequestBlock = [&]() { + requestSuccess = GetRemoteFile(urlString.c_str(), reply, error, + &responseCode, contentType, + postData, extraHeaders, nullptr, + timeout); + }; + + ExecuteFuncSafeBlock(apiRequestBlock); + + if (!requestSuccess || responseCode >= 400) { + handleTimeout(); + blog(LOG_WARNING, "Server response with error: %s", + error.c_str()); + } + + if (reply.empty()) { + handleEmpty(); + blog(LOG_WARNING, "Server response was empty"); + } + + QByteArray jsonBytes = QByteArray::fromStdString(reply); + handleResponse(jsonBytes); +} + +QUrlQuery FacebookEncoderSettingsProvider::inputVideoQueryFromCurrentSettings() +{ + // Get input settings, shorten name + EncoderSettingsRequest *input = currentSettings_.data(); + + QUrlQuery inputVideoSettingsQuery; + inputVideoSettingsQuery.addQueryItem("video_type", "live"); + if (input->userSelectedResolution.isNull()) { + inputVideoSettingsQuery.addQueryItem( + "input_video_width", + QString::number(input->videoWidth)); + inputVideoSettingsQuery.addQueryItem( + "input_video_height", + QString::number(input->videoHeight)); + } else { + QSize wizardResolution = input->userSelectedResolution.toSize(); + inputVideoSettingsQuery.addQueryItem( + "input_video_width", + QString::number(wizardResolution.width())); + inputVideoSettingsQuery.addQueryItem( + "input_video_height", + QString::number(wizardResolution.height())); + } + inputVideoSettingsQuery.addQueryItem("input_video_framerate", + QString::number(input->framerate)); + inputVideoSettingsQuery.addQueryItem( + "input_video_bitrate", QString::number(input->videoBitrate)); + inputVideoSettingsQuery.addQueryItem( + "input_audio_channels", QString::number(input->audioChannels)); + inputVideoSettingsQuery.addQueryItem( + "input_audio_samplerate", + QString::number(input->audioSamplerate)); + if (input->protocol == StreamProtocol::rtmps) { + inputVideoSettingsQuery.addQueryItem("cap_streaming_protocols", + "rtmps"); + } + // Defaults in OBS + inputVideoSettingsQuery.addQueryItem("cap_video_codecs", "h264"); + inputVideoSettingsQuery.addQueryItem("cap_audio_codecs", "aac"); + return inputVideoSettingsQuery; +} + +// Helper methods for FacebookEncoderSettingsProvider::handleResponse +void addInt(const QJsonObject &json, const char *jsonKey, SettingsMap *map, + QString mapKey) +{ + if (json[jsonKey].isDouble()) { + map->insert(mapKey, + QPair(QVariant(json[jsonKey].toInt()), true)); + } else { + blog(LOG_WARNING, + "FacebookEncoderSettingsProvider could not parse %s to Int", + jsonKey); + } +} + +void addStringDouble(const QJsonObject &json, const char *jsonKey, + SettingsMap *map, QString mapKey) +{ + if (!json[jsonKey].isString()) { + return; + } + bool converted = false; + QString valueString = json[jsonKey].toString(); + double numberValue = valueString.toDouble(&converted); + if (converted) { + map->insert(mapKey, QPair(QVariant(numberValue), true)); + } else { + blog(LOG_WARNING, + "FacebookEncoderSettingsProvider couldn't parse %s to Double from String", + jsonKey); + } +} + +void addQString(const QJsonObject &json, const char *jsonKey, SettingsMap *map, + QString mapKey) +{ + if (json[jsonKey].isString()) { + map->insert(mapKey, + QPair(QVariant(json[jsonKey].toString()), true)); + } else { + blog(LOG_WARNING, + "FacebookEncoderSettingsProvider could not parse %s to Strng", + jsonKey); + } +} + +void addBool(const QJsonObject &json, const char *jsonKey, SettingsMap *map, + QString mapKey) +{ + if (json[jsonKey].isBool()) { + map->insert(mapKey, + QPair(QVariant(json[jsonKey].toBool()), true)); + } else { + blog(LOG_WARNING, + "FacebookEncoderSettingsProvider could not parse %s to Bool", + jsonKey); + } +} + +void FacebookEncoderSettingsProvider::handleResponse(QByteArray reply) +{ + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply); + + // Parse bytes to json object + if (!jsonDoc.isObject()) { + blog(LOG_WARNING, + "FacebookEncoderSettingsProvider json doc not an object as expected"); + jsonParseError(); + return; + } + QJsonObject responseObject = jsonDoc.object(); + + // Get the RTMPS settings object + if (!responseObject["rtmps_settings"].isObject()) { + QString error = + responseObject["error"].toObject()["message"].toString(); + blog(LOG_INFO, + "FacebookEncoderSettingsProvider rtmps_settings not an object"); + jsonParseError(); + return; + } + QJsonObject rtmpSettings = responseObject["rtmps_settings"].toObject(); + + // Get the video codec object + if (!rtmpSettings["video_codec_settings"].isObject()) { + blog(LOG_INFO, + "FacebookEncoderSettingsProvider video_codec_settings not an object"); + jsonParseError(); + return; + } + QJsonObject videoSettingsJson = + rtmpSettings["video_codec_settings"].toObject(); + + if (videoSettingsJson.isEmpty()) { + handleEmpty(); + } + + // Create map to send to wizard + SettingsMap *settingsMap = new SettingsMap(); + + addInt(videoSettingsJson, "video_bitrate", settingsMap, + kSettingsResponseKeys.videoBitrate); + addInt(videoSettingsJson, "video_width", settingsMap, + kSettingsResponseKeys.videoWidth); + addInt(videoSettingsJson, "video_height", settingsMap, + kSettingsResponseKeys.videoHeight); + addStringDouble(videoSettingsJson, "video_framerate", settingsMap, + kSettingsResponseKeys.framerate); + addQString(videoSettingsJson, "video_h264_profile", settingsMap, + kSettingsResponseKeys.h264Profile); + addQString(videoSettingsJson, "video_h264_level", settingsMap, + kSettingsResponseKeys.h264Level); + addInt(videoSettingsJson, "video_gop_size", settingsMap, + kSettingsResponseKeys.gopSizeInFrames); + addBool(videoSettingsJson, "video_gop_closed", settingsMap, + kSettingsResponseKeys.gopClosed); + addInt(videoSettingsJson, "video_gop_num_b_frames", settingsMap, + kSettingsResponseKeys.gopBFrames); + addInt(videoSettingsJson, "video_gop_num_ref_frames", settingsMap, + kSettingsResponseKeys.gopRefFrames); + addQString(videoSettingsJson, "rate_control_mode", settingsMap, + kSettingsResponseKeys.streamRateControlMode); + addInt(videoSettingsJson, "buffer_size", settingsMap, + kSettingsResponseKeys.streamBufferSize); + + // If Empty emit to empty / error state + if (settingsMap->isEmpty()) { + handleEmpty(); + } + + // Wrap into shared pointer and emit + QSharedPointer settingsMapShrdPtr = + QSharedPointer(settingsMap); + emit newSettings(settingsMapShrdPtr); +} + +void FacebookEncoderSettingsProvider::handleTimeout() +{ + QString errorTitle = QTStr("PreLiveWizard.Configure.Error.JsonParse"); + QString errorDescription = QTStr("PreLiveWizard.Error.NetworkTimeout"); + emit returnErrorDescription(errorTitle, errorDescription); +} + +void FacebookEncoderSettingsProvider::handleEmpty() +{ + emit returnErrorDescription( + QTStr("PreLiveWizard.Configure.Error.NoData"), + QTStr("PreLiveWizard.Configure.Error.NoData.Description")); +} + +void FacebookEncoderSettingsProvider::jsonParseError() +{ + emit returnErrorDescription( + QTStr("PreLiveWizard.Configure.Error.JsonParse"), + QTStr("PreLiveWizard.Configure.Error.JsonParse.Description")); +} + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/encoder-settings-provider-facebook.hpp b/UI/pre-stream-wizard/encoder-settings-provider-facebook.hpp new file mode 100644 index 00000000000000..f32ec8914161c3 --- /dev/null +++ b/UI/pre-stream-wizard/encoder-settings-provider-facebook.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "pre-stream-current-settings.hpp" + +namespace StreamWizard { + +class FacebookEncoderSettingsProvider : public QObject { + Q_OBJECT + +public: + FacebookEncoderSettingsProvider(QObject *parent = nullptr); + + // Pass in encoder request to use, returns FALSE is there is an error. + void setEncoderRequest(QSharedPointer request); + + // Uses the EncoderSettingsRequest to generate SettingsMap + // Does not return in sync becuase can be an async API call + // Success: emits returnedEncoderSettings(SettingsMap) + // Failure: emits returnedError(QString title, QString description) + void run(); + +signals: + // On success. + void newSettings(QSharedPointer response); + // On failure. Will be shown to user. + void returnErrorDescription(QString title, QString description); + +private: + QSharedPointer currentSettings_; + bool pendingResponse_ = false; + + void makeRequest(QUrl &url); + QUrlQuery inputVideoQueryFromCurrentSettings(); + void handleResponse(QByteArray reply); + void handleTimeout(); + void handleEmpty(); + void jsonParseError(); +}; + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-completed.cpp b/UI/pre-stream-wizard/page-completed.cpp new file mode 100644 index 00000000000000..909c227ce757f6 --- /dev/null +++ b/UI/pre-stream-wizard/page-completed.cpp @@ -0,0 +1,70 @@ +#include "page-completed.hpp" + +#include +#include +#include +#include +#include +#include + +#include "obs-app.hpp" + +namespace StreamWizard { + +CompletedPage::CompletedPage(Destination destination, + LaunchContext launchContext, QWidget *parent) + : QWizardPage(parent) +{ + destination_ = destination; + launchContext_ = launchContext; + + setTitle(QTStr("PreLiveWizard.Completed.Title")); + setFinalPage(true); + + QVBoxLayout *mainlayout = new QVBoxLayout(this); + setLayout(mainlayout); + + // Later will suggest starting stream if launchContext is PreStream + QLabel *closeLabel = + new QLabel(QTStr("PreLiveWizard.Completed.BodyText"), this); + closeLabel->setWordWrap(true); + mainlayout->addWidget(closeLabel); + + if (destination_ == Destination::Facebook) { + mainlayout->addSpacerItem(new QSpacerItem(12, 12)); + + QLabel *facebookGoLiveLabel = new QLabel( + QTStr("PreLiveWizard.Completed.FacebookOnly"), this); + facebookGoLiveLabel->setWordWrap(true); + QPushButton *launchButton = new QPushButton( + QTStr("PreLiveWizard.Prompt.ResolutionHelpButton.FB"), + this); + launchButton->setToolTip(QTStr( + "PreLiveWizard.Prompt.ResolutionHelpButton.FB.ToolTip")); + connect(launchButton, &QPushButton::clicked, this, + &CompletedPage::didPushOpenWebsite); + + mainlayout->addWidget(facebookGoLiveLabel); + mainlayout->addWidget(launchButton); + } +} + +void CompletedPage::didPushOpenWebsite() +{ + QUrl helpUrl; + + // Prepare per-destination + if (destination_ == Destination::Facebook) { + helpUrl = QUrl( + "https://www.facebook.com/live/producer/?ref=OBS_Wizard", + QUrl::TolerantMode); + } else { + return; + } + // Launch + if (helpUrl.isValid()) { + QDesktopServices::openUrl(helpUrl); + } +} + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-completed.hpp b/UI/pre-stream-wizard/page-completed.hpp new file mode 100644 index 00000000000000..b634ad19f6a966 --- /dev/null +++ b/UI/pre-stream-wizard/page-completed.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "pre-stream-current-settings.hpp" + +namespace StreamWizard { + +// Last Page +class CompletedPage : public QWizardPage { + Q_OBJECT + +public: + CompletedPage(Destination destination, LaunchContext launchContext, + QWidget *parent = nullptr); + +private: + Destination destination_; + LaunchContext launchContext_; + +private slots: + void didPushOpenWebsite(); + +}; // class CompletedPage + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-error.cpp b/UI/pre-stream-wizard/page-error.cpp new file mode 100644 index 00000000000000..c851d7d0478e8b --- /dev/null +++ b/UI/pre-stream-wizard/page-error.cpp @@ -0,0 +1,33 @@ +#include "page-error.hpp" + +#include +#include + +#include "obs-app.hpp" + +namespace StreamWizard { + +ErrorPage::ErrorPage(QWidget *parent) : QWizardPage(parent) +{ + setTitle(QTStr("PreLiveWizard.Error.Title")); + setSubTitle(QTStr("PreLiveWizard.Error.Subtitle")); + setFinalPage(true); + + QVBoxLayout *mainlayout = new QVBoxLayout(this); + titleLabel_ = new QLabel(this); + titleLabel_->setStyleSheet("font-weight: bold;"); + descriptionLabel_ = new QLabel(this); + mainlayout->addWidget(titleLabel_); + mainlayout->addSpacerItem(new QSpacerItem(12, 12)); + mainlayout->addWidget(descriptionLabel_); + setText(QTStr("PreLiveWizard.Error.Generic.Headline"), + QTStr("PreLiveWizard.Error.Generic.BodyText")); +} + +void ErrorPage::setText(QString title, QString description) +{ + titleLabel_->setText(title); + descriptionLabel_->setText(description); +} + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-error.hpp b/UI/pre-stream-wizard/page-error.hpp new file mode 100644 index 00000000000000..a40120b812995e --- /dev/null +++ b/UI/pre-stream-wizard/page-error.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include "pre-stream-current-settings.hpp" + +namespace StreamWizard { + +// Page shown if there's an error emitted from provider. + +class ErrorPage : public QWizardPage { + Q_OBJECT + +public: + ErrorPage(QWidget *parent = nullptr); + + void setText(QString title, QString description); + +private: + QString title_; + QString description_; + QLabel *titleLabel_; + QLabel *descriptionLabel_; +}; // class ErrorPage + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-input-display.cpp b/UI/pre-stream-wizard/page-input-display.cpp new file mode 100644 index 00000000000000..3f62dd9c9b1fa6 --- /dev/null +++ b/UI/pre-stream-wizard/page-input-display.cpp @@ -0,0 +1,45 @@ +#include "page-input-display.hpp" + +#include +#include +#include +#include +#include + +#include "pre-stream-current-settings.hpp" + +QWizardPage *SettingsInputPage(StreamWizard::EncoderSettingsRequest *settings) +{ + QWizardPage *page = new QWizardPage(); + page->setTitle("Demo + Show Data"); + + // Layout for the entire widget / Wizard PAge + QVBoxLayout *mainLayout = new QVBoxLayout(page); + + // Scroll area that contains values + QScrollArea *scroll = new QScrollArea(); + + // Data list + QWidget *formContainer = new QWidget(); + QFormLayout *form = new QFormLayout(formContainer); + + form->addRow("Video Width", + new QLabel(QString::number(settings->videoWidth))); + form->addRow("Video Height", + new QLabel(QString::number(settings->videoHeight))); + form->addRow("Framerate", + new QLabel(QString::number(settings->framerate))); + form->addRow("V Bitrate", + new QLabel(QString::number(settings->videoBitrate))); + form->addRow("A channels", + new QLabel(QString::number(settings->audioChannels))); + form->addRow("A Samplerate", + new QLabel(QString::number(settings->audioSamplerate))); + form->addRow("A Bitrate", + new QLabel(QString::number(settings->audioBitrate))); + + page->setLayout(mainLayout); + mainLayout->addWidget(scroll); + scroll->setWidget(formContainer); + return page; +} diff --git a/UI/pre-stream-wizard/page-input-display.hpp b/UI/pre-stream-wizard/page-input-display.hpp new file mode 100644 index 00000000000000..a15dbb0eed1f86 --- /dev/null +++ b/UI/pre-stream-wizard/page-input-display.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +#include "pre-stream-current-settings.hpp" + +extern "C" { + +QWizardPage *SettingsInputPage(StreamWizard::EncoderSettingsRequest *settings); + +} // extern C diff --git a/UI/pre-stream-wizard/page-loading.cpp b/UI/pre-stream-wizard/page-loading.cpp new file mode 100644 index 00000000000000..c3fad9fa663182 --- /dev/null +++ b/UI/pre-stream-wizard/page-loading.cpp @@ -0,0 +1,51 @@ +#include "page-loading.hpp" + +#include +#include +#include +#include + +#include "obs-app.hpp" + +namespace StreamWizard { + +LoadingPage::LoadingPage(QWidget *parent) : QWizardPage(parent) +{ + setTitle(QTStr("PreLiveWizard.Loading.Title")); + setCommitPage(true); + + timer_ = new QTimer(this); + connect(timer_, &QTimer::timeout, this, &LoadingPage::tick); + timer_->setSingleShot(false); + + labels_.append(QTStr("Loading")); + labels_.append(QTStr("Loading") + "."); + labels_.append(QTStr("Loading") + "." + "."); + labels_.append(QTStr("Loading") + "." + "." + "."); + + QVBoxLayout *mainlayout = new QVBoxLayout(this); + loadingLabel_ = new QLabel(this); + loadingLabel_->setText(labels_.at(0)); + mainlayout->addWidget(loadingLabel_); + setLayout(mainlayout); +} + +void LoadingPage::initializePage() +{ + timer_->start(300); +} +void LoadingPage::cleanupPage() +{ + timer_->stop(); +} + +void LoadingPage::tick() +{ + count_++; + if (count_ > 3) + count_ = 0; + + loadingLabel_->setText(labels_.at(count_)); +} + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-loading.hpp b/UI/pre-stream-wizard/page-loading.hpp new file mode 100644 index 00000000000000..5f18398cc0f3f8 --- /dev/null +++ b/UI/pre-stream-wizard/page-loading.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "pre-stream-current-settings.hpp" + +namespace StreamWizard { + +// Page shown while a provider computes best settings. +class LoadingPage : public QWizardPage { + Q_OBJECT + +public: + LoadingPage(QWidget *parent = nullptr); + void initializePage() override; + void cleanupPage() override; + +private: + QLabel *loadingLabel_; + QTimer *timer_; + QStringList labels_; + int count_ = 0; + +private slots: + void tick(); + +}; // class LoadingPage + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-select-settings.cpp b/UI/pre-stream-wizard/page-select-settings.cpp new file mode 100644 index 00000000000000..e350ac4e3a0d79 --- /dev/null +++ b/UI/pre-stream-wizard/page-select-settings.cpp @@ -0,0 +1,194 @@ +#include "page-select-settings.hpp" + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "setting-selection-row.hpp" + +namespace StreamWizard { + +SelectionPage::SelectionPage(QWidget *parent) : QWizardPage(parent) +{ + this->setTitle(QTStr("PreLiveWizard.Selection.Title")); + setupLayout(); + + setButtonText(QWizard::WizardButton::NextButton, + QTStr("Basic.AutoConfig.ApplySettings")); + setButtonText(QWizard::WizardButton::CommitButton, + QTStr("Basic.AutoConfig.ApplySettings")); + setCommitPage(true); +} + +void SelectionPage::setupLayout() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + setLayout(mainLayout); + + // Description to user what scroll area contains + descriptionLabel_ = + new QLabel(QTStr("PreLiveWizard.Selection.Description")); + mainLayout->addWidget(descriptionLabel_); + + // Add Scroll area widget + scrollArea_ = new QScrollArea(); + scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + scrollArea_->setWidgetResizable(true); + + // ScrollArea_ holds a widget: + scrollBoxWidget_ = new QWidget(); + scrollArea_->setWidget(scrollBoxWidget_); + + // Scroll's Widget requires a layout + scrollVBoxLayout_ = new QVBoxLayout(); + + scrollBoxWidget_->setLayout(scrollVBoxLayout_); + scrollBoxWidget_->show(); + + // Add Scroll Aread to mainlayout + mainLayout->addWidget(scrollArea_); +} + +void SelectionPage::setSettingsMap(QSharedPointer settingsMap) +{ + if (settingsMap_ != nullptr) { + return; + } + settingsMap_ = settingsMap; + SettingsMap *mapInfo = settingsMap_.data(); + + if (mapInfo->contains(kSettingsResponseKeys.videoBitrate)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.videoBitrate).first; + QString bitrateString = QString::number(data.toInt()) + "kbps"; + addRow(QTStr("Basic.Settings.Output.VideoBitrate"), + bitrateString, kSettingsResponseKeys.videoBitrate); + } + + // Uses kSettingsResponseKeys.videoHeight to signal change + if (mapInfo->contains(kSettingsResponseKeys.videoWidth) && + mapInfo->contains(kSettingsResponseKeys.videoHeight)) { + int vidW = mapInfo->value(kSettingsResponseKeys.videoWidth) + .first.toInt(); + QString videoWidthString = QString::number(vidW); + int vidH = mapInfo->value(kSettingsResponseKeys.videoHeight) + .first.toInt(); + QString videoHeightString = QString::number(vidH); + QString valueString = + videoWidthString + "x" + videoHeightString; + + addRow(QTStr("Basic.Settings.Video.ScaledResolution"), + valueString, kSettingsResponseKeys.videoHeight); + } + + if (mapInfo->contains(kSettingsResponseKeys.framerate)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.framerate).first; + double framerate = data.toDouble(); + QString fpsString = QString().asprintf("%.3f", framerate); + addRow(QTStr("Basic.Settings.Video.FPS"), fpsString, + kSettingsResponseKeys.framerate); + } + + if (mapInfo->contains(kSettingsResponseKeys.h264Profile)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.h264Profile).first; + QString profileString = data.toString(); + addRow(QTStr("PreLiveWizard.Output.Mode.CodecProfile"), + profileString, kSettingsResponseKeys.h264Profile); + } + + if (mapInfo->contains(kSettingsResponseKeys.h264Level)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.h264Level).first; + QString level = data.toString(); + addRow(QTStr("PreLiveWizard.Output.Mode.CodecLevel"), level, + kSettingsResponseKeys.h264Level); + } + + if (mapInfo->contains(kSettingsResponseKeys.gopSizeInFrames)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.gopSizeInFrames) + .first; + QString gopFramesString = QString::number(data.toInt()); + addRow(QTStr("Basic.Settings.Output.Adv.FFmpeg.GOPSize"), + gopFramesString, kSettingsResponseKeys.gopSizeInFrames); + } + + if (mapInfo->contains(kSettingsResponseKeys.gopClosed)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.gopClosed).first; + QString gopClosedString = data.toBool() ? QTStr("Yes") + : QTStr("No"); + addRow(QTStr("Basic.Settings.Output.Adv.FFmpeg.GOPClosed"), + gopClosedString, kSettingsResponseKeys.gopClosed); + } + + if (mapInfo->contains(kSettingsResponseKeys.gopBFrames)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.gopBFrames).first; + QString bFramesString = QString::number(data.toInt()); + addRow(QTStr("Basic.Settings.Output.Adv.FFmpeg.BFrames"), + bFramesString, kSettingsResponseKeys.gopBFrames); + } + + if (mapInfo->contains(kSettingsResponseKeys.gopRefFrames)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.gopRefFrames).first; + QString gopRefFramesCountString = QString::number(data.toInt()); + addRow(QTStr("Basic.Settings.Output.Adv.FFmpeg.ReferenceFrames"), + gopRefFramesCountString, + kSettingsResponseKeys.gopRefFrames); + } + + if (mapInfo->contains(kSettingsResponseKeys.streamRateControlMode)) { + QVariant data = mapInfo->value(kSettingsResponseKeys + .streamRateControlMode) + .first; + QString rateControlString = data.toString().toUpper(); + addRow(QTStr("PreLiveWizard.Output.Mode.RateControl"), + rateControlString, + kSettingsResponseKeys.streamRateControlMode); + } + + if (mapInfo->contains(kSettingsResponseKeys.streamBufferSize)) { + QVariant data = + mapInfo->value(kSettingsResponseKeys.streamBufferSize) + .first; + QString bufferSizeString = QString::number(data.toInt()) + "Kb"; + addRow(QTStr("PreLiveWizard.Output.Mode.StreamBuffer"), + bufferSizeString, + kSettingsResponseKeys.streamBufferSize); + } +} + +void SelectionPage::addRow(QString title, QString value, QString mapKey) +{ + SelectionRow *row = new SelectionRow(); + row->setName(title); + row->setValueLabel(value); + row->setPropertyKey(mapKey); + scrollVBoxLayout_->addWidget(row); + connect(row, &SelectionRow::didChangeSelectedStatus, this, + &SelectionPage::checkboxRowChanged); +} + +void SelectionPage::checkboxRowChanged(QString propertyKey, bool selected) +{ + if (settingsMap_ == nullptr) { + return; + } + + SettingsMap *mapInfo = settingsMap_.data(); + if (mapInfo == nullptr || !mapInfo->contains(propertyKey)) { + return; + } + + QPair &dataPair = (*mapInfo)[propertyKey]; + dataPair.second = selected; +} + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-select-settings.hpp b/UI/pre-stream-wizard/page-select-settings.hpp new file mode 100644 index 00000000000000..fb859a6af59262 --- /dev/null +++ b/UI/pre-stream-wizard/page-select-settings.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "pre-stream-current-settings.hpp" + +namespace StreamWizard { + +/* +* Shows user encoder configuration options and allows them to select and apply +* each. For exmaple can apply a resolution limit but opt-out of using b-frames +* even if recommended. +*/ +class SelectionPage : public QWizardPage { + Q_OBJECT + +public: + SelectionPage(QWidget *parent = nullptr); + + // Sets data to layout scrollbox + void setSettingsMap(QSharedPointer settingsMap); + +private: + // Main UI setup, setSettingsMap finished layout with data + void setupLayout(); + + void mapContainsVariant(); + // Adds a row only if map contains values for it + void addRow(QString title, QString value, QString mapKey); + + // Data + QSharedPointer settingsMap_; + + // Layouts and subwidgets + QLabel *descriptionLabel_; + QScrollArea *scrollArea_; + QWidget *scrollBoxWidget_; + QVBoxLayout *scrollVBoxLayout_; + +private slots: + void checkboxRowChanged(QString propertyKey, bool selected); + +}; // class SelectionPage + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-start-prompt.cpp b/UI/pre-stream-wizard/page-start-prompt.cpp new file mode 100644 index 00000000000000..080a9cab16e580 --- /dev/null +++ b/UI/pre-stream-wizard/page-start-prompt.cpp @@ -0,0 +1,149 @@ +#include "page-start-prompt.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "obs-app.hpp" + +namespace StreamWizard { + +StartPage::StartPage(Destination dest, LaunchContext launchContext, + QSize videoSize, QWidget *parent) + : QWizardPage(parent) +{ + this->destination_ = dest; + this->launchContext_ = launchContext; + this->startVideoSize_ = videoSize; + + setTitle(QTStr("PreLiveWizard.Prompt.Title")); + + createLayout(); + connectRadioButtons(); + + //Default behavior per destination + if (destination_ == Destination::Facebook) { + res720Button_->setChecked(true); + } else { + resCurrentButton_->setChecked(true); + } +} + +void StartPage::initializePage() +{ + // emit defaul resolution check + if (destination_ == Destination::Facebook && + res720Button_->isChecked()) { + emit userSelectedResolution(QSize(1280, 720)); + } +} + +void StartPage::createLayout() +{ + QVBoxLayout *mainlayout = new QVBoxLayout(this); + + QLabel *explainer = + new QLabel(QTStr("PreLiveWizard.Prompt.Explainer"), this); + explainer->setWordWrap(true); + + QLabel *radioButtonsTitle = + new QLabel(QTStr("PreLiveWizard.Prompt.ResSelectTitle"), this); + radioButtonsTitle->setWordWrap(true); + radioButtonsTitle->setStyleSheet("font-weight: bold;"); + + // Radio Button Group + QString res720Label = QTStr("PreLiveWizard.Prompt.Resolution.720"); + res720Button_ = new QRadioButton(res720Label, this); + + QString res1080label = QTStr("PreLiveWizard.Prompt.Resolution.1080"); + res1080Button_ = new QRadioButton(res1080label, this); + + QString currentResLabel = + QTStr("PreLiveWizard.Prompt.Resolution.Current") + .arg(QString::number(startVideoSize_.width()), + QString::number(startVideoSize_.height())); + resCurrentButton_ = new QRadioButton(currentResLabel, this); + + // Optional Help Section + QLabel *helpLabel = nullptr; + QPushButton *helpButton = nullptr; + + if (destination_ == Destination::Facebook) { + // If FB, specfic help section + helpLabel = new QLabel( + QTStr("PreLiveWizard.Prompt.ResolutionHelp.FB"), this); + helpLabel->setWordWrap(true); + helpButton = new QPushButton( + QTStr("PreLiveWizard.Prompt.ResolutionHelpButton.FB"), + this); + helpButton->setToolTip(QTStr( + "PreLiveWizard.Prompt.ResolutionHelpButton.FB.ToolTip")); + connect(helpButton, &QPushButton::clicked, this, + &StartPage::didPushOpenWebsiteHelp); + } + + setLayout(mainlayout); + mainlayout->addWidget(explainer); + mainlayout->addSpacerItem(new QSpacerItem(12, 12)); + mainlayout->addWidget(radioButtonsTitle); + mainlayout->addWidget(res720Button_); + mainlayout->addWidget(res1080Button_); + mainlayout->addWidget(resCurrentButton_); + + if (helpLabel != nullptr) { + mainlayout->addSpacerItem(new QSpacerItem(24, 24)); + mainlayout->addWidget(helpLabel); + } + if (helpButton != nullptr) { + QHBoxLayout *buttonAndSpacerLayout = new QHBoxLayout(); + buttonAndSpacerLayout->addWidget(helpButton); + QSpacerItem *buttonSpacer = + new QSpacerItem(24, 24, QSizePolicy::MinimumExpanding, + QSizePolicy::Minimum); + buttonAndSpacerLayout->addSpacerItem(buttonSpacer); + mainlayout->addLayout(buttonAndSpacerLayout); + } +} + +void StartPage::connectRadioButtons() +{ + connect(res720Button_, &QRadioButton::clicked, [=]() { + QSize selectedVideoSize = QSize(1280, 720); + emit userSelectedResolution(selectedVideoSize); + }); + connect(res1080Button_, &QRadioButton::clicked, [=]() { + QSize selectedVideoSize = QSize(1920, 1080); + emit userSelectedResolution(selectedVideoSize); + }); + connect(resCurrentButton_, &QRadioButton::clicked, [=]() { + QSize selectedVideoSize = QSize(startVideoSize_.width(), + startVideoSize_.height()); + emit userSelectedResolution(selectedVideoSize); + }); +} + +void StartPage::didPushOpenWebsiteHelp() +{ + QUrl helpUrl; + + // Prepare per-destination + if (destination_ == Destination::Facebook) { + helpUrl = QUrl( + "https://www.facebook.com/live/producer/?ref=OBS_Wizard", + QUrl::TolerantMode); + } else { + return; + } + + // Launch + if (helpUrl.isValid()) { + QDesktopServices::openUrl(helpUrl); + } +} + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/page-start-prompt.hpp b/UI/pre-stream-wizard/page-start-prompt.hpp new file mode 100644 index 00000000000000..190c698d46b41a --- /dev/null +++ b/UI/pre-stream-wizard/page-start-prompt.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include "pre-stream-current-settings.hpp" + +class QRadioButton; +class QSignalMapper; +class QSize; + +namespace StreamWizard { + +// Prompt if the user wants to verify their settings or close. +class StartPage : public QWizardPage { + Q_OBJECT + +public: + StartPage(Destination dest, LaunchContext launchContext, + QSize videoSize, QWidget *parent = nullptr); + + void initializePage() override; + +signals: + // emitted selected resolution from start page radio buttons + void userSelectedResolution(QSize newVideoSize); + +private: + // Init information + Destination destination_; + LaunchContext launchContext_; + QSize startVideoSize_; + + // Selected settings + QRadioButton *res720Button_; + QRadioButton *res1080Button_; + QRadioButton *resCurrentButton_; + + void createLayout(); + void connectRadioButtons(); + +private slots: + void didPushOpenWebsiteHelp(); + +}; // class StartPage +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/pre-stream-current-settings.cpp b/UI/pre-stream-wizard/pre-stream-current-settings.cpp new file mode 100644 index 00000000000000..751e5aa47d0bc9 --- /dev/null +++ b/UI/pre-stream-wizard/pre-stream-current-settings.cpp @@ -0,0 +1,22 @@ +#include "pre-stream-current-settings.hpp" + +namespace StreamWizard { + +SettingsResponseKeys kSettingsResponseKeys = { + "videoWidth", + "videoHeight", + "framerate", + "videoBitrate", + "protocol", + "videoCodec", + "h264Profile", + "h264Level", + "gopSizeInFrames", + "gopClosed", + "gopBFrames", + "gopRefFrames", + "streamRateControlMode", + "streamBufferSize", +}; + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/pre-stream-current-settings.hpp b/UI/pre-stream-wizard/pre-stream-current-settings.hpp new file mode 100644 index 00000000000000..ad268e8584cc4c --- /dev/null +++ b/UI/pre-stream-wizard/pre-stream-current-settings.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace StreamWizard { + +enum class VideoType { + live, // Streaming + vod, // Video On Demand, recording uploads +}; + +enum class StreamProtocol { + rtmps, +}; + +enum class VideoCodec { + h264, +}; + +enum class AudioCodec { + aac, +}; + +/* There are two launch contexts for starting the wizard + - PreStream: the wizard is triggered between pressing Start Streaming and a + stream. So the user wizard should indicate when the encoder is ready to stream + but also allow the user to abort. + + - Settings: User start config workflow from the settings page or toolbar. + In this case, the wizard should not end with the stream starting but may end + wutg saving the settings if given signal +*/ +enum class LaunchContext { + PreStream, + Settings, +}; + +/* + To make the wizard expandable we can have multiple destinations. + In the case Facebook, it will use Facebook's no-auth encoder API. +*/ +enum class Destination { + Facebook, +}; + +// Data to send to encoder config API +struct EncoderSettingsRequest { + //// Stream + StreamProtocol protocol; // Expandable but only supports RTMPS for now + VideoType videoType; // LIVE or VOD (but always live for OBS) + + ///// Video Settings + int videoWidth; + int videoHeight; + double framerate; // in frames per second + int videoBitrate; // in kbps e.g., 4000kbps + VideoCodec videoCodec; + + ///// Audio Settings + int audioChannels; + int audioSamplerate; // in Hz, e.g., 48000Hz + int audioBitrate; // in kbps e.g., 128kbps + AudioCodec audioCodec; + + // If the user picked a resolution in the wizard + // Holds QSize or null + QVariant userSelectedResolution; +}; + +// Map for the repsonse passed to UI and settings is: +using SettingsMap = QMap>; +// where String = map key from kSettingsResponseKeys, and +// where QVariant is the Settings value, and +// where bool is if the user wants to apply the settings +// Keys for new settings QMap +struct SettingsResponseKeys { + QString videoWidth; //int + QString videoHeight; //int + QString framerate; // double (FPS) + QString videoBitrate; //int + QString protocol; // string + QString videoCodec; // string + QString h264Profile; // string ("High") + QString h264Level; // string ("4.1") + QString gopSizeInFrames; // int + QString gopClosed; // bool + QString gopBFrames; // int + QString gopRefFrames; // int + QString streamRateControlMode; // string "CBR" + QString streamBufferSize; // int (5000 kb) +}; + +// Defined in pre-stream-current-settings.cpp +extern SettingsResponseKeys kSettingsResponseKeys; + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/pre-stream-wizard.cpp b/UI/pre-stream-wizard/pre-stream-wizard.cpp new file mode 100644 index 00000000000000..8bb9c9fe9e2c81 --- /dev/null +++ b/UI/pre-stream-wizard/pre-stream-wizard.cpp @@ -0,0 +1,204 @@ +#include "pre-stream-wizard.hpp" + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "encoder-settings-provider-facebook.hpp" + +#include "page-input-display.hpp" +#include "page-start-prompt.hpp" +#include "page-loading.hpp" +#include "page-select-settings.hpp" +#include "page-completed.hpp" +#include "page-error.hpp" + +namespace StreamWizard { + +enum PSW_Page { + Page_StartPrompt, + Page_Loading, + Page_Selections, + Page_Complete, + Page_Error, +}; + +PreStreamWizard::PreStreamWizard( + Destination dest, LaunchContext launchContext, + QSharedPointer currentSettings, QWidget *parent) + : QWizard(parent) +{ + this->currentSettings_ = currentSettings; + this->destination_ = dest; + this->launchContext_ = launchContext; + connect(this, &QWizard::currentIdChanged, this, + &PreStreamWizard::onPageChanged); + + setWizardStyle(QWizard::ModernStyle); + setWindowTitle(QTStr("PreLiveWizard.Title")); + setOption(QWizard::NoBackButtonOnStartPage, true); + setOption(QWizard::NoBackButtonOnLastPage, true); + + // First Page: Explain to the user and confirm sending to API + QSize currentRes(currentSettings_->videoWidth, + currentSettings_->videoHeight); + startPage_ = + new StartPage(destination_, launchContext_, currentRes, this); + setPage(Page_StartPrompt, startPage_); + connect(startPage_, &StartPage::userSelectedResolution, this, + &PreStreamWizard::onUserSelectResolution); + + // Loading page: Shown when loading new settings + loadingPage_ = new LoadingPage(this); + setPage(Page_Loading, loadingPage_); + + // Suggestion Selection Page + selectionPage_ = new SelectionPage(this); + setPage(Page_Selections, selectionPage_); + + // Ending + Confirmation Page + CompletedPage *completedPage = + new CompletedPage(destination_, launchContext_, this); + setPage(Page_Complete, completedPage); + + // Add error page shown instead of completion in failure cases + errorPage_ = new ErrorPage(this); + setPage(Page_Error, errorPage_); +} + +void PreStreamWizard::requestSettings() +{ + if (destination_ == Destination::Facebook) { + + FacebookEncoderSettingsProvider *fbProvider = + new FacebookEncoderSettingsProvider(this); + connect(fbProvider, + &FacebookEncoderSettingsProvider::newSettings, this, + &PreStreamWizard::providerEncoderSettings); + connect(fbProvider, + &FacebookEncoderSettingsProvider::returnErrorDescription, + this, &PreStreamWizard::providerError); + + fbProvider->setEncoderRequest(currentSettings_); + fbProvider->run(); + + } else { + QString errorTitle = QTStr( + "PreLiveWizard.Configure.ServiceNotAvailable.Title"); + QString errorDescription = QTStr( + "PreLiveWizard.Configure.ServiceNotAvailable.Description"); + providerError(errorTitle, errorDescription); + } +} + +// SLOTS ------------- +void PreStreamWizard::providerEncoderSettings( + QSharedPointer response) +{ + blog(LOG_INFO, "PreStreamWizard got new settings response"); + newSettingsMap_ = response; + if (currentId() == Page_Loading) { + next(); + } +} + +void PreStreamWizard::providerError(QString title, QString description) +{ + sendToErrorPage_ = true; + errorPage_->setText(title, description); + if (currentId() == Page_Loading) + next(); +} + +void PreStreamWizard::onPageChanged(int id) +{ + switch (id) { + case Page_StartPrompt: + setOption(QWizard::NoCancelButton, false); + setButtons(NextStep); + break; + + case Page_Loading: + requestSettings(); + break; + + case Page_Selections: + selectionPage_->setSettingsMap(newSettingsMap_); + setButtons(CommitStep); + break; + + case Page_Complete: + setButtons(FinishOnly); + if (newSettingsMap_ != nullptr && !newSettingsMap_.isNull()) { + // ToDo: messaging in edge case this could be empty? + emit applySettings(newSettingsMap_); + }; + break; + + case Page_Error: + sendToErrorPage_ = false; + setButtons(FinishOnly); + break; + } +} + +int PreStreamWizard::nextId() const +{ + switch (currentId()) { + case Page_StartPrompt: + return Page_Loading; + case Page_Loading: { + if (sendToErrorPage_) + return Page_Error; + if (newSettingsMap_ == nullptr || newSettingsMap_.isNull()) { + errorPage_->setText( + QTStr("PreLiveWizard.Configure.Error.NoData"), + QTStr("PreLiveWizard.Configure.Error.JsonParse.Description")); + return Page_Error; + } + return Page_Selections; + } + case Page_Selections: + return Page_Complete; + case Page_Complete: + return -1; + case Page_Error: + return -1; + } + + return QWizard::nextId(); +} + +void PreStreamWizard::setButtons(ButtonLayout layout) +{ + QList layoutList; + layoutList << QWizard::Stretch; + switch (layout) { + case NextStep: + layoutList << QWizard::NextButton << QWizard::CancelButton; + break; + case CommitStep: + layoutList << QWizard::CommitButton << QWizard::CancelButton; + break; + case CancelOnly: + layoutList << QWizard::CancelButton; + break; + case FinishOnly: + layoutList << QWizard::FinishButton; + break; + } + setButtonLayout(layoutList); +} + +void PreStreamWizard::onUserSelectResolution(QSize newSize) +{ + blog(LOG_INFO, "Selected res %d x %d", newSize.width(), + newSize.height()); + EncoderSettingsRequest *req = currentSettings_.data(); + req->userSelectedResolution = QVariant(newSize); +} + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/pre-stream-wizard.hpp b/UI/pre-stream-wizard/pre-stream-wizard.hpp new file mode 100644 index 00000000000000..68a7f28779284a --- /dev/null +++ b/UI/pre-stream-wizard/pre-stream-wizard.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include + +#include "pre-stream-current-settings.hpp" + +namespace StreamWizard { + +class StartPage; +class SelectionPage; +class LoadingPage; +class ErrorPage; + +/* +* The pre-stream wizard is a workflow focused on delivering encoder settings +* specfic for the streaming destination before going Live. +* There is a launch context to know if launched from settings, otherwise +* we'll later add a wizard page and button for going live after new settings. +*/ +class PreStreamWizard : public QWizard { + Q_OBJECT + +public: + PreStreamWizard(Destination dest, LaunchContext launchContext, + QSharedPointer currentSettings, + QWidget *parent = nullptr); + + int nextId() const override; + +private: + // Pages + StartPage *startPage_; + LoadingPage *loadingPage_; + SelectionPage *selectionPage_; + ErrorPage *errorPage_; + + // External State + Destination destination_; + LaunchContext launchContext_; + QSharedPointer currentSettings_; + QSharedPointer newSettingsMap_; + bool sendToErrorPage_ = false; + + void requestSettings(); + + // Wizard uses custom button layouts to manage user flow & aborts. + enum ButtonLayout { + NextStep, + CancelOnly, + FinishOnly, + CommitStep, + }; + void setButtons(ButtonLayout layout); + +signals: + // Apply settings, don't start stream. e.g., is configuring from settings + void applySettings(QSharedPointer newSettings); + + // All configuration providers must call one of these exclusively when done to + // continue the wizard. This is the contract any servicer's + // configuration provider must fulfill +public slots: + // On success, returns a EncoderSettingsResponse object + void providerEncoderSettings(QSharedPointer response); + // Title and description to show the user. + void providerError(QString title, QString description); + +private slots: + void onPageChanged(int id); + void onUserSelectResolution(QSize newSize); +}; + +} //namespace StreamWizard diff --git a/UI/pre-stream-wizard/setting-selection-row.cpp b/UI/pre-stream-wizard/setting-selection-row.cpp new file mode 100644 index 00000000000000..321d5e4e62a401 --- /dev/null +++ b/UI/pre-stream-wizard/setting-selection-row.cpp @@ -0,0 +1,73 @@ +#include "setting-selection-row.hpp" + +#include + +namespace StreamWizard { + +SelectionRow::SelectionRow(QWidget *parent) : QWidget(parent) +{ + createLayout(); +} + +void SelectionRow::createLayout() +{ + QHBoxLayout *mainlayout = new QHBoxLayout(this); + QMargins margins = QMargins(3, 3, 3, 3); + mainlayout->setContentsMargins(margins); + checkboxValueString_ = QString(); + checkbox_ = new QCheckBox(checkboxValueString_); + checkbox_->setChecked(true); + + setLayout(mainlayout); + + mainlayout->addWidget(checkbox_); + QSpacerItem *checkboxSpace = new QSpacerItem( + 6, 6, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); + mainlayout->addSpacerItem(checkboxSpace); + connect(checkbox_, &QCheckBox::stateChanged, this, + &SelectionRow::checkboxUpdated); +} +void SelectionRow::checkboxUpdated() +{ + emit didChangeSelectedStatus(propertyKey_, checkbox_->isChecked()); +} + +void SelectionRow::updateLabel() +{ + checkboxValueString_ = name_ + separator_ + valueLabel_; + checkbox_->setText(checkboxValueString_); +} + +void SelectionRow::setName(QString newName) +{ + name_ = newName; + updateLabel(); +} + +QString SelectionRow::getName() +{ + return name_; +} + +void SelectionRow::setValueLabel(QString newLabel) +{ + valueLabel_ = newLabel; + updateLabel(); +} + +QString SelectionRow::getValueLabel() +{ + return valueLabel_; +} + +void SelectionRow::setPropertyKey(QString newKey) +{ + propertyKey_ = newKey; +} + +QString SelectionRow::getPropertyKey() +{ + return propertyKey_; +} + +} // namespace StreamWizard diff --git a/UI/pre-stream-wizard/setting-selection-row.hpp b/UI/pre-stream-wizard/setting-selection-row.hpp new file mode 100644 index 00000000000000..1492d3b7981b7d --- /dev/null +++ b/UI/pre-stream-wizard/setting-selection-row.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "pre-stream-current-settings.hpp" + +namespace StreamWizard { + +/* +* Shows user encoder configuration options and allows them to select and apply +* each. For exmaple can apply a resolution limit but opt-out of using b-frames +* even if recommended. +*/ + +class SelectionRow : public QWidget { + Q_OBJECT + +public: + SelectionRow(QWidget *parent = nullptr); + + // User facing name to be shown to the user. ("Resolution") + void setName(QString newName); + QString getName(); + + // Value in a user-presentable manner. ("1920x180p") + void setValueLabel(QString newValue); + QString getValueLabel(); + + // Key to mapping property to Settings Map + void setPropertyKey(QString newKey); + QString getPropertyKey(); + +signals: + // User changed if they want to apply an option + void didChangeSelectedStatus(QString propertyKey, bool selected); + +private: + void createLayout(); + void checkboxUpdated(); + void updateLabel(); + + // Visual from upper class + QString name_; + QString valueLabel_; + QString propertyKey_; + + // Layout and subwidgets + QString separator_ = ": "; + QString checkboxValueString_; + QCheckBox *checkbox_; + +}; // class SelectionRow + +} // namespace StreamWizard diff --git a/UI/streaming-settings-util.cpp b/UI/streaming-settings-util.cpp new file mode 100644 index 00000000000000..ce80fde3c4078f --- /dev/null +++ b/UI/streaming-settings-util.cpp @@ -0,0 +1,212 @@ +#include + +#include "common-settings.hpp" +#include + +#include + +QSharedPointer +StreamingSettingsUtility::makeEncoderSettingsFromCurrentState(config_t *config) +{ + QSharedPointer currentSettings( + new StreamWizard::EncoderSettingsRequest()); + + /* Stream info */ + + currentSettings->videoType = StreamWizard::VideoType::live; + // only live and rmpts is supported for now + currentSettings->protocol = StreamWizard::StreamProtocol::rtmps; + /* Video */ + currentSettings->videoWidth = + (int)config_get_uint(config, "Video", "OutputCX"); + currentSettings->videoHeight = + (int)config_get_uint(config, "Video", "OutputCY"); + currentSettings->framerate = CommonSettings::GetConfigFPSDouble(config); + + currentSettings->videoBitrate = + CommonSettings::GetVideoBitrateInUse(config); + currentSettings->videoCodec = StreamWizard::VideoCodec::h264; + + /* Audio */ + currentSettings->audioChannels = + CommonSettings::GetAudioChannelCount(config); + currentSettings->audioSamplerate = + config_get_default_int(config, "Audio", "SampleRate"); + currentSettings->audioBitrate = + CommonSettings::GetStreamingAudioBitrate(config); + //For now, only uses AAC + currentSettings->audioCodec = StreamWizard::AudioCodec::aac; + + return currentSettings; +}; + +/* +* StreamWizard::SettingsMap is sparse in that it wont have all keys +* as well, the user can opt out of settings being applied. +* The map looks like [ kSettingsResponseKeys : QPair ] +* AKA [KEY : SetttingPair ] +* if the key is available, it means the wizard provider added a value for it +* but the bool in the QPair is false if the user selected in the wizard not to +* apply the setting. A case would be we suggest a 720p stream but the user +* knows their account supports 1080 so disabled the setting from applying. + +* Returns TRUE is map contrains something for the key and it is marked true +*/ +bool CheckInMapAndSelected(StreamWizard::SettingsMap *map, QString key) +{ + if (!map->contains(key)) { + return false; + } + const QPair settingPair = map->value(key); + return settingPair.second; +} + +// Helper Functions for ::applyWizardSettings +int intFromMap(StreamWizard::SettingsMap *map, QString key) +{ + QPair settingPair = map->value(key); + QVariant data = settingPair.first; + return data.toInt(); +} + +QString stringFromMap(StreamWizard::SettingsMap *map, QString key) +{ + QPair settingPair = map->value(key); + QVariant data = settingPair.first; + return data.toString(); +} + +double doubleFromMap(StreamWizard::SettingsMap *map, QString key) +{ + QPair settingPair = map->value(key); + QVariant data = settingPair.first; + return data.toDouble(); +} + +bool boolFromMap(StreamWizard::SettingsMap *map, QString key) +{ + QPair settingPair = map->value(key); + QVariant data = settingPair.first; + return data.toBool(); +} + +/* +* Given a settings map [ kSettingsResponseKeys : QPair ] apply +* settings that are in the sparse map as well selected by the user (which is +* marked by the bool in the pair). +* Apply to Basic encoder settings. +* Possible later goal: autoconfig advanced settings too +*/ +void StreamingSettingsUtility::applyWizardSettings( + QSharedPointer newSettings, config_t *config) +{ + + if (newSettings == nullptr || newSettings.isNull()) + return; + + // scope to function usage + using namespace StreamWizard; + + SettingsMap *map = newSettings.data(); + + QStringList x264SettingList; + config_set_bool(config, "SimpleOutput", "UseAdvanced", true); + + // Resolution must have both + if (CheckInMapAndSelected(map, kSettingsResponseKeys.videoHeight) && + CheckInMapAndSelected(map, kSettingsResponseKeys.videoWidth)) { + + int canvasX = intFromMap(map, kSettingsResponseKeys.videoWidth); + int canvasY = + intFromMap(map, kSettingsResponseKeys.videoHeight); + config_set_uint(config, "Video", "OutputCX", canvasX); + config_set_uint(config, "Video", "OutputCY", canvasY); + } + + //TODO: FPS is hacky but covers all integer and standard drop frame cases + if (CheckInMapAndSelected(map, kSettingsResponseKeys.framerate)) { + double currentFPS = CommonSettings::GetConfigFPSDouble(config); + double newFPS = + doubleFromMap(map, kSettingsResponseKeys.framerate); + if (abs(currentFPS - newFPS) > + 0.001) { // Only change if different + if (abs(floor(newFPS) - newFPS) > + 0.01) { // Is a drop-frame FPS + int num = ceil(newFPS) * 1000; + int den = 1001; + config_set_uint(config, "Video", "FPSType", + 2); // Fraction + config_set_uint(config, "Video", "FPSNum", num); + config_set_uint(config, "Video", "FPSDen", den); + } else { // Is integer FPS + config_set_uint(config, "Video", "FPSType", + 1); // Integer + config_set_uint(config, "Video", "FPSInt", + (int)floor(newFPS)); + } + } + } + + if (CheckInMapAndSelected(map, kSettingsResponseKeys.videoBitrate)) { + int newBitrate = + intFromMap(map, kSettingsResponseKeys.videoBitrate); + config_set_int(config, "SimpleOutput", "VBitrate", newBitrate); + } + + if (CheckInMapAndSelected(map, kSettingsResponseKeys.h264Profile)) { + QString profile = + stringFromMap(map, kSettingsResponseKeys.h264Profile); + profile = profile.toLower(); + x264SettingList.append("profile=" + profile); + } + + if (CheckInMapAndSelected(map, kSettingsResponseKeys.h264Level)) { + QString levelString = + stringFromMap(map, kSettingsResponseKeys.h264Level); + x264SettingList.append("level=" + levelString); + } + + if (CheckInMapAndSelected(map, kSettingsResponseKeys.gopSizeInFrames)) { + int gopSize = + intFromMap(map, kSettingsResponseKeys.gopSizeInFrames); + x264SettingList.append("keyint=" + QString::number(gopSize)); + } + + if (CheckInMapAndSelected(map, kSettingsResponseKeys.gopClosed)) { + bool gopClose = + boolFromMap(map, kSettingsResponseKeys.gopClosed); + if (gopClose) { + x264SettingList.append("open_gop=0"); + } else { + x264SettingList.append("open_gop=1"); + } + } + + if (CheckInMapAndSelected(map, kSettingsResponseKeys.gopBFrames)) { + int bFrames = intFromMap(map, kSettingsResponseKeys.gopBFrames); + x264SettingList.append("bframes=" + QString::number(bFrames)); + } + + if (CheckInMapAndSelected(map, kSettingsResponseKeys.gopRefFrames)) { + int refFrames = + intFromMap(map, kSettingsResponseKeys.gopRefFrames); + x264SettingList.append("ref=" + QString::number(refFrames)); + } + + // kSettingsResponseKeys.streamRateControlMode defaults to CBR in Simple + // encoder mode. Can add later for advanced panel. + + if (CheckInMapAndSelected(map, + kSettingsResponseKeys.streamBufferSize)) { + int bufferSize = + intFromMap(map, kSettingsResponseKeys.streamBufferSize); + x264SettingList.append("vbv-bufsize=" + + QString::number(bufferSize)); + } + + std::string x264String = + x264SettingList.join(" ").toUtf8().toStdString(); + const char *x264_c_String = x264String.c_str(); + config_set_string(config, "SimpleOutput", "x264Settings", + x264_c_String); +}; diff --git a/UI/streaming-settings-util.hpp b/UI/streaming-settings-util.hpp new file mode 100644 index 00000000000000..3bf4c39835e233 --- /dev/null +++ b/UI/streaming-settings-util.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include +#include "pre-stream-current-settings.hpp" + +#include "obs-app.hpp" + +class StreamingSettingsUtility : public QObject { + Q_OBJECT + +public: + // Uses current settings in OBS + static QSharedPointer + makeEncoderSettingsFromCurrentState(config_t *config); + + static void applyWizardSettings( + QSharedPointer newSettings, + config_t *config); +}; diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index 1cf9224afab5fb..8679b4edbae98c 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -1,6 +1,7 @@ #include #include #include +#include "common-settings.hpp" #include "qt-wrappers.hpp" #include "audio-encoders.hpp" #include "window-basic-main.hpp" @@ -275,7 +276,6 @@ struct SimpleOutput : BasicOutputHandler { virtual void Update() override; void SetupOutputs() override; - int GetAudioBitrate() const; void LoadRecordingPreset_h264(const char *encoder); void LoadRecordingPreset_Lossless(); @@ -406,8 +406,10 @@ SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) LoadStreamingPreset_h264("obs_x264"); } - if (!CreateAACEncoder(aacStreaming, aacStreamEncID, GetAudioBitrate(), - "simple_aac", 0)) + if (!CreateAACEncoder( + aacStreaming, aacStreamEncID, + CommonSettings::GetSimpleAudioBitrate(main->Config()), + "simple_aac", 0)) throw "Failed to create aac streaming encoder (simple output)"; LoadRecordingPreset(); @@ -464,14 +466,6 @@ SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) "stopping", OBSRecordStopping, this); } -int SimpleOutput::GetAudioBitrate() const -{ - int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", - "ABitrate"); - - return FindClosestAvailableAACBitrate(bitrate); -} - void SimpleOutput::Update() { obs_data_t *h264Settings = obs_data_create(); @@ -479,7 +473,8 @@ void SimpleOutput::Update() int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); - int audioBitrate = GetAudioBitrate(); + int audioBitrate = + CommonSettings::GetSimpleAudioBitrate(main->Config()); bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); bool enforceBitrate = config_get_bool(main->Config(), "SimpleOutput", @@ -804,7 +799,9 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) if (strcmp(codec, "aac") != 0) { const char *id = FindAudioEncoderFromCodec(codec); - int audioBitrate = GetAudioBitrate(); + int audioBitrate = + CommonSettings::GetSimpleAudioBitrate( + main->Config()); obs_data_t *settings = obs_data_create(); obs_data_set_int(settings, "bitrate", audioBitrate); @@ -1076,7 +1073,6 @@ struct AdvancedOutput : BasicOutputHandler { inline void SetupRecording(); inline void SetupFFmpeg(); void SetupOutputs() override; - int GetAudioBitrate(size_t i) const; virtual bool SetupStreaming(obs_service_t *service) override; virtual bool StartStreaming(obs_service_t *service) override; @@ -1090,26 +1086,6 @@ struct AdvancedOutput : BasicOutputHandler { virtual bool ReplayBufferActive() const override; }; -static OBSData GetDataFromJsonFile(const char *jsonFile) -{ - char fullPath[512]; - obs_data_t *data = nullptr; - - int ret = GetProfilePath(fullPath, sizeof(fullPath), jsonFile); - if (ret > 0) { - BPtr jsonData = os_quick_read_utf8_file(fullPath); - if (!!jsonData) { - data = obs_data_create_from_json(jsonData); - } - } - - if (!data) - data = obs_data_create(); - OBSData dataRet(data); - obs_data_release(data); - return dataRet; -} - static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) { @@ -1136,8 +1112,10 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; - OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); - OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + OBSData streamEncSettings = + CommonSettings::GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = + CommonSettings::GetDataFromJsonFile("recordEncoder.json"); const char *rate_control = obs_data_get_string( useStreamEncoder ? streamEncSettings : recordEncSettings, @@ -1215,8 +1193,11 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) char name[9]; sprintf(name, "adv_aac%d", i); - if (!CreateAACEncoder(aacTrack[i], aacEncoderID[i], - GetAudioBitrate(i), name, i)) + if (!CreateAACEncoder( + aacTrack[i], aacEncoderID[i], + CommonSettings::GetAdvancedAudioBitrateForTrack( + main->Config(), i), + name, i)) throw "Failed to create audio encoder " "(advanced output)"; } @@ -1224,7 +1205,9 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) std::string id; int streamTrack = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; - if (!CreateAACEncoder(streamAudioEnc, id, GetAudioBitrate(streamTrack), + if (!CreateAACEncoder(streamAudioEnc, id, + CommonSettings::GetAdvancedAudioBitrateForTrack( + main->Config(), streamTrack), "avc_aac_stream", streamTrack)) throw "Failed to create streaming audio encoder " "(advanced output)"; @@ -1246,7 +1229,8 @@ void AdvancedOutput::UpdateStreamSettings() const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); - OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + OBSData settings = + CommonSettings::GetDataFromJsonFile("streamEncoder.json"); ApplyEncoderDefaults(settings, h264Streaming); if (applyServiceSettings) @@ -1268,7 +1252,8 @@ void AdvancedOutput::UpdateStreamSettings() inline void AdvancedOutput::UpdateRecordingSettings() { - OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + OBSData settings = + CommonSettings::GetDataFromJsonFile("recordEncoder.json"); obs_encoder_update(h264Recording, settings); } @@ -1456,7 +1441,10 @@ inline void AdvancedOutput::UpdateAudioSettings() for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { settings[i] = obs_data_create(); - obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i)); + obs_data_set_int( + settings[i], "bitrate", + CommonSettings::GetAdvancedAudioBitrateForTrack( + main->Config(), i)); } for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { @@ -1505,16 +1493,6 @@ void AdvancedOutput::SetupOutputs() SetupRecording(); } -int AdvancedOutput::GetAudioBitrate(size_t i) const -{ - static const char *names[] = { - "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", - "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", - }; - int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); - return FindClosestAvailableAACBitrate(bitrate); -} - bool AdvancedOutput::SetupStreaming(obs_service_t *service) { int streamTrack = diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index e2ca1eb47495f5..7c8c953aab8f22 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -51,6 +51,7 @@ #include "window-log-reply.hpp" #include "window-projector.hpp" #include "window-remux.hpp" +#include "common-settings.hpp" #include "qt-wrappers.hpp" #include "context-bar-controls.hpp" #include "obs-proxy-style.hpp" @@ -3910,7 +3911,7 @@ int OBSBasic::ResetVideo() struct obs_video_info ovi; int ret; - GetConfigFPS(ovi.fps_num, ovi.fps_den); + CommonSettings::GetConfigFPS(basicConfig, ovi.fps_num, ovi.fps_den); const char *colorFormat = config_get_string(basicConfig, "Video", "ColorFormat"); @@ -6510,75 +6511,6 @@ void OBSBasic::ToggleAlwaysOnTop() show(); } -void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const -{ - const char *val = config_get_string(basicConfig, "Video", "FPSCommon"); - - if (strcmp(val, "10") == 0) { - num = 10; - den = 1; - } else if (strcmp(val, "20") == 0) { - num = 20; - den = 1; - } else if (strcmp(val, "24 NTSC") == 0) { - num = 24000; - den = 1001; - } else if (strcmp(val, "25 PAL") == 0) { - num = 25; - den = 1; - } else if (strcmp(val, "29.97") == 0) { - num = 30000; - den = 1001; - } else if (strcmp(val, "48") == 0) { - num = 48; - den = 1; - } else if (strcmp(val, "50 PAL") == 0) { - num = 50; - den = 1; - } else if (strcmp(val, "59.94") == 0) { - num = 60000; - den = 1001; - } else if (strcmp(val, "60") == 0) { - num = 60; - den = 1; - } else { - num = 30; - den = 1; - } -} - -void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(basicConfig, "Video", "FPSInt"); - den = 1; -} - -void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const -{ - num = (uint32_t)config_get_uint(basicConfig, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(basicConfig, "Video", "FPSDen"); -} - -void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const -{ - num = 1000000000; - den = (uint32_t)config_get_uint(basicConfig, "Video", "FPSNS"); -} - -void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const -{ - uint32_t type = config_get_uint(basicConfig, "Video", "FPSType"); - - if (type == 1) //"Integer" - GetFPSInteger(num, den); - else if (type == 2) //"Fraction" - GetFPSFraction(num, den); - else if (false) //"Nanoseconds", currently not implemented - GetFPSNanoseconds(num, den); - else - GetFPSCommon(num, den); -} - config_t *OBSBasic::Config() const { return basicConfig; diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 9d3601b618de16..181bc3c1445425 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -334,12 +334,6 @@ class OBSBasic : public OBSMainWindow { void TimedCheckForUpdates(); void CheckForUpdates(bool manualUpdate); - void GetFPSCommon(uint32_t &num, uint32_t &den) const; - void GetFPSInteger(uint32_t &num, uint32_t &den) const; - void GetFPSFraction(uint32_t &num, uint32_t &den) const; - void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; - void GetConfigFPS(uint32_t &num, uint32_t &den) const; - void UpdatePreviewScalingMenu(); void LoadSceneListOrder(obs_data_array_t *array); diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index b1586d6e97a511..b760ffae1aa5cb 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -7,6 +7,7 @@ #include "window-basic-main.hpp" #include "qt-wrappers.hpp" #include "url-push-button.hpp" +#include "pre-stream-wizard.hpp" #ifdef BROWSER_AVAILABLE #include @@ -77,6 +78,8 @@ void OBSBasicSettings::InitStreamPage() this, SLOT(UpdateKeyLink())); connect(ui->service, SIGNAL(currentIndexChanged(int)), this, SLOT(UpdateMoreInfoLink())); + connect(ui->settingWizardBtn, SIGNAL(clicked(bool)), this, + SLOT(preStreamWizardLaunch())); } void OBSBasicSettings::LoadStream1Settings() @@ -249,6 +252,7 @@ void OBSBasicSettings::UpdateKeyLink() QString serviceName = ui->service->currentText(); QString customServer = ui->customServer->text(); QString streamKeyLink; + bool hasStreamWizard = false; if (serviceName == "Twitch") { streamKeyLink = "https://dashboard.twitch.tv/settings/stream"; } else if (serviceName.startsWith("YouTube")) { @@ -260,6 +264,7 @@ void OBSBasicSettings::UpdateKeyLink() (customServer.contains("fbcdn.net") && IsCustomService())) { streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS"; + hasStreamWizard = true; } else if (serviceName.startsWith("Twitter")) { streamKeyLink = "https://www.pscp.tv/account/producer"; } else if (serviceName.startsWith("YouStreamer")) { @@ -274,6 +279,55 @@ void OBSBasicSettings::UpdateKeyLink() ui->getStreamKeyButton->setTargetUrl(QUrl(streamKeyLink)); ui->getStreamKeyButton->show(); } + + hasStreamWizard &= ui->outputMode->currentText().contains("Simple"); + ui->settingWizardBtn->setHidden(!hasStreamWizard); +} + +void OBSBasicSettings::preStreamWizardLaunch() +{ + StreamWizard::Destination dest; + // Use UI to detect service. + QString serviceName = ui->service->currentText(); + QString customServer = ui->customServer->text(); + if (serviceName == "Facebook Live" || + (IsCustomService() && customServer.contains("fbcdn.net"))) { + dest = StreamWizard::Destination::Facebook; + } else { + blog(LOG_WARNING, + "Showed wizard button for service not supported"); + return; + } + + // Save any changes so far since we'll refrence them from config files + SaveSettings(); + + QSharedPointer currentSettings = + StreamingSettingsUtility::makeEncoderSettingsFromCurrentState( + main->Config()); + + StreamWizard::PreStreamWizard *wiz = new StreamWizard::PreStreamWizard( + dest, StreamWizard::LaunchContext::Settings, currentSettings, + this); + + connect(wiz, &StreamWizard::PreStreamWizard::applySettings, this, + &OBSBasicSettings::preStreamWizardApplySettings); + + // Show wizard over settings + wiz->exec(); +} + +void OBSBasicSettings::preStreamWizardApplySettings( + QSharedPointer newSettings) +{ + blog(LOG_INFO, "OBSBasicSettings::preStreamWizardApplySettings"); + + // Apply + StreamingSettingsUtility::applyWizardSettings(newSettings, + main->Config()); + + // Reload + LoadSettings(false); } void OBSBasicSettings::LoadServices(bool showAll) diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 80ed5144b85070..57b9065dbb7e86 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -660,6 +660,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) SLOT(UpdateStreamDelayEstimate())); connect(ui->outputMode, SIGNAL(currentIndexChanged(int)), this, SLOT(UpdateStreamDelayEstimate())); + connect(ui->outputMode, SIGNAL(currentIndexChanged(int)), this, + SLOT(UpdateKeyLink())); connect(ui->simpleOutputVBitrate, SIGNAL(valueChanged(int)), this, SLOT(UpdateStreamDelayEstimate())); connect(ui->simpleOutputABitrate, SIGNAL(currentIndexChanged(int)), diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index 89f6795061f25d..1d83a87efa6012 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -29,6 +29,7 @@ #include #include "auth-base.hpp" +#include "common-settings.hpp" class OBSBasic; class QAbstractButton; @@ -39,6 +40,7 @@ class OBSPropertiesView; class OBSHotkeyWidget; #include "ui_OBSBasicSettings.h" +#include "streaming-settings-util.hpp" #define VOLUME_METER_DECAY_FAST 23.53 #define VOLUME_METER_DECAY_MEDIUM 11.76 @@ -240,6 +242,9 @@ private slots: void UpdateServerList(); void UpdateKeyLink(); void UpdateMoreInfoLink(); + void preStreamWizardLaunch(); + void preStreamWizardApplySettings( + QSharedPointer newSettings); void on_show_clicked(); void on_authPwShow_clicked(); void on_connectAccount_clicked();