Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions js/module.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ export interface IVideoEncoder extends IConfigurable, IReleasable {
readonly active: boolean;
readonly id: string;
readonly lastError: string;
update(settings: ISettings, replace?: boolean): void;
}
export interface IAudioEncoder extends IReleasable {
name: string;
Expand Down
15 changes: 14 additions & 1 deletion js/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1768,6 +1768,19 @@ export interface IVideoEncoder extends IConfigurable, IReleasable {
readonly active: boolean,
readonly id: string,
readonly lastError: string

/**
* Updates explicit encoder settings. Requires an initialized IPC connection.
* Native defaults remain defaults, including adjustments made by the encoder during initialization.
* Replacement retains the encoder object and its references held by outputs.
* Validation failures leave the existing settings unchanged.
* @param settings - User settings to apply; omitted properties retain their current values unless replace is true
* @param replace - When true, removes previous user settings before applying these settings; only allowed while inactive
* @returns No value
* @throws {TypeError} If settings is not an object or replace is not a boolean
* @throws {Error} If the encoder reference is invalid, replacement is requested while active, or the IPC update fails
*/
update(settings: ISettings, replace?: boolean): void;
}

export interface IAudioEncoder extends IReleasable {
Expand Down Expand Up @@ -2424,7 +2437,7 @@ interface IAutoOptimizer {
}

/**
* Typed Auto Optimizer surface on the add-on's otherwise dynamic export.
* Typed methods on the add-on's otherwise dynamic export.
*/
interface INodeObs {
[key: string]: any;
Expand Down
38 changes: 37 additions & 1 deletion obs-studio-client/source/nodeobs_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,41 @@ std::vector<settings::SubCategory> serializeCategory(uint32_t subCategoriesCount
return category;
}

Napi::Value settings::OBS_settings_getEncoderSettings(const Napi::CallbackInfo &info)
{
if (info.Length() != 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
Napi::TypeError::New(info.Env(), "OBS_settings_getEncoderSettings expects encoder ID, output type, and mode strings")
.ThrowAsJavaScriptException();
return info.Env().Undefined();
}

std::string encoderId = info[0].As<Napi::String>().Utf8Value();
std::string outputType = info[1].As<Napi::String>().Utf8Value();
std::string mode = info[2].As<Napi::String>().Utf8Value();
if (encoderId.empty() || (outputType != "streaming" && outputType != "recording") || (mode != "Simple" && mode != "Advanced")) {
Napi::TypeError::New(info.Env(),
"OBS_settings_getEncoderSettings requires a nonempty encoder ID, streaming or recording, and Simple or Advanced")
.ThrowAsJavaScriptException();
return info.Env().Undefined();
}

auto conn = GetConnection(info);
if (!conn)
return info.Env().Undefined();

auto response = conn->call_synchronous_helper("Settings", "OBS_settings_getEncoderSettings", {encoderId, outputType, mode});
if (!ValidateResponse(info, response))
return info.Env().Undefined();
if (response.size() != 2 || response[1].type != ipc::type::String) {
Napi::Error::New(info.Env(), "Invalid encoder settings response").ThrowAsJavaScriptException();
return info.Env().Undefined();
}

Napi::Object json = info.Env().Global().Get("JSON").As<Napi::Object>();
Napi::Function parse = json.Get("parse").As<Napi::Function>();
return parse.Call(json, {Napi::String::New(info.Env(), response[1].value_str)});
}

Napi::Value settings::OBS_settings_getSettings(const Napi::CallbackInfo &info)
{
std::string category = info[0].ToString().Utf8Value();
Expand Down Expand Up @@ -539,6 +574,7 @@ void settings::OBS_settings_setEnhancedBroadcasting(const Napi::CallbackInfo &in
void settings::Init(Napi::Env env, Napi::Object exports)
{
exports.Set(Napi::String::New(env, "OBS_settings_getSettings"), Napi::Function::New(env, settings::OBS_settings_getSettings));
exports.Set(Napi::String::New(env, "OBS_settings_getEncoderSettings"), Napi::Function::New(env, settings::OBS_settings_getEncoderSettings));
exports.Set(Napi::String::New(env, "OBS_settings_saveSettings"), Napi::Function::New(env, settings::OBS_settings_saveSettings));
exports.Set(Napi::String::New(env, "OBS_settings_isValidEncoder"), Napi::Function::New(env, settings::OBS_settings_isValidEncoder));
exports.Set(Napi::String::New(env, "OBS_settings_getListCategories"), Napi::Function::New(env, settings::OBS_settings_getListCategories));
Expand All @@ -547,4 +583,4 @@ void settings::Init(Napi::Env env, Napi::Object exports)
exports.Set(Napi::String::New(env, "OBS_settings_getVideoDevices"), Napi::Function::New(env, settings::OBS_settings_getVideoDevices));
exports.Set(Napi::String::New(env, "OBS_settings_isEnhancedBroadcasting"), Napi::Function::New(env, settings::OBS_settings_isEnhancedBroadcasting));
exports.Set(Napi::String::New(env, "OBS_settings_setEnhancedBroadcasting"), Napi::Function::New(env, settings::OBS_settings_setEnhancedBroadcasting));
}
}
1 change: 1 addition & 0 deletions obs-studio-client/source/nodeobs_settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ struct SubCategory {
void Init(Napi::Env env, Napi::Object exports);

Napi::Value OBS_settings_getSettings(const Napi::CallbackInfo &info);
Napi::Value OBS_settings_getEncoderSettings(const Napi::CallbackInfo &info);
void OBS_settings_saveSettings(const Napi::CallbackInfo &info);
Napi::Value OBS_settings_isValidEncoder(const Napi::CallbackInfo &info);

Expand Down
8 changes: 7 additions & 1 deletion obs-studio-client/source/video-encoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ void osn::VideoEncoder::Release(const Napi::CallbackInfo &info)

void osn::VideoEncoder::Update(const Napi::CallbackInfo &info)
{
if (info.Length() < 1 || !info[0].IsObject() || info[0].IsArray() || info[0].IsNull() ||
(info.Length() > 1 && !info[1].IsUndefined() && !info[1].IsBoolean())) {
Napi::TypeError::New(info.Env(), "VideoEncoder.update expects a settings object and an optional replace boolean").ThrowAsJavaScriptException();
return;
}
const bool replace = info.Length() > 1 && info[1].IsBoolean() && info[1].As<Napi::Boolean>().Value();
Napi::Object jsonObj = info[0].ToObject();
Napi::Object json = info.Env().Global().Get("JSON").As<Napi::Object>();
Napi::Function stringify = json.Get("stringify").As<Napi::Function>();
Expand All @@ -249,7 +255,7 @@ void osn::VideoEncoder::Update(const Napi::CallbackInfo &info)
if (!conn)
return;

auto response = conn->call_synchronous_helper("VideoEncoder", "Update", {ipc::value(this->uid), ipc::value(jsondata)});
auto response = conn->call_synchronous_helper("VideoEncoder", "Update", {ipc::value(this->uid), ipc::value(jsondata), ipc::value(uint32_t(replace))});
ValidateResponse(info, response);
}

Expand Down
2 changes: 2 additions & 0 deletions obs-studio-server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -625,12 +625,14 @@ if(BUILD_TESTING)
add_executable(
obs_studio_server_unit_tests
"tests/test-osn-source.cpp"
"tests/test-osn-encoder-settings.cpp"
"tests/test-osn-file-output.cpp"
"tests/test-osn-multitrack-video-output.cpp"
"tests/test-osn-scene-relative-coordinates.cpp"
"tests/test-osn-video-mix-lifecycle.cpp"
"tests/obs-setup.cpp"
"tests/obs-setup.hpp"
"tests/scoped-helpers.hpp"
)

file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" OSN_SOURCE_DIR_PATH)
Expand Down
81 changes: 81 additions & 0 deletions obs-studio-server/source/nodeobs_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ void OBS_settings::Register(ipc::server &srv)

cls->register_function(
std::make_shared<ipc::function>("OBS_settings_getSettings", std::vector<ipc::type>{ipc::type::String}, OBS_settings_getSettings));
cls->register_function(std::make_shared<ipc::function>("OBS_settings_getEncoderSettings",
std::vector<ipc::type>{ipc::type::String, ipc::type::String, ipc::type::String},
OBS_settings_getEncoderSettings));
cls->register_function(std::make_shared<ipc::function>(
"OBS_settings_saveSettings", std::vector<ipc::type>{ipc::type::String, ipc::type::UInt32, ipc::type::UInt32, ipc::type::Binary},
OBS_settings_saveSettings));
Expand All @@ -89,6 +92,84 @@ void OBS_settings::Register(ipc::server &srv)
srv.register_collection(cls);
}

void OBS_settings::OBS_settings_getEncoderSettings(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval)
{
const std::string &encoderId = args[0].value_str;
const std::string &outputType = args[1].value_str;
const std::string &mode = args[2].value_str;
if (!obs_initialized()) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "OBS must be initialized before reading encoder settings.");
}
if ((mode != "Simple" && mode != "Advanced") || (outputType != "streaming" && outputType != "recording")) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Invalid encoder settings mode or output type.");
}
if (!osn::EncoderUtils::isEncoderRegistered(encoderId) || obs_get_encoder_type(encoderId.c_str()) != OBS_ENCODER_VIDEO) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Encoder settings require a registered video encoder ID.");
}

config_t *config = ConfigManager::getInstance().getBasic();
if (mode != utility::GetSafeString(config_get_string(config, "Output", "Mode"))) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Requested encoder settings mode does not match the saved output mode.");
}
bool simple = mode == "Simple";
bool recording = outputType == "recording";
const char *section = simple ? "SimpleOutput" : "AdvOut";
std::string selected = utility::GetSafeString(config_get_string(config, section, recording ? "RecEncoder" : (simple ? "StreamEncoder" : "Encoder")));
if (recording &&
((simple && strcmp(utility::GetSafeString(config_get_string(config, section, "RecQuality")), "Stream") == 0) || (!simple && selected == "none"))) {
recording = false;
selected = utility::GetSafeString(config_get_string(config, section, simple ? "StreamEncoder" : "Encoder"));
}
// Match the existing JIM encoder migration without changing the saved selection during a read.
if (osn::EncoderUtils::isOldJimNvencEncoder(selected))
selected = ENCODER_NVENC_H264_TEX;

std::string selectedId = selected;
if (simple) {
bool found = false;
for (const auto &encoder : osn::EncoderUtils::videoEncoderOptions) {
if (!encoder.simple_name.empty() && encoder.simple_name == selected) {
found = true;
break;
}
}
if (!found) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Saved simple encoder selection is invalid.");
}
selectedId = osn::EncoderUtils::getInternalEncoderFromSimple(selected.c_str());
}
if (selectedId != encoderId) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Requested encoder does not match the saved output encoder.");
}

OBSDataAutoRelease settings = obs_data_create();
if (simple) {
// Simple recording quality and service restrictions are applied by the output when it starts.
if (!recording) {
OBSDataAutoRelease simpleSettings = osn::EncoderUtils::getSimpleStreamingEncoderSettings(selected.c_str());
obs_data_apply(settings, simpleSettings);
}
} else {
std::string path = recording ? ConfigManager::getInstance().getRecord() : ConfigManager::getInstance().getStream();
OBSDataAutoRelease savedSettings = obs_data_create_from_json_file(path.c_str());
// The safe file loader restores the backup by renaming it. Read it directly to keep this API read-only.
if (!savedSettings)
savedSettings = obs_data_create_from_json_file((path + ".bak").c_str());
if (!savedSettings && (os_file_exists(path.c_str()) || os_file_exists((path + ".bak").c_str()))) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Cannot read saved encoder settings or their backup.");
}
if (savedSettings) {
osn::EncoderUtils::updateNvencPresets(savedSettings, encoderId.c_str());
obs_data_apply(settings, savedSettings);
}
}

rval.push_back(ipc::value((uint64_t)ErrorCode::Ok));
// Keep defaults native so encoders can adjust them during initialization.
rval.push_back(ipc::value(obs_data_get_json(settings)));
AUTO_DEBUG;
}

void OBS_settings::OBS_settings_getSettings(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval)
{
std::string nameCategory = args[0].value_str;
Expand Down
1 change: 1 addition & 0 deletions obs-studio-server/source/nodeobs_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class OBS_settings {
static void Register(ipc::server &);

static void OBS_settings_getSettings(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
static void OBS_settings_getEncoderSettings(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
static void OBS_settings_saveSettings(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);

static void OBS_settings_isValidEncoder(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
Expand Down
1 change: 1 addition & 0 deletions obs-studio-server/source/osn-advanced-streaming.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ static bool setAudioEncoder(osn::AdvancedStreaming *streaming)
obs_data_t *settings = obs_data_create();
obs_data_set_int(settings, "bitrate", audioTrack->bitrate);
obs_encoder_update(streaming->audioEncoder, settings);
obs_encoder_set_name(streaming->audioEncoder, audioTrack->name.empty() ? "audio-encoder-streaming" : audioTrack->name.c_str());
obs_data_release(settings);
}
}
Expand Down
33 changes: 33 additions & 0 deletions obs-studio-server/source/osn-encoders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,39 @@ std::string osn::EncoderUtils::getPublicEncoderTitle(const char *encoder)
return {};
}

obs_data_t *osn::EncoderUtils::getSimpleStreamingEncoderSettings(const char *encoder)
{
config_t *config = ConfigManager::getInstance().getBasic();
obs_data_t *settings = obs_data_create();
obs_data_set_string(settings, "rate_control", "CBR");
obs_data_set_int(settings, "bitrate", config_get_uint(config, "SimpleOutput", "VBitrate"));

if (config_get_bool(config, "SimpleOutput", "UseAdvanced")) {
std::string presetType = getEncoderPreset(encoder);
const char *preset = utility::GetSafeString(config_get_string(config, "SimpleOutput", presetType.c_str()));
if (presetType == PRESET_NVENC && strlen(preset) == 0) {
const char *oldPreset = utility::GetSafeString(config_get_string(config, "SimpleOutput", PRESET_NVENC_DEP));
if (strlen(oldPreset) != 0)
preset = convertNvencSimplePreset(oldPreset);
}

std::string encoderId = getInternalEncoderFromSimple(encoder);
const char *presetProperty = "preset";
if (presetType == PRESET_QSV)
presetProperty = "target_usage";
else if (presetType == PRESET_NVENC && encoderId.compare(0, 7, "ffmpeg_") == 0)
presetProperty = "preset2";
if (strlen(preset) != 0)
obs_data_set_string(settings, presetProperty, preset);
obs_data_set_string(settings, "x264opts", utility::GetSafeString(config_get_string(config, "SimpleOutput", "x264Settings")));
}

if (getEncoderFamily(encoder) == FAMILY_APPLE)
obs_data_set_string(settings, "profile", utility::GetSafeString(config_get_string(config, "SimpleOutput", "Profile")));

return settings;
}

bool osn::EncoderUtils::isOldJimNvencEncoder(const std::string &encoderId)
{
return encoderId == ENCODER_JIM_NVENC || encoderId == ENCODER_JIM_HEVC_NVENC || encoderId == ENCODER_JIM_AV1_NVENC;
Expand Down
1 change: 1 addition & 0 deletions obs-studio-server/source/osn-encoders.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ std::string getEncoderPreset(const char *encoder);
// backend-only family constants such as FAMILY_QSV.
std::string getPublicEncoderFamily(const char *encoder);
std::string getPublicEncoderTitle(const char *encoder);
obs_data_t *getSimpleStreamingEncoderSettings(const char *encoder);
bool isOldJimNvencEncoder(const std::string &encoderId);
void convertOldJimNvencEncoder(config_t *config, const std::string &configSection, const std::string &streamEncoderSetting,
const std::string &recordingEncoderSetting);
Expand Down
48 changes: 5 additions & 43 deletions obs-studio-server/source/osn-simple-streaming.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,11 @@ static void StopTwitchSoundtrackAudio(osn::Streaming *streaming)
obs_source_release(desktopSource2);
}

void UpdateStreamingSettings_amd(obs_data_t *settings, int bitrate)
void UpdateStreamingSettings_amd(obs_data_t *settings, int bitrate, bool useAdvanced)
{
obs_data_set_string(settings, "profile", "high");
obs_data_set_string(settings, "preset", "quality");
if (!useAdvanced)
obs_data_set_string(settings, "preset", "quality");
obs_data_set_string(settings, "rate_control", "CBR");
obs_data_set_int(settings, "bitrate", bitrate);
obs_data_set_int(settings, "keyint_sec", 2);
Expand All @@ -281,7 +282,7 @@ void osn::SimpleStreaming::updateEncoders()

std::string id = obs_encoder_get_id(videoEncoder);
if (id.compare(ADVANCED_ENCODER_AMD) == 0)
UpdateStreamingSettings_amd(videoEncSettings, vBitrate);
UpdateStreamingSettings_amd(videoEncSettings, vBitrate, useAdvanced);

obs_data_set_string(videoEncSettings, "rate_control", "CBR");
obs_data_set_int(videoEncSettings, "bitrate", vBitrate);
Expand Down Expand Up @@ -458,48 +459,9 @@ obs_encoder_t *osn::ISimpleStreaming::CreateLegacyVideoEncoder()
config_save_safe(ConfigManager::getInstance().getBasic(), "tmp", nullptr);
}

obs_data_t *videoEncData = obs_data_create();
obs_data_set_string(videoEncData, "rate_control", "CBR");
obs_data_set_int(videoEncData, "bitrate", config_get_uint(ConfigManager::getInstance().getBasic(), "SimpleOutput", "VBitrate"));

bool advanced = config_get_bool(ConfigManager::getInstance().getBasic(), "SimpleOutput", "UseAdvanced");
const char *custom = utility::GetSafeString(config_get_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", "x264Settings"));

const char *preset = nullptr;

std::string presetType = osn::EncoderUtils::getEncoderPreset(encId);
obs_data_t *videoEncData = osn::EncoderUtils::getSimpleStreamingEncoderSettings(encId);
std::string encIdOBS = osn::EncoderUtils::getInternalEncoderFromSimple(encId);

preset = utility::GetSafeString(config_get_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", presetType.c_str()));

if (presetType == PRESET_NVENC) {
if (strlen(preset) == 0) {
const char *oldParamName = PRESET_NVENC_DEP;
const char *oldValue = utility::GetSafeString(config_get_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", oldParamName));
if (strlen(oldValue) != 0) {
preset = osn::EncoderUtils::convertNvencSimplePreset(oldValue);
blog(LOG_INFO, "NVENC preset converted from %s to %s", oldValue, preset);
}
}
}

if (advanced) {
obs_data_set_string(videoEncData, "preset", preset);
obs_data_set_string(videoEncData, "x264opts", custom);
}

bool enforceServiceBitrate = config_get_bool(ConfigManager::getInstance().getBasic(), "SimpleOutput", "EnforceBitrate");

if (advanced && !enforceServiceBitrate) {
obs_data_set_int(videoEncData, "bitrate", config_get_uint(ConfigManager::getInstance().getBasic(), "SimpleOutput", "VBitrate"));
}

if (osn::EncoderUtils::getEncoderFamily(encId) == FAMILY_APPLE) {
const char *profile = utility::GetSafeString(config_get_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", "Profile"));
if (profile)
obs_data_set_string(videoEncData, "profile", profile);
}

obs_encoder_t *videoEncoder = obs_video_encoder_create(encIdOBS.c_str(), "video-encoder", videoEncData, nullptr);
obs_data_release(videoEncData);

Expand Down
Loading
Loading