Skip to content
Open
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
24 changes: 24 additions & 0 deletions js/module.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ export interface IVideo {
destroy(): void;
readonly skippedFrames: number;
readonly encodedFrames: number;
readonly canvasId: number;
}
export interface IVideoFactory {
create(): IVideo;
Expand Down Expand Up @@ -965,6 +966,29 @@ export interface IAudioTrackFactory {
importLegacySettings(): void;
saveLegacySettings(): void;
}
export interface IAutoConfigResourcePercentile {
p50: number;
p95: number;
}
export interface IAutoConfigResourceGpu {
available: boolean;
vramUsedMB?: IAutoConfigResourcePercentile;
vramBudgetMB?: number;
}
export type AutoConfigResourcePhase = 'bandwidth' | 'stream_encoder' | 'recording_encoder';
export interface IAutoConfigResourceUsage {
phase: AutoConfigResourcePhase;
sampleCount: number;
durationMs: number;
cpuPct: IAutoConfigResourcePercentile;
procRamMB: IAutoConfigResourcePercentile;
gpu: IAutoConfigResourceGpu;
}
export interface IAutoConfigSummary {
complete: boolean;
resourceUsage: IAutoConfigResourceUsage[];
[key: string]: unknown;
}
export declare const enum VCamOutputType {
Invalid = 0,
SceneOutput = 1,
Expand Down
47 changes: 47 additions & 0 deletions js/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,11 @@ export interface IVideo {
* Number of total encoded frames
*/
readonly encodedFrames: number;

/**
* Server-side canvas id. Pass to APIs that reference video contexts by id.
*/
readonly canvasId: number;
}

export interface IVideoFactory {
Expand Down Expand Up @@ -1921,6 +1926,48 @@ export interface IAudioTrackFactory {
saveLegacySettings(): void;
}

// ---- Autoconfig resource-usage telemetry ----
//
// Shapes for the JSON payload of the autoconfig 'resource_usage' event, and
// the matching `resourceUsage` array inside GetAutoConfigSummary()'s JSON.
//
// p50 is the typical value during the phase; p95 is the sustained ceiling
// after dropping single-sample spikes from unrelated OS noise. min / max / avg
// are deliberately not exposed — max overweights one-off background activity
// and avg is hard to act on.

export interface IAutoConfigResourcePercentile {
p50: number;
p95: number;
}

export interface IAutoConfigResourceGpu {
available: boolean;
vramUsedMB?: IAutoConfigResourcePercentile;
vramBudgetMB?: number;
}

export type AutoConfigResourcePhase = 'bandwidth' | 'stream_encoder' | 'recording_encoder';

export interface IAutoConfigResourceUsage {
phase: AutoConfigResourcePhase;
sampleCount: number;
durationMs: number;
cpuPct: IAutoConfigResourcePercentile;
procRamMB: IAutoConfigResourcePercentile;
gpu: IAutoConfigResourceGpu;
}

// Parsed shape of NodeObs.GetAutoConfigSummary(). Only the fields the
// resource-usage feature consumes are typed; other historical fields
// (encoderDetection, videoDecision, bandwidthTest, selection) are present in
// the JSON but intentionally left as `unknown` — type them when you need them.
export interface IAutoConfigSummary {
complete: boolean;
resourceUsage: IAutoConfigResourceUsage[];
[key: string]: unknown;
}

export const enum VCamOutputType {
Invalid,
SceneOutput,
Expand Down
54 changes: 49 additions & 5 deletions obs-studio-client/source/nodeobs_autoconfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include "nodeobs_autoconfig.hpp"
#include "polling-pacer.hpp"
#include "shared.hpp"
#include "streaming.hpp"
#include <cstring>

bool autoConfig::isWorkerRunning = false;
bool autoConfig::worker_stop = true;
Expand Down Expand Up @@ -59,6 +61,10 @@ void autoConfig::worker()
data->event = response[1].value_str;
data->description = response[2].value_str;
data->percentage = response[3].value_union.fp64;
// Optional 5th payload field (added for the POC UI). Older
// servers won't include it — guard the read.
if (response.size() >= 5)
data->payload = response[4].value_str;
ac_queue_task_workers.push_back(new std::thread(&autoConfig::queueTask, data));
}
}
Expand Down Expand Up @@ -103,20 +109,37 @@ void autoConfig::stop_worker()

Napi::Value autoConfig::InitializeAutoConfig(const Napi::CallbackInfo &info)
{
Napi::Function async_callback = info[0].As<Napi::Function>();
Napi::Object serverInfo = info[1].ToObject();
std::string continent = serverInfo.Get("continent").ToString().Utf8Value();
std::string service = serverInfo.Get("service_name").ToString().Utf8Value();
if (info.Length() < 2 || !info[0].IsArray() || !info[1].IsFunction()) {
Napi::TypeError::New(info.Env(), "InitializeAutoConfig expects (streamings: IStreaming[], callback)").ThrowAsJavaScriptException();
return info.Env().Undefined();
}

Napi::Array array = info[0].As<Napi::Array>();
std::vector<uint64_t> uids(array.Length());
for (uint32_t i = 0; i < array.Length(); i++) {
if (!osn::TryUnwrapStreamingUid(array.Get(i), uids[i])) {
Napi::TypeError::New(info.Env(), "InitializeAutoConfig: streamings[i] is not an IStreaming instance").ThrowAsJavaScriptException();
return info.Env().Undefined();
}
}
std::vector<char> uidsBin(uids.size() * sizeof(uint64_t));
if (!uids.empty())
memcpy(uidsBin.data(), uids.data(), uidsBin.size());

Napi::Function async_callback = info[1].As<Napi::Function>();

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

std::vector<ipc::value> response = conn->call_synchronous_helper("AutoConfig", "InitializeAutoConfig", {continent, service});
std::vector<ipc::value> response = conn->call_synchronous_helper("AutoConfig", "InitializeAutoConfig", {ipc::value(uidsBin)});

if (!ValidateResponse(info, response))
return info.Env().Undefined();

if (isWorkerRunning)
stop_worker();

js_thread = Napi::ThreadSafeFunction::New(info.Env(), async_callback, "AutoConfig", 0, 1, [](Napi::Env) {});

start_worker();
Expand Down Expand Up @@ -178,6 +201,9 @@ void autoConfig::queueTask(AutoConfigInfo *data)
if (event_data->event.compare("error") != 0) {
result.Set(Napi::String::New(env, "percentage"), Napi::Number::New(env, event_data->percentage));
}
if (!event_data->payload.empty()) {
result.Set(Napi::String::New(env, "payload"), Napi::String::New(env, event_data->payload));
}
result.Set(Napi::String::New(env, "continent"), Napi::String::New(env, ""));

jsCallback.Call({result});
Expand Down Expand Up @@ -265,6 +291,23 @@ Napi::Value autoConfig::StartSaveSettings(const Napi::CallbackInfo &info)
return info.Env().Undefined();
}

Napi::Value autoConfig::GetAutoConfigSummary(const Napi::CallbackInfo &info)
{
auto conn = GetConnection(info);
if (!conn)
return info.Env().Undefined();

std::vector<ipc::value> response = conn->call_synchronous_helper("AutoConfig", "GetAutoConfigSummary", {});

if (!ValidateResponse(info, response))
return info.Env().Undefined();

if (response.size() < 2)
return info.Env().Undefined();

return Napi::String::New(info.Env(), response[1].value_str);
}

Napi::Value autoConfig::TerminateAutoConfig(const Napi::CallbackInfo &info)
{
auto conn = GetConnection(info);
Expand Down Expand Up @@ -293,4 +336,5 @@ void autoConfig::Init(Napi::Env env, Napi::Object exports)
exports.Set(Napi::String::New(env, "StartSaveStreamSettings"), Napi::Function::New(env, autoConfig::StartSaveStreamSettings));
exports.Set(Napi::String::New(env, "StartSaveSettings"), Napi::Function::New(env, autoConfig::StartSaveSettings));
exports.Set(Napi::String::New(env, "TerminateAutoConfig"), Napi::Function::New(env, autoConfig::TerminateAutoConfig));
exports.Set(Napi::String::New(env, "GetAutoConfigSummary"), Napi::Function::New(env, autoConfig::GetAutoConfigSummary));
}
5 changes: 5 additions & 0 deletions obs-studio-client/source/nodeobs_autoconfig.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ struct AutoConfigInfo {
std::string event;
std::string description;
double percentage = 0;
// Optional JSON payload for new event types (bandwidth_result,
// selection_decision, video_decision, encoder_detection). Empty for legacy
// events. Surfaced to JS as a "payload" property when non-empty.
std::string payload;
};

extern const char *ac_sem_name;
Expand Down Expand Up @@ -62,4 +66,5 @@ Napi::Value StartSetDefaultSettings(const Napi::CallbackInfo &info);
Napi::Value StartSaveStreamSettings(const Napi::CallbackInfo &info);
Napi::Value StartSaveSettings(const Napi::CallbackInfo &info);
Napi::Value TerminateAutoConfig(const Napi::CallbackInfo &info);
Napi::Value GetAutoConfigSummary(const Napi::CallbackInfo &info);
}
41 changes: 41 additions & 0 deletions obs-studio-client/source/streaming.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
#include "reconnect.hpp"
#include "network.hpp"
#include "video.hpp"
#include "simple-streaming.hpp"
#include "advanced-streaming.hpp"
#include "enhanced-broadcasting-simple-streaming.hpp"
#include "enhanced-broadcasting-advanced-streaming.hpp"

void osn::Streaming::ReleaseObjects()
{
Expand Down Expand Up @@ -442,3 +446,40 @@ Napi::Value osn::Streaming::GetDataOutput(const Napi::CallbackInfo &info)

return Napi::Number::New(info.Env(), response[1].value_union.fp64);
}

bool osn::TryUnwrapStreamingUid(const Napi::Value &value, uint64_t &out_uid)
{
if (!value.IsObject())
return false;
Napi::Object obj = value.As<Napi::Object>();

if (obj.InstanceOf(osn::SimpleStreaming::constructor.Value())) {
auto *s = Napi::ObjectWrap<osn::SimpleStreaming>::Unwrap(obj);
if (s) {
out_uid = s->uid;
return true;
}
}
if (obj.InstanceOf(osn::AdvancedStreaming::constructor.Value())) {
auto *s = Napi::ObjectWrap<osn::AdvancedStreaming>::Unwrap(obj);
if (s) {
out_uid = s->uid;
return true;
}
}
if (obj.InstanceOf(osn::EnhancedBroadcastingSimpleStreaming::constructor.Value())) {
auto *s = Napi::ObjectWrap<osn::EnhancedBroadcastingSimpleStreaming>::Unwrap(obj);
if (s) {
out_uid = s->uid;
return true;
}
}
if (obj.InstanceOf(osn::EnhancedBroadcastingAdvancedStreaming::constructor.Value())) {
auto *s = Napi::ObjectWrap<osn::EnhancedBroadcastingAdvancedStreaming>::Unwrap(obj);
if (s) {
out_uid = s->uid;
return true;
}
}
return false;
}
2 changes: 2 additions & 0 deletions obs-studio-client/source/streaming.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,6 @@ class Streaming : public WorkerSignals {
void Start(const Napi::CallbackInfo &info);
void Stop(const Napi::CallbackInfo &info);
};

bool TryUnwrapStreamingUid(const Napi::Value &value, uint64_t &out_uid);
}
25 changes: 12 additions & 13 deletions obs-studio-client/source/video.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Napi::Object osn::Video::Init(Napi::Env env, Napi::Object exports)

InstanceAccessor("skippedFrames", &osn::Video::GetSkippedFrames, nullptr),
InstanceAccessor("encodedFrames", &osn::Video::GetEncodedFrames, nullptr),
InstanceAccessor("canvasId", &osn::Video::GetCanvasId, nullptr),
});
exports.Set("Video", func);
osn::Video::constructor = Napi::Persistent(func);
Expand Down Expand Up @@ -105,10 +106,14 @@ void osn::Video::Destroy(const Napi::CallbackInfo &info)

auto response = conn->call_synchronous_helper("Video", "RemoveVideoContext", {ipc::value((uint64_t)(this->canvasId))});
ValidateResponse(info, response);
isLastVideoValid = false;
return;
}

Napi::Value osn::Video::GetCanvasId(const Napi::CallbackInfo &info)
{
return Napi::Number::New(info.Env(), (double)this->canvasId);
}

inline void CreateVideo(const Napi::CallbackInfo &info, const std::vector<ipc::value> &response, Napi::Object &video, uint32_t index)
{
video.Set("fpsNum", response.at(index++).value_union.ui32);
Expand Down Expand Up @@ -146,19 +151,16 @@ Napi::Value osn::Video::get(const Napi::CallbackInfo &info)
if (!conn)
return info.Env().Undefined();

if (!isLastVideoValid) {
lastVideo = conn->call_synchronous_helper("Video", "GetVideoContext", {ipc::value((uint64_t)this->canvasId)});
auto response = conn->call_synchronous_helper("Video", "GetVideoContext", {ipc::value((uint64_t)this->canvasId)});

if (!ValidateResponse(info, lastVideo))
return info.Env().Undefined();
if (!ValidateResponse(info, response))
return info.Env().Undefined();

if (!(lastVideo.size() == 11 || lastVideo.size() == 12))
return info.Env().Undefined();
isLastVideoValid = true;
}
if (!(response.size() == 11 || response.size() == 12))
return info.Env().Undefined();

Napi::Object video = Napi::Object::New(info.Env());
CreateVideo(info, lastVideo, video, 1);
CreateVideo(info, response, video, 1);

return video;
}
Expand All @@ -182,9 +184,6 @@ void osn::Video::set(const Napi::CallbackInfo &info, const Napi::Value &value)

auto response = conn->call_synchronous_helper("Video", "SetVideoContext", args);

lastVideo.resize(0);
isLastVideoValid = false;

if (!ValidateResponse(info, response))
return;
}
Expand Down
10 changes: 5 additions & 5 deletions obs-studio-client/source/video.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ class Video : public Napi::ObjectWrap<osn::Video> {
uint64_t canvasId = 0;
constexpr static uint64_t nonCavasId = std::numeric_limits<uint64_t>::max();

private:
std::vector<ipc::value> lastVideo;
bool isLastVideoValid = false;

public:
static Napi::FunctionReference constructor;
static Napi::Object Init(Napi::Env env, Napi::Object exports);
Video(const Napi::CallbackInfo &info);
Expand All @@ -46,5 +41,10 @@ class Video : public Napi::ObjectWrap<osn::Video> {

Napi::Value GetLegacySettings(const Napi::CallbackInfo &info);
void SetLegacySettings(const Napi::CallbackInfo &info, const Napi::Value &value);

// Read-only accessor exposing the server-side canvas id (the same value the
// server's osn::Video::Manager keys this object by). Required so the frontend
// can refer to a canvas in APIs like autoconfig that take ids.
Napi::Value GetCanvasId(const Napi::CallbackInfo &info);
};
}
2 changes: 2 additions & 0 deletions obs-studio-server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ SET(osn-server_SOURCES
"${PROJECT_SOURCE_DIR}/source/nodeobs_audio_encoders.h"
"${PROJECT_SOURCE_DIR}/source/nodeobs_autoconfig.cpp"
"${PROJECT_SOURCE_DIR}/source/nodeobs_autoconfig.h"
"${PROJECT_SOURCE_DIR}/source/nodeobs_autoconfig_resource_sampler.cpp"
"${PROJECT_SOURCE_DIR}/source/nodeobs_autoconfig_resource_sampler.h"
"${PROJECT_SOURCE_DIR}/source/nodeobs_configManager.cpp"
"${PROJECT_SOURCE_DIR}/source/nodeobs_configManager.hpp"
"${PROJECT_SOURCE_DIR}/source/nodeobs_display.cpp"
Expand Down
1 change: 1 addition & 0 deletions obs-studio-server/source/nodeobs_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "util/lexer.h"
#include "util-crashmanager.h"
#include "util-metricsprovider.h"
#include "nodeobs_service.h"

#include "osn-streaming.hpp"
#include "osn-recording.hpp"
Expand Down
2 changes: 1 addition & 1 deletion obs-studio-server/source/nodeobs_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
#include <vector>
#include <deque>
#include "nodeobs_configManager.hpp"
#include "nodeobs_service.h"
//#include "nodeobs_service.h"
#include "util-osx.hpp"

extern std::string g_moduleDirectory;
Expand Down
Loading
Loading