Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
62 changes: 58 additions & 4 deletions obs-studio-server/source/osn-video.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
#include "osn-video.hpp"
#include <ipc-server.hpp>
#include <obs.h>
#include <cstring>
#include <mutex>
#include <unordered_map>
#include "osn-error.hpp"
#include "shared.hpp"
#include "osn-streaming.hpp"
Expand Down Expand Up @@ -311,6 +314,49 @@ enum video_range_type osn::Video::ColoRangeFromStr(const std::string &value)
return VIDEO_RANGE_DEFAULT;
}

// Settings last applied successfully to each canvas through SetVideoContext. libobs rebuilds the
// canvas' whole video pipeline on every obs_set_video_info call (~130 ms), even when nothing
// changes, so identical re-applies are skipped. Keyed on the canvas pointer; entries are
// dropped when the canvas is removed.
static std::mutex lastAppliedVideoMutex;
static std::unordered_map<const obs_video_info *, obs_video_info> lastAppliedVideo;

static bool SameVideoInfo(const obs_video_info &a, const obs_video_info &b)
{
const char *ga = a.graphics_module ? a.graphics_module : "";
const char *gb = b.graphics_module ? b.graphics_module : "";
return strcmp(ga, gb) == 0 && a.fps_num == b.fps_num && a.fps_den == b.fps_den && a.fps_type == b.fps_type && a.base_width == b.base_width &&
a.base_height == b.base_height && a.output_width == b.output_width && a.output_height == b.output_height && a.output_format == b.output_format &&
a.colorspace == b.colorspace && a.range == b.range && a.scale_type == b.scale_type && a.adapter == b.adapter &&
a.gpu_conversion == b.gpu_conversion;
}

static bool VideoInfoUnchanged(const obs_video_info *canvas, const obs_video_info &requested)
{
std::lock_guard<std::mutex> lock(lastAppliedVideoMutex);
const auto it = lastAppliedVideo.find(canvas);
return it != lastAppliedVideo.end() && SameVideoInfo(it->second, requested);
}

static void RememberAppliedVideoInfo(const obs_video_info *canvas, const obs_video_info &applied)
{
std::lock_guard<std::mutex> lock(lastAppliedVideoMutex);
lastAppliedVideo[canvas] = applied;
}

static void ForgetAppliedVideoInfo(const obs_video_info *canvas)
{
std::lock_guard<std::mutex> lock(lastAppliedVideoMutex);
lastAppliedVideo.erase(canvas);
}

static void ApplyConfiguredVideoLevels()
{
const float sdr_white_level = (float)config_get_uint(ConfigManager::getInstance().getBasic(), "Video", "SdrWhiteLevel");
const float hdr_nominal_peak_level = (float)config_get_uint(ConfigManager::getInstance().getBasic(), "Video", "HdrNominalPeakLevel");
obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level);
}

void osn::Video::SetVideoContext(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval)
{
blog(LOG_INFO, "[VIDEO_CANVAS] Set video context called");
Expand Down Expand Up @@ -357,6 +403,14 @@ void osn::Video::SetVideoContext(void *data, const int64_t id, const std::vector
video.gpu_conversion = true;
video.fps_type = args[10].value_union.ui32;

if (VideoInfoUnchanged(canvas, video)) {
blog(LOG_INFO, "[VIDEO_CANVAS] Set video context skipped for 0x%" PRIXPTR ": settings unchanged", (uintptr_t)canvas);
ApplyConfiguredVideoLevels();
rval.push_back(ipc::value((uint64_t)ErrorCode::Ok));
AUTO_DEBUG;
return;
}

// Updating a canvas has the same libobs restriction as removing it. Cancel
// active Auto Optimizer work and release its temporary outputs first.
if (!autoOptimizer::CancelActiveSession()) {
Expand All @@ -379,10 +433,8 @@ void osn::Video::SetVideoContext(void *data, const int64_t id, const std::vector
blog(LOG_ERROR, "Failed to set video context");
rval.push_back(ipc::value((uint64_t)ErrorCode::Error));
} else {
const float sdr_white_level = (float)config_get_uint(ConfigManager::getInstance().getBasic(), "Video", "SdrWhiteLevel");
const float hdr_nominal_peak_level = (float)config_get_uint(ConfigManager::getInstance().getBasic(), "Video", "HdrNominalPeakLevel");
obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level);

RememberAppliedVideoInfo(canvas, video);
Comment thread
summeroff marked this conversation as resolved.
ApplyConfiguredVideoLevels();
rval.push_back(ipc::value((uint64_t)ErrorCode::Ok));
}

Expand Down Expand Up @@ -442,13 +494,15 @@ void osn::Video::RemoveVideoContext(void *data, const int64_t id, const std::vec
if (ret == OBS_VIDEO_INFO_IN_USE) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Cannot remove video context while scene items are assigned to it.");
} else if (ret == OBS_VIDEO_REINITIALIZATION_FAILED) {
ForgetAppliedVideoInfo(canvas);
osn::Video::Manager::GetInstance().free(args[0].value_union.ui64);
PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Video context was removed, but the remaining video contexts failed to initialize.");
} else if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Cannot remove video context while video is active.");
} else if (ret != OBS_VIDEO_SUCCESS) {
PRETTY_ERROR_RETURN(ErrorCode::Error, "Failed to remove video context.");
} else {
ForgetAppliedVideoInfo(canvas);
osn::Video::Manager::GetInstance().free(args[0].value_union.ui64);
rval.push_back(ipc::value((uint64_t)ErrorCode::Ok));
}
Expand Down
35 changes: 35 additions & 0 deletions tests/osn-tests/src/test_osn_video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,41 @@ describe(testName, () => {
context.destroy();
});

it('Re-applying identical video settings is accepted and later changes still apply', () => {
const context = osn.VideoFactory.create();
const videoInfo: osn.IVideoInfo = {
fpsNum: 60,
fpsDen: 1,
baseWidth: 1280,
baseHeight: 720,
outputWidth: 1280,
outputHeight: 720,
outputFormat: osn.EVideoFormat.NV12,
colorspace: osn.EColorSpace.CS709,
range: osn.ERangeType.Partial,
scaleType: osn.EScaleType.Bicubic,
fpsType: EFPSType.Fractional
};
context.video = videoInfo;
// identical re-applies: must succeed and read back unchanged
context.video = videoInfo;
context.video = { ...videoInfo };
Comment thread
summeroff marked this conversation as resolved.
let current = context.video;
expect(current.baseWidth).to.equal(1280, GetErrorMessage(ETestErrorMsg.VideoSetBaseWidth));
expect(current.baseHeight).to.equal(720, GetErrorMessage(ETestErrorMsg.VideoSetBaseHeight));
expect(current.outputWidth).to.equal(1280, GetErrorMessage(ETestErrorMsg.VideoSetOutputWidth));
expect(current.fpsNum).to.equal(60, GetErrorMessage(ETestErrorMsg.VideoSetFPSNum));
expect(current.scaleType).to.equal(osn.EScaleType.Bicubic, GetErrorMessage(ETestErrorMsg.VideoSetScaleType));
// a real change after the no-op re-applies must still be applied
context.video = { ...videoInfo, baseWidth: 1920, baseHeight: 1080, outputWidth: 1920, outputHeight: 1080, fpsNum: 30 };
current = context.video;
expect(current.baseWidth).to.equal(1920, GetErrorMessage(ETestErrorMsg.VideoSetBaseWidth));
expect(current.baseHeight).to.equal(1080, GetErrorMessage(ETestErrorMsg.VideoSetBaseHeight));
expect(current.outputWidth).to.equal(1920, GetErrorMessage(ETestErrorMsg.VideoSetOutputWidth));
expect(current.fpsNum).to.equal(30, GetErrorMessage(ETestErrorMsg.VideoSetFPSNum));
context.destroy();
});

it('Get video capture devices', function() {
const devices = osn.NodeObs.OBS_settings_getVideoDevices();
expect(devices).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.VideoDevices));
Expand Down
Loading