Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
24a1855
Implement object clean up for encoders and outputs related objects
summeroff May 28, 2025
c335a11
fix test
summeroff Sep 22, 2025
773c4c6
Code moved from osn-output-signals to osn-output
avoitenko-logitech Dec 19, 2025
ee273c6
Added PopReceivedSignal method to the Output class and refactored sig…
avoitenko-logitech Dec 19, 2025
5596bbc
Made canvas private in the Output class
avoitenko-logitech Dec 19, 2025
221dac6
Refactored the Output class. Made all data member private
avoitenko-logitech Dec 19, 2025
4d9b385
Enhanced broadcasting with the new API proof of concept
avoitenko-logitech Jan 12, 2026
fa74915
Implemented enhanced broadcasting for the simple and advanced modes f…
avoitenko-logitech Jan 14, 2026
9bf5f0f
Enhanced broadcasting tests
avoitenko-logitech Jan 14, 2026
ce077e2
Refined constructGoLivePost for enhanced broadcasting to accept desir…
avoitenko-logitech Jan 15, 2026
da21854
Implemented dual streaming with the new Enhnaced Broadcasting API
avoitenko-logitech Jan 27, 2026
dabe446
Minor scene cleanup in tests
avoitenko-logitech Jan 27, 2026
9f211b5
Clang format
avoitenko-logitech Jan 27, 2026
2a373b3
Skipping GPU-related tests on CI
avoitenko-logitech Jan 28, 2026
0aa9291
Added exceptions on stream start
avoitenko-logitech Jan 28, 2026
ddbd067
Merge branch 'staging' into enhanced-broadcasting-new-api
avoitenko-logitech Feb 18, 2026
2a8c637
Merged branch object_cleanup_prerebase into factory-api-v2-and-enhanc…
avoitenko-logitech Feb 18, 2026
3ca6806
Tests fix
avoitenko-logitech Feb 18, 2026
3383133
Introduced Connection Epoch concept to not finalize stale objects
avoitenko-logitech Feb 18, 2026
e61ac27
Minor comment update
avoitenko-logitech Feb 19, 2026
49f9ae2
Set video mix when duplicating encoder
mhoyer-streamlabs Feb 24, 2026
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
39 changes: 31 additions & 8 deletions js/module.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ export declare const VideoEncoderFactory: IVideoEncoderFactory;
export declare const ServiceFactory: IServiceFactory;
export declare const SimpleStreamingFactory: ISimpleStreamingFactory;
export declare const AdvancedStreamingFactory: IAdvancedStreamingFactory;
export declare const EnhancedBroadcastingAdvancedStreamingFactory: IEnhancedBroadcastingAdvancedStreamingFactory;
export declare const EnhancedBroadcastingSimpleStreamingFactory: IEnhancedBroadcastingSimpleStreamingFactory;
export declare const DelayFactory: IDelayFactory;
export declare const ReconnectFactory: IReconnectFactory;
export declare const NetworkFactory: INetworkFactory;
Expand Down Expand Up @@ -754,8 +756,8 @@ export declare const enum ERecordingFormat {
FLV = "flv",
MOV = "mov",
MKV = "mkv",
TS = "mpegts",
M3M8 = "m3m8"
MPEGTS = "ts",
HLS = "m3u8"
}
export declare const enum ERecordingQuality {
Stream = 0,
Expand All @@ -774,27 +776,28 @@ export declare const enum EProcessPriority {
BelowNormal = "BelowNormal",
Idle = "Idle"
}
export interface IVideoEncoder extends IConfigurable {
export interface IVideoEncoder extends IConfigurable, IReleasable {
name: string;
readonly type: EVideoEncoderType;
readonly active: boolean;
readonly id: string;
readonly lastError: string;
}
export interface IAudioEncoder {
export interface IAudioEncoder extends IReleasable {
name: string;
bitrate: number;
}
export interface IAudioEncoderFactory {
create(): IAudioEncoder;
create(id: string, name: string): IAudioEncoder;
}
export interface IVideoEncoderFactory {
types(): string[];
types(filter: EVideoEncoderType): string[];
create(id: string, name: string, settings?: ISettings): IVideoEncoder;
}
export interface IStreaming {
videoEncoder: IVideoEncoder;
// Video encoder value is only ignored in the Enhanced Broadcasting mode, otherwise it should be set
videoEncoder?: IVideoEncoder;
service: IService;
enforceServiceBitrate: boolean;
enableTwitchVOD: boolean;
Expand All @@ -803,7 +806,7 @@ export interface IStreaming {
network: INetwork;
video: IVideo;
signalHandler: (signal: EOutputSignal) => void;
start(): void;
start(): void; // throws
stop(force?: boolean): void;
droppedFrames: number;
totalFrames: number;
Expand Down Expand Up @@ -838,6 +841,26 @@ export interface IAdvancedStreamingFactory {
destroy(stream: IAdvancedStreaming): void;
legacySettings: IAdvancedStreaming;
}
export interface IEnhancedBroadcastingAdvancedStreaming extends IAdvancedStreaming {
// If set, the Enhanced Broadcasting stream will be in the Dual Output mode.
// This value should be initialized before the stream start.
additionalVideo?: IVideo,
}
export interface IEnhancedBroadcastingAdvancedStreamingFactory {
create(): IEnhancedBroadcastingAdvancedStreaming;
destroy(stream: IEnhancedBroadcastingAdvancedStreaming): void;
legacySettings: IEnhancedBroadcastingAdvancedStreaming;
}
export interface IEnhancedBroadcastingSimpleStreaming extends ISimpleStreaming {
// If set, the Enhanced Broadcasting stream will be in the Dual Output mode.
// This value should be initialized before the stream start.
additionalVideo?: IVideo,
}
export interface IEnhancedBroadcastingSimpleStreamingFactory {
create(): IEnhancedBroadcastingSimpleStreaming;
destroy(stream: IEnhancedBroadcastingSimpleStreaming): void;
legacySettings: IEnhancedBroadcastingSimpleStreaming;
}
export interface IFileOutput {
path: string;
format: ERecordingFormat;
Expand Down Expand Up @@ -911,7 +934,7 @@ export interface ISimpleReplayBufferFactory {
export interface IAdvancedReplayBufferFactory {
create(): IAdvancedReplayBuffer;
destroy(stream: IAdvancedReplayBuffer): void;
legacySettings: IAdvancedReplayBufferFactory;
legacySettings: IAdvancedReplayBuffer;
}
export interface IDelay {
enabled: boolean;
Expand Down
10 changes: 6 additions & 4 deletions js/module.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeObs = exports.getSourcesSize = exports.createSources = exports.addItems = exports.AdvancedReplayBufferFactory = exports.SimpleReplayBufferFactory = exports.AudioEncoderFactory = exports.AdvancedRecordingFactory = exports.SimpleRecordingFactory = exports.AudioTrackFactory = exports.NetworkFactory = exports.ReconnectFactory = exports.DelayFactory = exports.AdvancedStreamingFactory = exports.SimpleStreamingFactory = exports.ServiceFactory = exports.VideoEncoderFactory = exports.IPC = exports.ModuleFactory = exports.AudioFactory = exports.Audio = exports.FaderFactory = exports.VolmeterFactory = exports.DisplayFactory = exports.TransitionFactory = exports.FilterFactory = exports.SceneFactory = exports.InputFactory = exports.VideoFactory = exports.Video = exports.Global = exports.DefaultPluginPathMac = exports.DefaultPluginDataPath = exports.DefaultPluginPath = exports.DefaultDataPath = exports.DefaultBinPath = exports.DefaultDrawPluginPath = exports.DefaultOpenGLPath = exports.DefaultD3D11Path = void 0;
exports.NodeObs = exports.getSourcesSize = exports.createSources = exports.addItems = exports.AdvancedReplayBufferFactory = exports.SimpleReplayBufferFactory = exports.AudioEncoderFactory = exports.AdvancedRecordingFactory = exports.SimpleRecordingFactory = exports.AudioTrackFactory = exports.NetworkFactory = exports.ReconnectFactory = exports.DelayFactory = exports.AdvancedStreamingFactory = exports.EnhancedBroadcastingSimpleStreamingFactory = exports.EnhancedBroadcastingAdvancedStreamingFactory = exports.SimpleStreamingFactory = exports.ServiceFactory = exports.VideoEncoderFactory = exports.IPC = exports.ModuleFactory = exports.AudioFactory = exports.Audio = exports.FaderFactory = exports.VolmeterFactory = exports.DisplayFactory = exports.TransitionFactory = exports.FilterFactory = exports.SceneFactory = exports.InputFactory = exports.VideoFactory = exports.Video = exports.Global = exports.DefaultPluginPathMac = exports.DefaultPluginDataPath = exports.DefaultPluginPath = exports.DefaultDataPath = exports.DefaultBinPath = exports.DefaultDrawPluginPath = exports.DefaultOpenGLPath = exports.DefaultD3D11Path = void 0;
const path = require("path");
const fs = require("fs");
// Mac- search for optional OSN.app bundle (Chromium requires an app bundle to find obs64 helper apps)
Expand Down Expand Up @@ -34,6 +34,8 @@ exports.VideoEncoderFactory = obs.VideoEncoder;
exports.ServiceFactory = obs.Service;
exports.SimpleStreamingFactory = obs.SimpleStreaming;
exports.AdvancedStreamingFactory = obs.AdvancedStreaming;
exports.EnhancedBroadcastingAdvancedStreamingFactory = obs.EnhancedBroadcastingAdvancedStreaming;
exports.EnhancedBroadcastingSimpleStreamingFactory = obs.EnhancedBroadcastingSimpleStreaming;
exports.DelayFactory = obs.Delay;
exports.ReconnectFactory = obs.Reconnect;
exports.NetworkFactory = obs.Network;
Expand Down Expand Up @@ -70,7 +72,7 @@ function createSources(sources) {
console.error(`[OSN] Failed to create input for source "${source.name}":`, error instanceof Error ? error.message : error);
return; // Skip the rest of this iteration if input creation fails
}

if (newSource) {
if (newSource.audioMixers) {
newSource.muted = source.muted ?? false;
Expand All @@ -80,7 +82,7 @@ function createSources(sources) {
newSource.deinterlaceMode = source.deinterlaceMode;
newSource.deinterlaceFieldOrder = source.deinterlaceFieldOrder;
items.push(newSource);

const filters = source.filters;
if (Array.isArray(filters)) {
filters.forEach(function (filter) {
Expand All @@ -90,7 +92,7 @@ function createSources(sources) {
} catch (filterError) {
console.error(`[OSN] Failed to create filter "${filter.name}" for source "${source.name}":`, filterError instanceof Error ? filterError.message : filterError);
}

if (ObsFilter) {
ObsFilter.enabled = filter.enabled ?? true;
newSource.addFilter(ObsFilter);
Expand Down
49 changes: 38 additions & 11 deletions js/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ export const VideoEncoderFactory: IVideoEncoderFactory = obs.VideoEncoder;
export const ServiceFactory: IServiceFactory = obs.Service;
export const SimpleStreamingFactory: ISimpleStreamingFactory = obs.SimpleStreaming;
export const AdvancedStreamingFactory: IAdvancedStreamingFactory = obs.AdvancedStreaming;
export const EnhancedBroadcastingAdvancedStreamingFactory: IEnhancedBroadcastingAdvancedStreamingFactory = obs.EnhancedBroadcastingAdvancedStreaming;
export const EnhancedBroadcastingSimpleStreamingFactory: IEnhancedBroadcastingSimpleStreamingFactory = obs.EnhancedBroadcastingSimpleStreaming;
export const DelayFactory: IDelayFactory = obs.Delay;
export const ReconnectFactory: IReconnectFactory = obs.Reconnect;
export const NetworkFactory: INetworkFactory = obs.Network;
Expand Down Expand Up @@ -581,12 +583,12 @@ export interface IGlobal {
readonly version: number;

/**
* Percentage of CPU being used
* Percentage of CPU being used
*/
readonly cpuPercentage: number;

/**
* Current FPS
* Current FPS
*/
readonly currentFrameRate: number;

Expand All @@ -601,7 +603,7 @@ export interface IGlobal {
readonly diskSpaceAvailable: number;

/**
* Current memory usage
* Current memory usage
*/
readonly memoryUsage: number;
}
Expand Down Expand Up @@ -1657,8 +1659,8 @@ export const enum ERecordingFormat {
FLV = 'flv',
MOV = 'mov',
MKV = 'mkv',
TS = 'mpegts',
M3M8 = 'm3m8'
MPEGTS = 'ts',
HLS = 'm3u8'
}

export const enum ERecordingQuality {
Expand All @@ -1681,21 +1683,21 @@ export const enum EProcessPriority {
Idle = 'Idle'
}

export interface IVideoEncoder extends IConfigurable {
export interface IVideoEncoder extends IConfigurable, IReleasable {
name: string,
readonly type: EVideoEncoderType,
readonly active: boolean,
readonly id: string,
readonly lastError: string
}

export interface IAudioEncoder {
export interface IAudioEncoder extends IReleasable {
name: string,
bitrate: number
}

export interface IAudioEncoderFactory {
create(): IAudioEncoder
create(id: string, name: string): IAudioEncoder
}

export interface IVideoEncoderFactory {
Expand All @@ -1705,7 +1707,8 @@ export interface IVideoEncoderFactory {
}

export interface IStreaming {
videoEncoder: IVideoEncoder,
// Video encoder value is ignored only in the Enhanced Broadcasting mode, otherwise it should be set
videoEncoder?: IVideoEncoder,
service: IService,
enforceServiceBitrate: boolean,
enableTwitchVOD: boolean,
Expand All @@ -1714,7 +1717,7 @@ export interface IStreaming {
network: INetwork,
video: IVideo,
signalHandler: (signal: EOutputSignal) => void,
start(): void,
start(): void, // throws
stop(force?: boolean): void,
droppedFrames: number;
totalFrames: number;
Expand Down Expand Up @@ -1755,6 +1758,30 @@ export interface IAdvancedStreamingFactory {
legacySettings: IAdvancedStreaming;
}

export interface IEnhancedBroadcastingAdvancedStreaming extends IAdvancedStreaming {
// If set, the Enhanced Broadcasting stream will be in the Dual Output mode.
// This value should be initialized before the stream start.
additionalVideo?: IVideo,
}

export interface IEnhancedBroadcastingAdvancedStreamingFactory {
create(): IEnhancedBroadcastingAdvancedStreaming;
destroy(stream: IEnhancedBroadcastingAdvancedStreaming): void;
legacySettings: IEnhancedBroadcastingAdvancedStreaming;
}

export interface IEnhancedBroadcastingSimpleStreaming extends ISimpleStreaming {
// If set, the Enhanced Broadcasting stream will be in the Dual Output mode.
// This value should be initialized before the stream start.
additionalVideo?: IVideo,
}

export interface IEnhancedBroadcastingSimpleStreamingFactory {
create(): IEnhancedBroadcastingSimpleStreaming;
destroy(stream: IEnhancedBroadcastingSimpleStreaming): void;
legacySettings: IEnhancedBroadcastingSimpleStreaming;
}

export interface IFileOutput {
path: string,
format: ERecordingFormat,
Expand Down Expand Up @@ -1838,7 +1865,7 @@ export interface ISimpleReplayBufferFactory {
export interface IAdvancedReplayBufferFactory {
create(): IAdvancedReplayBuffer;
destroy(stream: IAdvancedReplayBuffer): void;
legacySettings: IAdvancedReplayBufferFactory;
legacySettings: IAdvancedReplayBuffer;
}

export interface IDelay {
Expand Down
12 changes: 10 additions & 2 deletions obs-studio-client/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,18 @@ SET(osn-client_SOURCES
"source/audio.cpp"
"source/streaming.hpp"
"source/streaming.cpp"
"source/simple-streaming-base.hpp"
"source/simple-streaming-base.cpp"
"source/simple-streaming.hpp"
"source/simple-streaming.cpp"
"source/advanced-streaming-base.hpp"
"source/advanced-streaming-base.cpp"
"source/advanced-streaming.hpp"
"source/advanced-streaming.cpp"
"source/enhanced-broadcasting-advanced-streaming.hpp"
"source/enhanced-broadcasting-advanced-streaming.cpp"
"source/enhanced-broadcasting-simple-streaming.hpp"
"source/enhanced-broadcasting-simple-streaming.cpp"
"source/worker-signals.hpp"
"source/delay.hpp"
"source/delay.cpp"
Expand Down Expand Up @@ -187,7 +195,7 @@ target_link_libraries(${PROJECT_NAME} ${LIBOBS_LIBRARIES})
add_definitions(-DNAPI_VERSION=7)

#Define the OSN_VERSION
add_compile_definitions(OSN_VERSION=\"$ENV{tagartifact}\")
add_compile_definitions(OSN_VERSION=\"$ENV{tagartifact}\")
set_target_properties(
obs_studio_client
PROPERTIES
Expand Down Expand Up @@ -235,5 +243,5 @@ install(
TARGETS obs_studio_client
RUNTIME DESTINATION "./" COMPONENT Runtime
LIBRARY DESTINATION "./" COMPONENT Runtime
ARCHIVE DESTINATION "./" COMPONENT Runtime
ARCHIVE DESTINATION "./" COMPONENT Runtime
)
49 changes: 35 additions & 14 deletions obs-studio-client/source/advanced-recording.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ osn::AdvancedRecording::AdvancedRecording(const Napi::CallbackInfo &info) : Napi
this->className = std::string("AdvancedRecording");
}

void osn::AdvancedRecording::Finalize(Napi::Env)
{
ReleaseObjects();
}

void osn::AdvancedRecording::ReleaseObjects()
{
if (!videoEncoderRef.IsEmpty())
videoEncoderRef.Reset();
if (!streamingRef.IsEmpty())
streamingRef.Reset();
}

Napi::Value osn::AdvancedRecording::Create(const Napi::CallbackInfo &info)
{
auto conn = GetConnection(info);
Expand All @@ -107,6 +120,8 @@ void osn::AdvancedRecording::Destroy(const Napi::CallbackInfo &info)
recording->stopWorker();
recording->cb.Reset();

recording->ReleaseObjects();

auto conn = GetConnection(info);
if (!conn)
return;
Expand Down Expand Up @@ -268,32 +283,38 @@ void osn::AdvancedRecording::SetLegacySettings(const Napi::CallbackInfo &info, c
}

Napi::Value osn::AdvancedRecording::GetStreaming(const Napi::CallbackInfo &info)
{
return streamingRef.IsEmpty() ? info.Env().Undefined() : streamingRef.Value();
}

void osn::AdvancedRecording::SetStreaming(const Napi::CallbackInfo &info, const Napi::Value &value)
{
auto conn = GetConnection(info);
if (!conn)
return info.Env().Undefined();

std::vector<ipc::value> response = conn->call_synchronous_helper(className, "GetStreaming", {ipc::value(this->uid)});
return;

if (!ValidateResponse(info, response))
return info.Env().Undefined();
if (value.IsNull() || value.IsUndefined()) {
if (!streamingRef.IsEmpty())
streamingRef.Reset();
conn->call(className, "SetStreaming", {ipc::value(this->uid), ipc::value(UINT64_MAX)});
return;
}

auto instance = osn::AdvancedStreaming::constructor.New({Napi::Number::New(info.Env(), static_cast<double>(response[1].value_union.ui64))});
return instance;
}
Napi::Object obj = value.As<Napi::Object>();
if (!obj.InstanceOf(osn::AdvancedStreaming::constructor.Value()))
Napi::TypeError::New(info.Env(), "Object is not a AdvancedStreaming").ThrowAsJavaScriptException();

void osn::AdvancedRecording::SetStreaming(const Napi::CallbackInfo &info, const Napi::Value &value)
{
osn::AdvancedStreaming *streaming = Napi::ObjectWrap<osn::AdvancedStreaming>::Unwrap(value.ToObject());

if (!streaming) {
Napi::TypeError::New(info.Env(), "Invalid streaming argument").ThrowAsJavaScriptException();
return;
}

auto conn = GetConnection(info);
if (!conn)
return;

conn->call(className, "SetStreaming", {ipc::value(this->uid), ipc::value(streaming->uid)});

if (!streamingRef.IsEmpty())
streamingRef.Reset();

streamingRef = Napi::Persistent(obj);
}
3 changes: 2 additions & 1 deletion obs-studio-client/source/advanced-recording.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class AdvancedRecording : public Napi::ObjectWrap<osn::AdvancedRecording>, publi
static Napi::FunctionReference constructor;
static Napi::Object Init(Napi::Env env, Napi::Object exports);
AdvancedRecording(const Napi::CallbackInfo &info);

void Finalize(Napi::Env);
void ReleaseObjects();
static Napi::Value Create(const Napi::CallbackInfo &info);
static void Destroy(const Napi::CallbackInfo &info);

Expand Down
Loading
Loading