diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini
index 908af15890c577..0edc1b5fb6ea2e 100644
--- a/frontend/data/locale/en-US.ini
+++ b/frontend/data/locale/en-US.ini
@@ -1632,3 +1632,30 @@ PluginManager.Section.Discover="Browse"
PluginManager.Section.Manage="Installed"
PluginManager.Section.Updates="Updates"
PluginManager.Section.Manage.Title="Manage Enabled Plugins"
+
+# Missing Encoder Warning
+EncoderMissing.Title="Missing Encoders"
+EncoderMissing.Text="The following configured encoders are missing:\n
\nSee the Knowledge Base Article for further information."
+EncoderMissing.ItemText="\n%1
\n%2 (code: %3)\n"
+EncoderMissing.Unknown="Unknown"
+EncoderMissing.MissingModule="Module \"%1\" not loaded"
+
+## Nvenc specific errors
+EncoderMissing.NVENC.TestProgramFailedStartup="NVENC check program start failure"
+EncoderMissing.NVENC.TestProgramExitWithError="NVENC check process failure, exit code: %1"
+EncoderMissing.NVENC.TestProgramReadFailure="NVENC check process output read failure, exit code: %1"
+EncoderMissing.NVENC.TestProgramError="NVENC check failed with reason: %1"
+EncoderMissing.NVENC.NoDevices="No NVIDIA GPUs found"
+EncoderMissing.NVENC.Unsupported.Kepler="The architecture (Kepler) of your GPU (%1) is no longer supported. Please use OBS 30.2 or earlier instead."
+EncoderMissing.NVENC.Reason.NvmlLoad="Failed loading NVML library"
+EncoderMissing.NVENC.Reason.NvmlInit="Failed initializing NVML"
+EncoderMissing.NVENC.Reason.NvencLoad="Failed loading NVENC library"
+EncoderMissing.NVENC.Reason.NvencInit="Failed initializing NVENC"
+EncoderMissing.NVENC.Reason.NvencVersion="NVENC version empty"
+EncoderMissing.NVENC.Reason.CudaLoad="Failed loading CUDA library"
+EncoderMissing.NVENC.Reason.CudaInit="Failed initializing CUDA"
+EncoderMissing.NVENC.Reason.CudaVersion="Invalid/Missing CUDA version"
+EncoderMissing.NVENC.Reason.DriverOutdated="Outdated driver"
+EncoderMissing.NVENC.Reason.NoSupportedDevices="No NVIDIA devices with NVENC support found"
+EncoderMissing.NVENC.Reason.SessionLimitExceeded="Encoder session limit exceeded"
+EncoderMissing.NVENC.Reason.AV1Unsupported="AV1 encoding is not supported by any of the system's NVIDIA GPUs"
diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp
index 0e02ad3c6adbe4..a4b9f549dc6699 100644
--- a/frontend/widgets/OBSBasic.hpp
+++ b/frontend/widgets/OBSBasic.hpp
@@ -904,6 +904,7 @@ private slots:
std::vector GetRestartRequirements(const ConfigFile &config) const;
void ResetProfileData();
void CheckForSimpleModeX264Fallback();
+ void CheckForMissingEncoders();
public:
inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; };
diff --git a/frontend/widgets/OBSBasic_Profiles.cpp b/frontend/widgets/OBSBasic_Profiles.cpp
index 4c655915fe1edf..f8430032c4cd53 100644
--- a/frontend/widgets/OBSBasic_Profiles.cpp
+++ b/frontend/widgets/OBSBasic_Profiles.cpp
@@ -22,7 +22,9 @@
#endif
#include
+#include
#include
+#include
#include
#include
@@ -40,6 +42,7 @@ extern void DestroyPanelCookieManager();
extern void DuplicateCurrentCookieProfile(ConfigFile &config);
extern void CheckExistingCookieId();
extern void DeleteCookies();
+extern const char *get_simple_output_encoder(const char *name);
// MARK: - Anonymous Namespace
namespace {
@@ -734,6 +737,7 @@ void OBSBasic::ActivateProfile(const OBSProfile &profile, bool reset)
void OBSBasic::UpdateProfileEncoders()
{
InitBasicConfigDefaults2();
+ CheckForMissingEncoders();
CheckForSimpleModeX264Fallback();
}
@@ -904,3 +908,174 @@ void OBSBasic::CheckForSimpleModeX264Fallback()
activeConfiguration.SaveSafe("tmp");
}
}
+
+namespace {
+static auto args_deleter = [](os_process_args_t *args) {
+ os_process_args_destroy(args);
+};
+using OsProcessArgs = std::unique_ptr;
+using CheckResult = std::optional>;
+
+#ifndef __APPLE__
+static CheckResult CheckNVENCInternal(std::unordered_set &missing_encoders)
+{
+ const std::vector> nvencFailureReasons = {
+ {"nvml_lib", "EncoderMissing.NVENC.Reason.NvmlLoad"},
+ // Some error messages are a prefix + numeric error code
+ {"nvml_init", "EncoderMissing.NVENC.Reason.NvmlInit"},
+ {"nvenc_lib", "EncoderMissing.NVENC.Reason.NvencLoad"},
+ {"nvenc_init", "EncoderMissing.NVENC.Reason.NvencInit"},
+ {"cuda_lib", "EncoderMissing.NVENC.Reason.CudaLoad"},
+ {"cuda_init", "EncoderMissing.NVENC.Reason.CudaInit"},
+ {"no_cuda_version", "EncoderMissing.NVENC.Reason.CudaVersion"},
+ {"no_devices", "EncoderMissing.NVENC.NoDevices"},
+ {"no_nvenc_version", "EncoderMissing.NVENC.Reason.NvencVersion"},
+ {"outdated_driver", "EncoderMissing.NVENC.Reason.DriverOutdated"},
+ {"no_supported_devices", "EncoderMissing.NVENC.Reason.NoSupportedDevices"},
+ {"session_limit", "EncoderMissing.NVENC.Reason.SessionLimitExceeded"},
+ };
+ const bool includes_av1 = missing_encoders.count("obs_nvenc_av1_cuda") ||
+ missing_encoders.count("obs_nvenc_av1_soft") ||
+ missing_encoders.count("obs_nvenc_av1_tex");
+
+#ifdef _WIN32
+ BPtr test_exe = os_get_executable_path_ptr("obs-nvenc-test.exe");
+#else
+ BPtr test_exe = os_get_executable_path_ptr("obs-nvenc-test");
+#endif
+
+ OsProcessArgs args{os_process_args_create(test_exe), args_deleter};
+
+ os_process_pipe_t *pp = os_process_pipe_create2(args.get(), "r");
+ if (!pp)
+ return std::make_pair("", QTStr("EncoderMissing.NVENC.TestProgramFailedStartup"));
+
+ std::string caps_result;
+ for (;;) {
+ char data[2048];
+ size_t len = os_process_pipe_read(pp, (uint8_t *)data, sizeof(data));
+ if (!len)
+ break;
+
+ caps_result.append(data, len);
+ }
+
+ int exit_code = os_process_pipe_destroy(pp);
+
+ if (caps_result.empty())
+ return std::make_pair("", QTStr("EncoderMissing.NVENC.TestProgramExitWithError").arg(exit_code));
+
+ auto config = ConfigFile();
+ if (config.OpenString(caps_result.c_str()) != CONFIG_SUCCESS)
+ return std::make_pair("", QTStr("EncoderMissing.NVENC.TestProgramReadFailure").arg(exit_code));
+
+ /* Read devices and attempt to find reason for failure */
+ auto numDevices = config_get_uint(config, "general", "cuda_devices");
+ if (!numDevices)
+ return std::make_pair("", QTStr("EncoderMissing.NVENC.NoDevices"));
+
+ /* Check device generation (architecture) */
+ for (uint64_t i = 0; i < numDevices; i++) {
+ std::string section = "device." + std::to_string(i);
+
+ if (!config_has_user_value(config, section.c_str(), "name"))
+ continue;
+
+ const QString name(config_get_string(config, section.c_str(), "name"));
+ const std::string_view arch = config_get_string(config, section.c_str(), "architecture_name");
+
+ if (arch == "Kepler")
+ return std::make_pair("kepler", QTStr("EncoderMissing.NVENC.Unsupported.Kepler").arg(name));
+ }
+
+ /* All other specific failures of the test binary */
+ if (config_has_user_value(config, "general", "reason")) {
+ const std::string_view reason = config_get_string(config, "general", "reason");
+
+ for (auto &[code, desc] : nvencFailureReasons) {
+ if (reason.find(code) == std::string_view::npos)
+ continue;
+
+ QString ret = QTStr("EncoderMissing.NVENC.TestProgramError").arg(QTStr(desc.data()));
+ QString crumb = QString::fromUtf8(code.data());
+ return std::make_pair(crumb, ret);
+ }
+ }
+
+ /* Finally, if everything else looks good but the encoder missing is AV1, and AV1 is not supported on any GPUs,
+ * that's probably the issue the user is having (for example, when copying a profile from another machine). */
+ if (includes_av1 && !config_get_bool(config, "av1", "codec_supported"))
+ return std::make_pair("no_av1", QTStr("EncoderMissing.NVENC.Reason.AV1Unsupported"));
+
+ return std::nullopt;
+}
+
+static CheckResult CheckNVENC(std::unordered_set &missing_encoders)
+{
+ static auto result = CheckNVENCInternal(missing_encoders);
+ return result;
+}
+#endif
+
+static CheckResult CheckX264()
+{
+ static const bool module_loaded = obs_get_module("obs-x264") != nullptr;
+ /* This should be the only failure mode possible here. */
+ if (!module_loaded) {
+ return std::make_pair("plugin_not_loaded", QTStr("EncoderMissing.MissingModule").arg("obs-x264"));
+ }
+
+ return std::nullopt;
+}
+} // namespace
+
+void OBSBasic::CheckForMissingEncoders()
+{
+ constexpr QStringView kbURL(u"https://obsproject.com/kb/encoder-missing#%1");
+ const QString encoderListItem = QTStr("EncoderMissing.ItemText");
+ const QString unknownErrorText = QTStr("EncoderMissing.Unknown");
+
+ std::unordered_set missing_encoders = {
+ config_get_string(activeConfiguration, "AdvOut", "Encoder"),
+ config_get_string(activeConfiguration, "AdvOut", "RecEncoder"),
+ get_simple_output_encoder(config_get_string(activeConfiguration, "SimpleOutput", "StreamEncoder")),
+ get_simple_output_encoder(config_get_string(activeConfiguration, "SimpleOutput", "RecEncoder")),
+ };
+
+ size_t idx = 0;
+ const char *id;
+ while (obs_enum_encoder_types(idx++, &id))
+ missing_encoders.erase(id);
+
+ if (missing_encoders.empty() || (missing_encoders.size() == 1 && missing_encoders.count("none")))
+ return;
+
+ QStringView crumb;
+ QStringList encoderList;
+
+ for (auto &encoder : missing_encoders) {
+ std::optional> res;
+
+ if (encoder.find("x264") != std::string_view::npos) {
+ res = CheckX264();
+ }
+#ifndef __APPLE__
+ else if (encoder.find("nvenc") != std::string_view::npos) {
+ res = CheckNVENC(missing_encoders);
+ }
+#endif
+
+ if (res) {
+ encoderList += encoderListItem.arg(encoder.data()).arg(res->second).arg(res->first);
+ if (crumb.empty())
+ crumb = res->first;
+ } else {
+ encoderList += encoderListItem.arg(encoder.data()).arg(unknownErrorText).arg("unknown");
+ }
+ }
+
+ // Format and show error message
+ const QString encoderMissingMessage =
+ QTStr("EncoderMissing.Text").arg(encoderList.join("\n")).arg(kbURL.arg(crumb));
+ OBSMessageBox::warning(this, QTStr("EncoderMissing.Title"), encoderMissingMessage, true);
+}