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
2 changes: 2 additions & 0 deletions obs-studio-server/source/nodeobs_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,11 @@ int OBS_service::doResetVideoContext(obs_video_info *ovi)
base_canvas = obs_create_video_info();

int ret = obs_set_video_info(base_canvas, ovi);
osn::Video::InvalidateAppliedVideoInfo();

return ret;
} catch (const char *error) {
osn::Video::InvalidateAppliedVideoInfo();
blog(LOG_ERROR, "Failed to reset video: %s", error);
return OBS_VIDEO_FAIL;
}
Expand Down
1 change: 1 addition & 0 deletions obs-studio-server/source/nodeobs_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3781,6 +3781,7 @@ bool OBS_settings::saveSettings(std::string nameCategory, std::vector<SubCategor
struct obs_video_info ovi = {0};
obs_get_video_info(&ovi);
const int resetResult = obs_reset_video(&ovi);
osn::Video::InvalidateAppliedVideoInfo();
if (resetResult != OBS_VIDEO_SUCCESS) {
blog(LOG_ERROR, "Failed to reset video while applying advanced settings: %d", resetResult);
return false;
Expand Down
71 changes: 67 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,56 @@ 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);
}

// libobs video state is process-wide, so a failed call can leave every canvas torn down.
void osn::Video::InvalidateAppliedVideoInfo()
{
std::lock_guard<std::mutex> lock(lastAppliedVideoMutex);
lastAppliedVideo.clear();
}
Comment on lines +353 to +358

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6081996: doResetVideoContext now invalidates the applied video info after obs_set_video_info (success or failure) and in the catch path.


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 +410,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 @@ -377,12 +438,11 @@ void osn::Video::SetVideoContext(void *data, const int64_t id, const std::vector

if (ret != OBS_VIDEO_SUCCESS) {
blog(LOG_ERROR, "Failed to set video context");
InvalidateAppliedVideoInfo();
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 +502,16 @@ 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) {
InvalidateAppliedVideoInfo();
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) {
InvalidateAppliedVideoInfo();
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
1 change: 1 addition & 0 deletions obs-studio-server/source/osn-video.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Video {
static void GetVideoContext(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
static void SetVideoContext(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
static void AddVideoContext(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
static void InvalidateAppliedVideoInfo();
static void RemoveVideoContext(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
static void GetLegacySettings(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
static void SetLegacySettings(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval);
Expand Down
77 changes: 77 additions & 0 deletions tests/osn-tests/src/test_osn_video.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'mocha';
import { expect } from 'chai';
import * as fs from 'fs';
import * as path from 'path';
import * as osn from '../osn';
import { logInfo, logEmptyLine } from '../util/logger';
import { OBSHandler } from '../util/obs_handler';
Expand All @@ -9,6 +11,15 @@ import { EFPSType } from '../osn';

const testName = 'osn-video';

function countSkippedSetVideoCalls(): number {
const logDir = path.join(__dirname, '..', 'osnData', 'slobs-client', 'node-obs', 'logs');
const newest = fs.readdirSync(logDir)
.map(name => ({ name, mtime: fs.statSync(path.join(logDir, name)).mtimeMs }))
.sort((a, b) => b.mtime - a.mtime)[0];
const log = fs.readFileSync(path.join(logDir, newest.name), 'utf8');
return log.split('Set video context skipped').length - 1;
}

describe(testName, () => {
let obs: OBSHandler;
let hasTestFailed: boolean = false;
Expand Down Expand Up @@ -192,6 +203,72 @@ 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;
const skippedBefore = countSkippedSetVideoCalls();
// identical re-applies: must succeed, be skipped by the server, and read back unchanged
context.video = videoInfo;
context.video = { ...videoInfo };
Comment thread
summeroff marked this conversation as resolved.
expect(countSkippedSetVideoCalls() - skippedBefore).to.equal(2, 'Identical video settings were not skipped');
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('Re-applying settings after a failed video update is not skipped', () => {
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;
expect(() => {
context.video = { ...videoInfo, baseWidth: 0, baseHeight: 0, outputWidth: 0, outputHeight: 0 };
}).to.throw();

const skippedBefore = countSkippedSetVideoCalls();
context.video = videoInfo;
expect(countSkippedSetVideoCalls()).to.equal(skippedBefore, 'Settings were skipped after a failed update');
const current = context.video;
expect(current.baseWidth).to.equal(1280, GetErrorMessage(ETestErrorMsg.VideoSetBaseWidth));
expect(current.outputHeight).to.equal(720, GetErrorMessage(ETestErrorMsg.VideoSetOutputHeight));
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