diff --git a/js/module.d.ts b/js/module.d.ts index 2bb3aee9f..68b263649 100644 --- a/js/module.d.ts +++ b/js/module.d.ts @@ -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; @@ -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, @@ -774,19 +776,19 @@ 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[]; @@ -794,7 +796,8 @@ export interface IVideoEncoderFactory { 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; @@ -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; @@ -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; @@ -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; diff --git a/js/module.js b/js/module.js index 48ea85376..61471a348 100644 --- a/js/module.js +++ b/js/module.js @@ -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) @@ -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; @@ -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; @@ -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) { @@ -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); diff --git a/js/module.ts b/js/module.ts index 6ef9a70ea..b3ba8fb0c 100644 --- a/js/module.ts +++ b/js/module.ts @@ -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; @@ -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; @@ -601,7 +603,7 @@ export interface IGlobal { readonly diskSpaceAvailable: number; /** - * Current memory usage + * Current memory usage */ readonly memoryUsage: number; } @@ -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 { @@ -1681,7 +1683,7 @@ 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, @@ -1689,13 +1691,13 @@ export interface IVideoEncoder extends IConfigurable { 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 { @@ -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, @@ -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; @@ -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, @@ -1838,7 +1865,7 @@ export interface ISimpleReplayBufferFactory { export interface IAdvancedReplayBufferFactory { create(): IAdvancedReplayBuffer; destroy(stream: IAdvancedReplayBuffer): void; - legacySettings: IAdvancedReplayBufferFactory; + legacySettings: IAdvancedReplayBuffer; } export interface IDelay { diff --git a/obs-studio-client/CMakeLists.txt b/obs-studio-client/CMakeLists.txt index b8abce6c9..ec3530915 100644 --- a/obs-studio-client/CMakeLists.txt +++ b/obs-studio-client/CMakeLists.txt @@ -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" @@ -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 @@ -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 ) diff --git a/obs-studio-client/source/advanced-recording.cpp b/obs-studio-client/source/advanced-recording.cpp index d98f75a1a..92bf450a1 100644 --- a/obs-studio-client/source/advanced-recording.cpp +++ b/obs-studio-client/source/advanced-recording.cpp @@ -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); @@ -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; @@ -268,22 +283,27 @@ 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 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(response[1].value_union.ui64))}); - return instance; -} + Napi::Object obj = value.As(); + 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::Unwrap(value.ToObject()); if (!streaming) { @@ -291,9 +311,10 @@ void osn::AdvancedRecording::SetStreaming(const Napi::CallbackInfo &info, const 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); } \ No newline at end of file diff --git a/obs-studio-client/source/advanced-recording.hpp b/obs-studio-client/source/advanced-recording.hpp index 8bdf25edb..af3215585 100644 --- a/obs-studio-client/source/advanced-recording.hpp +++ b/obs-studio-client/source/advanced-recording.hpp @@ -26,7 +26,8 @@ class AdvancedRecording : public Napi::ObjectWrap, 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); diff --git a/obs-studio-client/source/advanced-replay-buffer.cpp b/obs-studio-client/source/advanced-replay-buffer.cpp index cb26f1445..6ee7eef7b 100644 --- a/obs-studio-client/source/advanced-replay-buffer.cpp +++ b/obs-studio-client/source/advanced-replay-buffer.cpp @@ -78,6 +78,17 @@ osn::AdvancedReplayBuffer::AdvancedReplayBuffer(const Napi::CallbackInfo &info) this->className = std::string("AdvancedReplayBuffer"); } +void osn::AdvancedReplayBuffer::Finalize(Napi::Env) +{ + ReleaseObjects(); +} + +void osn::AdvancedReplayBuffer::ReleaseObjects() +{ + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); +} + Napi::Value osn::AdvancedReplayBuffer::Create(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); @@ -103,6 +114,7 @@ void osn::AdvancedReplayBuffer::Destroy(const Napi::CallbackInfo &info) replayBuffer->stopWorker(); replayBuffer->cb.Reset(); + replayBuffer->ReleaseObjects(); auto conn = GetConnection(info); if (!conn) @@ -174,62 +186,82 @@ void osn::AdvancedReplayBuffer::SetLegacySettings(const Napi::CallbackInfo &info Napi::Value osn::AdvancedReplayBuffer::GetStreaming(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetStreaming", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) + if (usesStream) { + return parentOutputRef.IsEmpty() ? info.Env().Undefined() : parentOutputRef.Value(); + } else { return info.Env().Undefined(); - - auto instance = osn::AdvancedStreaming::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - return instance; + } } void osn::AdvancedReplayBuffer::SetStreaming(const Napi::CallbackInfo &info, const Napi::Value &value) { - osn::AdvancedStreaming *encoder = Napi::ObjectWrap::Unwrap(value.ToObject()); + auto conn = GetConnection(info); + if (!conn) + return; - if (!encoder) { - Napi::TypeError::New(info.Env(), "Invalid streaming argument").ThrowAsJavaScriptException(); + if (value.IsNull() || value.IsUndefined()) { + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); + conn->call(className, "SetStreaming", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); + usesStream = false; return; } - auto conn = GetConnection(info); - if (!conn) + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::AdvancedStreaming::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a valid Streaming").ThrowAsJavaScriptException(); + + osn::AdvancedStreaming *streaming = Napi::ObjectWrap::Unwrap(value.ToObject()); + + if (!streaming) { + Napi::TypeError::New(info.Env(), "Invalid streaming argument").ThrowAsJavaScriptException(); return; + } + + conn->call(className, "SetStreaming", {ipc::value(this->uid), ipc::value(streaming->uid)}); + usesStream = true; + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); - conn->call(className, "SetStreaming", {ipc::value(this->uid), ipc::value(encoder->uid)}); + parentOutputRef = Napi::Persistent(obj); } Napi::Value osn::AdvancedReplayBuffer::GetRecording(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) + if (usesStream) { return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetRecording", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - auto instance = osn::AdvancedRecording::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - return instance; + } else { + return parentOutputRef.IsEmpty() ? info.Env().Undefined() : parentOutputRef.Value(); + } } void osn::AdvancedReplayBuffer::SetRecording(const Napi::CallbackInfo &info, const Napi::Value &value) { - osn::AdvancedRecording *recording = Napi::ObjectWrap::Unwrap(value.ToObject()); + auto conn = GetConnection(info); + if (!conn) + return; - if (!recording) { - Napi::TypeError::New(info.Env(), "Invalid streaming argument").ThrowAsJavaScriptException(); + if (value.IsNull() || value.IsUndefined()) { + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); + conn->call(className, "SetRecording", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); + usesStream = false; return; } - auto conn = GetConnection(info); - if (!conn) + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::AdvancedRecording::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a Valid Recording").ThrowAsJavaScriptException(); + + osn::AdvancedRecording *recording = Napi::ObjectWrap::Unwrap(obj); + if (!recording) { + Napi::TypeError::New(info.Env(), "Invalid recording argument").ThrowAsJavaScriptException(); return; + } conn->call(className, "SetRecording", {ipc::value(this->uid), ipc::value(recording->uid)}); + usesStream = false; + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); + parentOutputRef = Napi::Persistent(obj); } \ No newline at end of file diff --git a/obs-studio-client/source/advanced-replay-buffer.hpp b/obs-studio-client/source/advanced-replay-buffer.hpp index 4395f8fa4..60cdeeb36 100644 --- a/obs-studio-client/source/advanced-replay-buffer.hpp +++ b/obs-studio-client/source/advanced-replay-buffer.hpp @@ -26,7 +26,8 @@ class AdvancedReplayBuffer : public Napi::ObjectWrap, static Napi::FunctionReference constructor; static Napi::Object Init(Napi::Env env, Napi::Object exports); AdvancedReplayBuffer(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); diff --git a/obs-studio-client/source/advanced-streaming-base.cpp b/obs-studio-client/source/advanced-streaming-base.cpp new file mode 100644 index 000000000..3c13403ef --- /dev/null +++ b/obs-studio-client/source/advanced-streaming-base.cpp @@ -0,0 +1,134 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#include "advanced-streaming-base.hpp" + +Napi::Value osn::AdvancedStreamingBase::GetAudioTrack(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetAudioTrack", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + return Napi::Number::New(info.Env(), response[1].value_union.ui32); +} + +void osn::AdvancedStreamingBase::SetAudioTrack(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call_synchronous_helper("AdvancedStreaming", "SetAudioTrack", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); +} + +Napi::Value osn::AdvancedStreamingBase::GetTwitchTrack(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetTwitchTrack", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + return Napi::Number::New(info.Env(), response[1].value_union.ui32); +} + +void osn::AdvancedStreamingBase::SetTwitchTrack(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call_synchronous_helper("AdvancedStreaming", "SetTwitchTrack", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); +} + +Napi::Value osn::AdvancedStreamingBase::GetRescaling(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetRescaling", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + return Napi::Boolean::New(info.Env(), response[1].value_union.ui32); +} + +void osn::AdvancedStreamingBase::SetRescaling(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call_synchronous_helper("AdvancedStreaming", "SetRescaling", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); +} + +Napi::Value osn::AdvancedStreamingBase::GetOutputWidth(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetOutputWidth", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + return Napi::Number::New(info.Env(), response[1].value_union.ui32); +} + +void osn::AdvancedStreamingBase::SetOutputWidth(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call_synchronous_helper("AdvancedStreaming", "SetOutputWidth", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); +} + +Napi::Value osn::AdvancedStreamingBase::GetOutputHeight(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetOutputHeight", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + return Napi::Number::New(info.Env(), response[1].value_union.ui32); +} + +void osn::AdvancedStreamingBase::SetOutputHeight(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call_synchronous_helper("AdvancedStreaming", "SetOutputHeight", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); +} diff --git a/obs-studio-client/source/advanced-streaming-base.hpp b/obs-studio-client/source/advanced-streaming-base.hpp new file mode 100644 index 000000000..1ad27dea1 --- /dev/null +++ b/obs-studio-client/source/advanced-streaming-base.hpp @@ -0,0 +1,39 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once +#include +#include "streaming.hpp" + +namespace osn { + +class AdvancedStreamingBase : public osn::Streaming { +public: + Napi::Value GetAudioTrack(const Napi::CallbackInfo &info); + void SetAudioTrack(const Napi::CallbackInfo &info, const Napi::Value &value); + Napi::Value GetTwitchTrack(const Napi::CallbackInfo &info); + void SetTwitchTrack(const Napi::CallbackInfo &info, const Napi::Value &value); + Napi::Value GetRescaling(const Napi::CallbackInfo &info); + void SetRescaling(const Napi::CallbackInfo &info, const Napi::Value &value); + Napi::Value GetOutputWidth(const Napi::CallbackInfo &info); + void SetOutputWidth(const Napi::CallbackInfo &info, const Napi::Value &value); + Napi::Value GetOutputHeight(const Napi::CallbackInfo &info); + void SetOutputHeight(const Napi::CallbackInfo &info, const Napi::Value &value); +}; + +} \ No newline at end of file diff --git a/obs-studio-client/source/advanced-streaming.cpp b/obs-studio-client/source/advanced-streaming.cpp index 267169b07..f38f27b65 100644 --- a/obs-studio-client/source/advanced-streaming.cpp +++ b/obs-studio-client/source/advanced-streaming.cpp @@ -80,6 +80,16 @@ osn::AdvancedStreaming::AdvancedStreaming(const Napi::CallbackInfo &info) : Napi this->className = std::string("AdvancedStreaming"); } +void osn::AdvancedStreaming::Finalize(Napi::Env) +{ + ReleaseObjects(); +} + +void osn::AdvancedStreaming::ReleaseObjects() +{ + osn::Streaming::ReleaseObjects(); +} + Napi::Value osn::AdvancedStreaming::Create(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); @@ -106,129 +116,16 @@ void osn::AdvancedStreaming::Destroy(const Napi::CallbackInfo &info) stream->stopWorker(); stream->cb.Reset(); - auto conn = GetConnection(info); - if (!conn) - return; - - std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "Destroy", {ipc::value(stream->uid)}); + stream->ReleaseObjects(); - if (!ValidateResponse(info, response)) - return; -} - -Napi::Value osn::AdvancedStreaming::GetAudioTrack(const Napi::CallbackInfo &info) -{ - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetAudioTrack", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - return Napi::Number::New(info.Env(), response[1].value_union.ui32); -} - -void osn::AdvancedStreaming::SetAudioTrack(const Napi::CallbackInfo &info, const Napi::Value &value) -{ - auto conn = GetConnection(info); - if (!conn) - return; - - conn->call_synchronous_helper("AdvancedStreaming", "SetAudioTrack", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); -} - -Napi::Value osn::AdvancedStreaming::GetTwitchTrack(const Napi::CallbackInfo &info) -{ - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetTwitchTrack", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - return Napi::Number::New(info.Env(), response[1].value_union.ui32); -} - -void osn::AdvancedStreaming::SetTwitchTrack(const Napi::CallbackInfo &info, const Napi::Value &value) -{ auto conn = GetConnection(info); if (!conn) return; - conn->call_synchronous_helper("AdvancedStreaming", "SetTwitchTrack", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); -} - -Napi::Value osn::AdvancedStreaming::GetRescaling(const Napi::CallbackInfo &info) -{ - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetRescaling", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - return Napi::Boolean::New(info.Env(), response[1].value_union.ui32); -} - -void osn::AdvancedStreaming::SetRescaling(const Napi::CallbackInfo &info, const Napi::Value &value) -{ - auto conn = GetConnection(info); - if (!conn) - return; - - conn->call_synchronous_helper("AdvancedStreaming", "SetRescaling", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); -} - -Napi::Value osn::AdvancedStreaming::GetOutputWidth(const Napi::CallbackInfo &info) -{ - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetOutputWidth", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - return Napi::Number::New(info.Env(), response[1].value_union.ui32); -} - -void osn::AdvancedStreaming::SetOutputWidth(const Napi::CallbackInfo &info, const Napi::Value &value) -{ - auto conn = GetConnection(info); - if (!conn) - return; - - conn->call_synchronous_helper("AdvancedStreaming", "SetOutputWidth", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); -} - -Napi::Value osn::AdvancedStreaming::GetOutputHeight(const Napi::CallbackInfo &info) -{ - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "GetOutputHeight", {ipc::value(this->uid)}); + std::vector response = conn->call_synchronous_helper("AdvancedStreaming", "Destroy", {ipc::value(stream->uid)}); if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - return Napi::Number::New(info.Env(), response[1].value_union.ui32); -} - -void osn::AdvancedStreaming::SetOutputHeight(const Napi::CallbackInfo &info, const Napi::Value &value) -{ - auto conn = GetConnection(info); - if (!conn) return; - - conn->call_synchronous_helper("AdvancedStreaming", "SetOutputHeight", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); } Napi::Value osn::AdvancedStreaming::GetLegacySettings(const Napi::CallbackInfo &info) diff --git a/obs-studio-client/source/advanced-streaming.hpp b/obs-studio-client/source/advanced-streaming.hpp index 261c5585c..a1cdbd5ae 100644 --- a/obs-studio-client/source/advanced-streaming.hpp +++ b/obs-studio-client/source/advanced-streaming.hpp @@ -18,30 +18,22 @@ #pragma once #include -#include "streaming.hpp" +#include "advanced-streaming-base.hpp" namespace osn { -class AdvancedStreaming : public Napi::ObjectWrap, public osn::Streaming { + +class AdvancedStreaming : public Napi::ObjectWrap, public osn::AdvancedStreamingBase { public: static Napi::FunctionReference constructor; static Napi::Object Init(Napi::Env env, Napi::Object exports); AdvancedStreaming(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); - Napi::Value GetAudioTrack(const Napi::CallbackInfo &info); - void SetAudioTrack(const Napi::CallbackInfo &info, const Napi::Value &value); - Napi::Value GetTwitchTrack(const Napi::CallbackInfo &info); - void SetTwitchTrack(const Napi::CallbackInfo &info, const Napi::Value &value); - Napi::Value GetRescaling(const Napi::CallbackInfo &info); - void SetRescaling(const Napi::CallbackInfo &info, const Napi::Value &value); - Napi::Value GetOutputWidth(const Napi::CallbackInfo &info); - void SetOutputWidth(const Napi::CallbackInfo &info, const Napi::Value &value); - Napi::Value GetOutputHeight(const Napi::CallbackInfo &info); - void SetOutputHeight(const Napi::CallbackInfo &info, const Napi::Value &value); - static Napi::Value GetLegacySettings(const Napi::CallbackInfo &info); static void SetLegacySettings(const Napi::CallbackInfo &info, const Napi::Value &value); }; + } \ No newline at end of file diff --git a/obs-studio-client/source/audio-encoder.cpp b/obs-studio-client/source/audio-encoder.cpp index 08cd273a6..d12ffbab2 100644 --- a/obs-studio-client/source/audio-encoder.cpp +++ b/obs-studio-client/source/audio-encoder.cpp @@ -32,6 +32,8 @@ Napi::Object osn::AudioEncoder::Init(Napi::Env env, Napi::Object exports) InstanceAccessor("name", &osn::AudioEncoder::GetName, &osn::AudioEncoder::SetName), InstanceAccessor("bitrate", &osn::AudioEncoder::GetBitrate, &osn::AudioEncoder::SetBitrate), + + InstanceMethod("release", &osn::AudioEncoder::Release), }); exports.Set("AudioEncoder", func); osn::AudioEncoder::constructor = Napi::Persistent(func); @@ -45,6 +47,8 @@ osn::AudioEncoder::AudioEncoder(const Napi::CallbackInfo &info) : Napi::ObjectWr Napi::HandleScope scope(env); size_t length = info.Length(); this->uid = 0; + this->encoderInitialized = false; + this->connectionEpoch = 0; if (length <= 0 || !info[0].IsNumber()) { Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); @@ -52,15 +56,25 @@ osn::AudioEncoder::AudioEncoder(const Napi::CallbackInfo &info) : Napi::ObjectWr } this->uid = (uint64_t)info[0].ToNumber().Int64Value(); + this->encoderInitialized = true; + this->connectionEpoch = Controller::GetInstance().GetConnectionEpoch(); } Napi::Value osn::AudioEncoder::Create(const Napi::CallbackInfo &info) { + std::string id = "ffmpeg_aac"; + std::string name = "audio-encoder"; + + if (info.Length() >= 2) { + id = info[0].ToString().Utf8Value(); + name = info[1].ToString().Utf8Value(); + } + auto conn = GetConnection(info); if (!conn) return info.Env().Undefined(); - std::vector response = conn->call_synchronous_helper("AudioEncoder", "Create", {}); + std::vector response = conn->call_synchronous_helper("AudioEncoder", "Create", {ipc::value(id), ipc::value(name)}); if (!ValidateResponse(info, response)) return info.Env().Undefined(); @@ -70,6 +84,46 @@ Napi::Value osn::AudioEncoder::Create(const Napi::CallbackInfo &info) return instance; } +void osn::AudioEncoder::Finalize(Napi::Env env) +{ + if (!this->encoderInitialized) + return; + + // If OBS was restarted/disconnected, skip IPC cleanup for this stale wrapper. + if (this->connectionEpoch != Controller::GetInstance().GetConnectionEpoch()) { + this->encoderInitialized = false; + this->uid = UINT64_MAX; + return; + } + + auto conn = Controller::GetInstance().GetConnection(); + if (!conn) { + this->encoderInitialized = false; + this->uid = UINT64_MAX; + return; + } + + conn->call_synchronous_helper("AudioEncoder", "Finalize", {ipc::value(this->uid)}); + this->encoderInitialized = false; + this->uid = UINT64_MAX; +} + +void osn::AudioEncoder::Release(const Napi::CallbackInfo &info) +{ + if (!this->encoderInitialized) + return; + + auto conn = GetConnection(info); + if (!conn) + return; + + this->encoderInitialized = false; + std::vector response = conn->call_synchronous_helper("AudioEncoder", "Release", {ipc::value(this->uid)}); + this->uid = UINT64_MAX; + if (!ValidateResponse(info, response)) + return; +} + Napi::Value osn::AudioEncoder::GetName(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); @@ -116,4 +170,4 @@ void osn::AudioEncoder::SetBitrate(const Napi::CallbackInfo &info, const Napi::V return; conn->call("AudioEncoder", "SetBitrate", {ipc::value(this->uid), ipc::value(value.ToNumber().Uint32Value())}); -} \ No newline at end of file +} diff --git a/obs-studio-client/source/audio-encoder.hpp b/obs-studio-client/source/audio-encoder.hpp index f03c2d1b3..609d58cc5 100644 --- a/obs-studio-client/source/audio-encoder.hpp +++ b/obs-studio-client/source/audio-encoder.hpp @@ -24,6 +24,8 @@ namespace osn { class AudioEncoder : public Napi::ObjectWrap { public: uint64_t uid; + bool encoderInitialized; + uint64_t connectionEpoch; public: static Napi::FunctionReference constructor; @@ -31,10 +33,12 @@ class AudioEncoder : public Napi::ObjectWrap { AudioEncoder(const Napi::CallbackInfo &info); static Napi::Value Create(const Napi::CallbackInfo &info); + void Finalize(Napi::Env env); + void Release(const Napi::CallbackInfo &info); Napi::Value GetName(const Napi::CallbackInfo &info); void SetName(const Napi::CallbackInfo &info, const Napi::Value &value); Napi::Value GetBitrate(const Napi::CallbackInfo &info); void SetBitrate(const Napi::CallbackInfo &info, const Napi::Value &value); }; -} \ No newline at end of file +} diff --git a/obs-studio-client/source/controller.cpp b/obs-studio-client/source/controller.cpp index ee7dd2ccc..3d78408c1 100644 --- a/obs-studio-client/source/controller.cpp +++ b/obs-studio-client/source/controller.cpp @@ -372,6 +372,10 @@ std::shared_ptr Controller::connect(const std::string &uri) } m_connection = cl; + // Note: this increment in the current implementation is not strictly required for the basic + // stale-after-reconnect case, but it is a useful safe guard for a possible future expansion. + // Bump epoch on successful connect to invalidate stale native wrappers, if any. + m_connectionEpoch.fetch_add(1, std::memory_order_relaxed); return m_connection; } @@ -382,6 +386,8 @@ void Controller::disconnect() m_isServer = false; } m_connection = nullptr; + // Bump epoch on disconnect so finalizers skip IPC calls for old sessions. + m_connectionEpoch.fetch_add(1, std::memory_order_relaxed); } DWORD Controller::GetExitCode() @@ -394,6 +400,11 @@ std::shared_ptr Controller::GetConnection() return m_connection; } +uint64_t Controller::GetConnectionEpoch() const +{ + return m_connectionEpoch.load(std::memory_order_relaxed); +} + Napi::Value js_setServerPath(const Napi::CallbackInfo &info) { if (info.Length() == 0) { diff --git a/obs-studio-client/source/controller.hpp b/obs-studio-client/source/controller.hpp index 577b65196..8827e2a2d 100644 --- a/obs-studio-client/source/controller.hpp +++ b/obs-studio-client/source/controller.hpp @@ -17,6 +17,7 @@ ******************************************************************************/ #pragma once +#include #include #include #include @@ -53,8 +54,14 @@ class Controller { std::shared_ptr GetConnection(); + // Used to identify stale connections and properly implement Node.js finalizers + uint64_t GetConnectionEpoch() const; + private: bool m_isServer = false; std::shared_ptr m_connection; ipc::ProcessInfo procId; + // Monotonic "epoch" for the IPC connection lifecycle. Helps to detect stale wrappers + // after disconnect/reconnect so finalizers can safely no-op. + std::atomic m_connectionEpoch{0}; }; diff --git a/obs-studio-client/source/delay.cpp b/obs-studio-client/source/delay.cpp index 87f27990a..8beb28caa 100644 --- a/obs-studio-client/source/delay.cpp +++ b/obs-studio-client/source/delay.cpp @@ -46,6 +46,7 @@ osn::Delay::Delay(const Napi::CallbackInfo &info) : Napi::ObjectWrap Napi::HandleScope scope(env); size_t length = info.Length(); this->uid = 0; + this->connectionEpoch = 0; if (length <= 0 || !info[0].IsNumber()) { Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); @@ -53,6 +54,7 @@ osn::Delay::Delay(const Napi::CallbackInfo &info) : Napi::ObjectWrap } this->uid = (uint64_t)info[0].ToNumber().Int64Value(); + this->connectionEpoch = Controller::GetInstance().GetConnectionEpoch(); } Napi::Value osn::Delay::Create(const Napi::CallbackInfo &info) @@ -71,6 +73,28 @@ Napi::Value osn::Delay::Create(const Napi::CallbackInfo &info) return instance; } +void osn::Delay::Finalize(Napi::Env env) +{ + if (this->uid == UINT64_MAX) + return; + + // If OBS was restarted/disconnected, skip IPC cleanup for this stale wrapper. + if (this->connectionEpoch != Controller::GetInstance().GetConnectionEpoch()) { + this->uid = UINT64_MAX; + return; + } + + auto conn = Controller::GetInstance().GetConnection(); + if (!conn) { + this->uid = UINT64_MAX; + return; + } + + conn->call_synchronous_helper("Delay", "Destroy", {ipc::value(this->uid)}); + + this->uid = UINT64_MAX; +} + Napi::Value osn::Delay::GetEnabled(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); @@ -138,4 +162,4 @@ void osn::Delay::SetPreserveDelay(const Napi::CallbackInfo &info, const Napi::Va return; conn->call_synchronous_helper("Delay", "SetPreserveDelay", {ipc::value(this->uid), ipc::value(value.ToBoolean().Value())}); -} \ No newline at end of file +} diff --git a/obs-studio-client/source/delay.hpp b/obs-studio-client/source/delay.hpp index 605a5754f..472e637a2 100644 --- a/obs-studio-client/source/delay.hpp +++ b/obs-studio-client/source/delay.hpp @@ -23,6 +23,7 @@ namespace osn { class Delay : public Napi::ObjectWrap { public: uint64_t uid; + uint64_t connectionEpoch; public: static Napi::FunctionReference constructor; @@ -30,7 +31,7 @@ class Delay : public Napi::ObjectWrap { Delay(const Napi::CallbackInfo &info); static Napi::Value Create(const Napi::CallbackInfo &info); - + void Finalize(Napi::Env env); Napi::Value GetEnabled(const Napi::CallbackInfo &info); void SetEnabled(const Napi::CallbackInfo &info, const Napi::Value &value); Napi::Value GetDelaySec(const Napi::CallbackInfo &info); @@ -38,4 +39,4 @@ class Delay : public Napi::ObjectWrap { Napi::Value GetPreserveDelay(const Napi::CallbackInfo &info); void SetPreserveDelay(const Napi::CallbackInfo &info, const Napi::Value &value); }; -} \ No newline at end of file +} diff --git a/obs-studio-client/source/enhanced-broadcasting-advanced-streaming.cpp b/obs-studio-client/source/enhanced-broadcasting-advanced-streaming.cpp new file mode 100644 index 000000000..0744f9899 --- /dev/null +++ b/obs-studio-client/source/enhanced-broadcasting-advanced-streaming.cpp @@ -0,0 +1,201 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#include "enhanced-broadcasting-advanced-streaming.hpp" +#include "utility.hpp" +#include "service.hpp" +#include "delay.hpp" +#include "reconnect.hpp" +#include "network.hpp" +#include "video.hpp" + +Napi::FunctionReference osn::EnhancedBroadcastingAdvancedStreaming::constructor; + +Napi::Object osn::EnhancedBroadcastingAdvancedStreaming::Init(Napi::Env env, Napi::Object exports) +{ + Napi::HandleScope scope(env); + Napi::Function func = DefineClass( + env, "EnhancedBroadcastingAdvancedStreaming", + {StaticMethod("create", &osn::EnhancedBroadcastingAdvancedStreaming::Create), + StaticMethod("destroy", &osn::EnhancedBroadcastingAdvancedStreaming::Destroy), + + InstanceAccessor("videoEncoder", &osn::EnhancedBroadcastingAdvancedStreaming::GetVideoEncoder, + &osn::EnhancedBroadcastingAdvancedStreaming::SetVideoEncoder), + InstanceAccessor("service", &osn::EnhancedBroadcastingAdvancedStreaming::GetService, &osn::EnhancedBroadcastingAdvancedStreaming::SetService), + InstanceAccessor("enforceServiceBitrate", &osn::EnhancedBroadcastingAdvancedStreaming::GetEnforceServiceBirate, + &osn::EnhancedBroadcastingAdvancedStreaming::SetEnforceServiceBirate), + InstanceAccessor("enableTwitchVOD", &osn::EnhancedBroadcastingAdvancedStreaming::GetEnableTwitchVOD, + &osn::EnhancedBroadcastingAdvancedStreaming::SetEnableTwitchVOD), + InstanceAccessor("signalHandler", &osn::EnhancedBroadcastingAdvancedStreaming::GetSignalHandler, + &osn::EnhancedBroadcastingAdvancedStreaming::SetSignalHandler), + InstanceAccessor("delay", &osn::EnhancedBroadcastingAdvancedStreaming::GetDelay, &osn::EnhancedBroadcastingAdvancedStreaming::SetDelay), + InstanceAccessor("reconnect", &osn::EnhancedBroadcastingAdvancedStreaming::GetReconnect, + &osn::EnhancedBroadcastingAdvancedStreaming::SetReconnect), + InstanceAccessor("network", &osn::EnhancedBroadcastingAdvancedStreaming::GetNetwork, &osn::EnhancedBroadcastingAdvancedStreaming::SetNetwork), + InstanceAccessor("video", &osn::EnhancedBroadcastingAdvancedStreaming::GetCanvas, &osn::EnhancedBroadcastingAdvancedStreaming::SetCanvas), + InstanceAccessor("additionalVideo", &osn::EnhancedBroadcastingAdvancedStreaming::GetAdditionalCanvas, + &osn::EnhancedBroadcastingAdvancedStreaming::SetAdditionalCanvas), + InstanceAccessor("droppedFrames", &osn::EnhancedBroadcastingAdvancedStreaming::GetDroppedFrames, nullptr), + InstanceAccessor("totalFrames", &osn::EnhancedBroadcastingAdvancedStreaming::GetTotalFrames, nullptr), + InstanceAccessor("kbitsPerSec", &osn::EnhancedBroadcastingAdvancedStreaming::GetKBitsPerSec, nullptr), + InstanceAccessor("dataOutput", &osn::EnhancedBroadcastingAdvancedStreaming::GetDataOutput, nullptr), + + InstanceAccessor("audioTrack", &osn::EnhancedBroadcastingAdvancedStreaming::GetAudioTrack, + &osn::EnhancedBroadcastingAdvancedStreaming::SetAudioTrack), + InstanceAccessor("twitchTrack", &osn::EnhancedBroadcastingAdvancedStreaming::GetTwitchTrack, + &osn::EnhancedBroadcastingAdvancedStreaming::SetTwitchTrack), + InstanceAccessor("rescaling", &osn::EnhancedBroadcastingAdvancedStreaming::GetRescaling, + &osn::EnhancedBroadcastingAdvancedStreaming::SetRescaling), + InstanceAccessor("outputWidth", &osn::EnhancedBroadcastingAdvancedStreaming::GetOutputWidth, + &osn::EnhancedBroadcastingAdvancedStreaming::SetOutputWidth), + InstanceAccessor("outputHeight", &osn::EnhancedBroadcastingAdvancedStreaming::GetOutputHeight, + &osn::EnhancedBroadcastingAdvancedStreaming::SetOutputHeight), + + InstanceMethod("start", &osn::EnhancedBroadcastingAdvancedStreaming::Start), + InstanceMethod("stop", &osn::EnhancedBroadcastingAdvancedStreaming::Stop), + + StaticAccessor("legacySettings", &osn::EnhancedBroadcastingAdvancedStreaming::GetLegacySettings, + &osn::EnhancedBroadcastingAdvancedStreaming::SetLegacySettings)}); + + exports.Set("EnhancedBroadcastingAdvancedStreaming", func); + osn::EnhancedBroadcastingAdvancedStreaming::constructor = Napi::Persistent(func); + osn::EnhancedBroadcastingAdvancedStreaming::constructor.SuppressDestruct(); + + return exports; +} + +osn::EnhancedBroadcastingAdvancedStreaming::EnhancedBroadcastingAdvancedStreaming(const Napi::CallbackInfo &info) + : Napi::ObjectWrap(info) +{ + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + size_t length = info.Length(); + + if (length <= 0 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); + return; + } + + this->uid = (uint64_t)info[0].ToNumber().Int64Value(); + this->className = std::string("EnhancedBroadcastingAdvancedStreaming"); +} + +Napi::Value osn::EnhancedBroadcastingAdvancedStreaming::Create(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("EnhancedBroadcastingAdvancedStreaming", "Create", {}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + auto instance = + osn::EnhancedBroadcastingAdvancedStreaming::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + + return instance; +} + +void osn::EnhancedBroadcastingAdvancedStreaming::Destroy(const Napi::CallbackInfo &info) +{ + if (info.Length() != 1) + return; + + auto stream = Napi::ObjectWrap::Unwrap(info[0].ToObject()); + + stream->stopWorker(); + stream->cb.Reset(); + + auto conn = GetConnection(info); + if (!conn) + return; + + std::vector response = conn->call_synchronous_helper("EnhancedBroadcastingAdvancedStreaming", "Destroy", {ipc::value(stream->uid)}); + + if (!ValidateResponse(info, response)) + return; +} + +Napi::Value osn::EnhancedBroadcastingAdvancedStreaming::GetAdditionalCanvas(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper(className, "GetAdditionalVideoCanvas", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + auto instance = osn::Video::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + + return instance; +} + +void osn::EnhancedBroadcastingAdvancedStreaming::SetAdditionalCanvas(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + osn::Video *canvas = Napi::ObjectWrap::Unwrap(value.ToObject()); + + if (!canvas) { + Napi::TypeError::New(info.Env(), "Invalid canvas argument").ThrowAsJavaScriptException(); + return; + } + + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call(className, "SetAdditionalVideoCanvas", {ipc::value(this->uid), ipc::value(canvas->canvasId)}); +} + +Napi::Value osn::EnhancedBroadcastingAdvancedStreaming::GetLegacySettings(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("EnhancedBroadcastingAdvancedStreaming", "GetLegacySettings", {}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + auto instance = + osn::EnhancedBroadcastingAdvancedStreaming::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + + return instance; +} + +void osn::EnhancedBroadcastingAdvancedStreaming::SetLegacySettings(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + osn::EnhancedBroadcastingAdvancedStreaming *streaming = Napi::ObjectWrap::Unwrap(value.ToObject()); + + if (!streaming) { + Napi::TypeError::New(info.Env(), "Invalid service argument").ThrowAsJavaScriptException(); + return; + } + + auto conn = GetConnection(info); + if (!conn) + return; + + std::vector response = conn->call_synchronous_helper("EnhancedBroadcastingAdvancedStreaming", "SetLegacySettings", {streaming->uid}); + + if (!ValidateResponse(info, response)) + return; +} \ No newline at end of file diff --git a/obs-studio-client/source/enhanced-broadcasting-advanced-streaming.hpp b/obs-studio-client/source/enhanced-broadcasting-advanced-streaming.hpp new file mode 100644 index 000000000..37ee11a2f --- /dev/null +++ b/obs-studio-client/source/enhanced-broadcasting-advanced-streaming.hpp @@ -0,0 +1,39 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once +#include +#include "advanced-streaming.hpp" + +namespace osn { +class EnhancedBroadcastingAdvancedStreaming : public Napi::ObjectWrap, public osn::AdvancedStreamingBase { +public: + static Napi::FunctionReference constructor; + static Napi::Object Init(Napi::Env env, Napi::Object exports); + EnhancedBroadcastingAdvancedStreaming(const Napi::CallbackInfo &info); + + static Napi::Value Create(const Napi::CallbackInfo &info); + static void Destroy(const Napi::CallbackInfo &info); + + Napi::Value GetAdditionalCanvas(const Napi::CallbackInfo &info); + void SetAdditionalCanvas(const Napi::CallbackInfo &info, const Napi::Value &value); + + static Napi::Value GetLegacySettings(const Napi::CallbackInfo &info); + static void SetLegacySettings(const Napi::CallbackInfo &info, const Napi::Value &value); +}; +} \ No newline at end of file diff --git a/obs-studio-client/source/enhanced-broadcasting-simple-streaming.cpp b/obs-studio-client/source/enhanced-broadcasting-simple-streaming.cpp new file mode 100644 index 000000000..01d95d940 --- /dev/null +++ b/obs-studio-client/source/enhanced-broadcasting-simple-streaming.cpp @@ -0,0 +1,198 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#include "enhanced-broadcasting-simple-streaming.hpp" +#include "utility.hpp" +#include "service.hpp" +#include "delay.hpp" +#include "reconnect.hpp" +#include "network.hpp" +#include "video.hpp" + +Napi::FunctionReference osn::EnhancedBroadcastingSimpleStreaming::constructor; + +Napi::Object osn::EnhancedBroadcastingSimpleStreaming::Init(Napi::Env env, Napi::Object exports) +{ + Napi::HandleScope scope(env); + Napi::Function func = DefineClass( + env, "EnhancedBroadcastingSimpleStreaming", + {StaticMethod("create", &osn::EnhancedBroadcastingSimpleStreaming::Create), + StaticMethod("destroy", &osn::EnhancedBroadcastingSimpleStreaming::Destroy), + + InstanceAccessor("videoEncoder", &osn::EnhancedBroadcastingSimpleStreaming::GetVideoEncoder, + &osn::EnhancedBroadcastingSimpleStreaming::SetVideoEncoder), + InstanceAccessor("audioEncoder", &osn::EnhancedBroadcastingSimpleStreaming::GetAudioEncoder, + &osn::EnhancedBroadcastingSimpleStreaming::SetAudioEncoder), + InstanceAccessor("service", &osn::EnhancedBroadcastingSimpleStreaming::GetService, &osn::EnhancedBroadcastingSimpleStreaming::SetService), + InstanceAccessor("enforceServiceBitrate", &osn::EnhancedBroadcastingSimpleStreaming::GetEnforceServiceBirate, + &osn::EnhancedBroadcastingSimpleStreaming::SetEnforceServiceBirate), + InstanceAccessor("enableTwitchVOD", &osn::EnhancedBroadcastingSimpleStreaming::GetEnableTwitchVOD, + &osn::EnhancedBroadcastingSimpleStreaming::SetEnableTwitchVOD), + InstanceAccessor("audioEncoder", &osn::EnhancedBroadcastingSimpleStreaming::GetAudioEncoder, + &osn::EnhancedBroadcastingSimpleStreaming::SetAudioEncoder), + InstanceAccessor("useAdvanced", &osn::EnhancedBroadcastingSimpleStreaming::GetUseAdvanced, + &osn::EnhancedBroadcastingSimpleStreaming::SetUseAdvanced), + InstanceAccessor("customEncSettings", &osn::EnhancedBroadcastingSimpleStreaming::GetCustomEncSettings, + &osn::EnhancedBroadcastingSimpleStreaming::SetCustomEncSettings), + InstanceAccessor("signalHandler", &osn::EnhancedBroadcastingSimpleStreaming::GetSignalHandler, + &osn::EnhancedBroadcastingSimpleStreaming::SetSignalHandler), + InstanceAccessor("delay", &osn::EnhancedBroadcastingSimpleStreaming::GetDelay, &osn::EnhancedBroadcastingSimpleStreaming::SetDelay), + InstanceAccessor("reconnect", &osn::EnhancedBroadcastingSimpleStreaming::GetReconnect, + &osn::EnhancedBroadcastingSimpleStreaming::SetReconnect), + InstanceAccessor("network", &osn::EnhancedBroadcastingSimpleStreaming::GetNetwork, &osn::EnhancedBroadcastingSimpleStreaming::SetNetwork), + InstanceAccessor("video", &osn::EnhancedBroadcastingSimpleStreaming::GetCanvas, &osn::EnhancedBroadcastingSimpleStreaming::SetCanvas), + InstanceAccessor("additionalVideo", &osn::EnhancedBroadcastingSimpleStreaming::GetAdditionalCanvas, + &osn::EnhancedBroadcastingSimpleStreaming::SetAdditionalCanvas), + InstanceAccessor("droppedFrames", &osn::EnhancedBroadcastingSimpleStreaming::GetDroppedFrames, nullptr), + InstanceAccessor("totalFrames", &osn::EnhancedBroadcastingSimpleStreaming::GetTotalFrames, nullptr), + InstanceAccessor("kbitsPerSec", &osn::EnhancedBroadcastingSimpleStreaming::GetKBitsPerSec, nullptr), + InstanceAccessor("dataOutput", &osn::EnhancedBroadcastingSimpleStreaming::GetDataOutput, nullptr), + + InstanceMethod("start", &osn::EnhancedBroadcastingSimpleStreaming::Start), + InstanceMethod("stop", &osn::EnhancedBroadcastingSimpleStreaming::Stop), + + StaticAccessor("legacySettings", &osn::EnhancedBroadcastingSimpleStreaming::GetLegacySettings, + &osn::EnhancedBroadcastingSimpleStreaming::SetLegacySettings)}); + + exports.Set("EnhancedBroadcastingSimpleStreaming", func); + osn::EnhancedBroadcastingSimpleStreaming::constructor = Napi::Persistent(func); + osn::EnhancedBroadcastingSimpleStreaming::constructor.SuppressDestruct(); + + return exports; +} + +osn::EnhancedBroadcastingSimpleStreaming::EnhancedBroadcastingSimpleStreaming(const Napi::CallbackInfo &info) + : Napi::ObjectWrap(info) +{ + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + size_t length = info.Length(); + + if (length <= 0 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); + return; + } + + this->uid = (uint64_t)info[0].ToNumber().Int64Value(); + this->className = std::string("EnhancedBroadcastingSimpleStreaming"); +} + +Napi::Value osn::EnhancedBroadcastingSimpleStreaming::Create(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("EnhancedBroadcastingSimpleStreaming", "Create", {}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + auto instance = + osn::EnhancedBroadcastingSimpleStreaming::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + + return instance; +} + +void osn::EnhancedBroadcastingSimpleStreaming::Destroy(const Napi::CallbackInfo &info) +{ + if (info.Length() != 1) + return; + + auto stream = Napi::ObjectWrap::Unwrap(info[0].ToObject()); + + stream->stopWorker(); + stream->cb.Reset(); + + auto conn = GetConnection(info); + if (!conn) + return; + + std::vector response = conn->call_synchronous_helper("EnhancedBroadcastingSimpleStreaming", "Destroy", {ipc::value(stream->uid)}); + + if (!ValidateResponse(info, response)) + return; +} + +Napi::Value osn::EnhancedBroadcastingSimpleStreaming::GetAdditionalCanvas(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper(className, "GetAdditionalVideoCanvas", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + auto instance = osn::Video::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + + return instance; +} + +void osn::EnhancedBroadcastingSimpleStreaming::SetAdditionalCanvas(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + osn::Video *canvas = Napi::ObjectWrap::Unwrap(value.ToObject()); + + if (!canvas) { + Napi::TypeError::New(info.Env(), "Invalid canvas argument").ThrowAsJavaScriptException(); + return; + } + + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call(className, "SetAdditionalVideoCanvas", {ipc::value(this->uid), ipc::value(canvas->canvasId)}); +} + +Napi::Value osn::EnhancedBroadcastingSimpleStreaming::GetLegacySettings(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("EnhancedBroadcastingSimpleStreaming", "GetLegacySettings", {}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + auto instance = + osn::EnhancedBroadcastingSimpleStreaming::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + + return instance; +} + +void osn::EnhancedBroadcastingSimpleStreaming::SetLegacySettings(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + osn::EnhancedBroadcastingSimpleStreaming *streaming = Napi::ObjectWrap::Unwrap(value.ToObject()); + + if (!streaming) { + Napi::TypeError::New(info.Env(), "Invalid service argument").ThrowAsJavaScriptException(); + return; + } + + auto conn = GetConnection(info); + if (!conn) + return; + + std::vector response = conn->call_synchronous_helper("EnhancedBroadcastingSimpleStreaming", "SetLegacySettings", {streaming->uid}); + + if (!ValidateResponse(info, response)) + return; +} \ No newline at end of file diff --git a/obs-studio-client/source/enhanced-broadcasting-simple-streaming.hpp b/obs-studio-client/source/enhanced-broadcasting-simple-streaming.hpp new file mode 100644 index 000000000..00b60d49b --- /dev/null +++ b/obs-studio-client/source/enhanced-broadcasting-simple-streaming.hpp @@ -0,0 +1,39 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once +#include +#include "simple-streaming.hpp" + +namespace osn { +class EnhancedBroadcastingSimpleStreaming : public Napi::ObjectWrap, public osn::SimpleStreamingBase { +public: + static Napi::FunctionReference constructor; + static Napi::Object Init(Napi::Env env, Napi::Object exports); + EnhancedBroadcastingSimpleStreaming(const Napi::CallbackInfo &info); + + static Napi::Value Create(const Napi::CallbackInfo &info); + static void Destroy(const Napi::CallbackInfo &info); + + Napi::Value GetAdditionalCanvas(const Napi::CallbackInfo &info); + void SetAdditionalCanvas(const Napi::CallbackInfo &info, const Napi::Value &value); + + static Napi::Value GetLegacySettings(const Napi::CallbackInfo &info); + static void SetLegacySettings(const Napi::CallbackInfo &info, const Napi::Value &value); +}; +} \ No newline at end of file diff --git a/obs-studio-client/source/main.cpp b/obs-studio-client/source/main.cpp index 132f8efac..5b439f5ca 100644 --- a/obs-studio-client/source/main.cpp +++ b/obs-studio-client/source/main.cpp @@ -54,6 +54,8 @@ #include "advanced-recording.hpp" #include "simple-replay-buffer.hpp" #include "advanced-replay-buffer.hpp" +#include "enhanced-broadcasting-advanced-streaming.hpp" +#include "enhanced-broadcasting-simple-streaming.hpp" #if defined(_WIN32) // Checks ForceGPUAsRenderDevice setting @@ -138,6 +140,8 @@ Napi::Object main_node(Napi::Env env, Napi::Object exports) osn::AdvancedRecording::Init(env, exports); osn::SimpleReplayBuffer::Init(env, exports); osn::AdvancedReplayBuffer::Init(env, exports); + osn::EnhancedBroadcastingAdvancedStreaming::Init(env, exports); + osn::EnhancedBroadcastingSimpleStreaming::Init(env, exports); return exports; }; diff --git a/obs-studio-client/source/network.cpp b/obs-studio-client/source/network.cpp index 840548efa..0df83e801 100644 --- a/obs-studio-client/source/network.cpp +++ b/obs-studio-client/source/network.cpp @@ -48,7 +48,8 @@ osn::Network::Network(const Napi::CallbackInfo &info) : Napi::ObjectWrapuid = 0; + this->uid = UINT64_MAX; + this->connectionEpoch = 0; if (length <= 0 || !info[0].IsNumber()) { Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); @@ -56,6 +57,7 @@ osn::Network::Network(const Napi::CallbackInfo &info) : Napi::ObjectWrapuid = (uint64_t)info[0].ToNumber().Int64Value(); + this->connectionEpoch = Controller::GetInstance().GetConnectionEpoch(); } Napi::Value osn::Network::Create(const Napi::CallbackInfo &info) @@ -74,6 +76,28 @@ Napi::Value osn::Network::Create(const Napi::CallbackInfo &info) return instance; } +void osn::Network::Finalize(Napi::Env env) +{ + if (this->uid == UINT64_MAX) + return; + + // If OBS was restarted/disconnected, skip IPC cleanup for this stale wrapper. + if (this->connectionEpoch != Controller::GetInstance().GetConnectionEpoch()) { + this->uid = UINT64_MAX; + return; + } + + auto conn = Controller::GetInstance().GetConnection(); + if (!conn) { + this->uid = UINT64_MAX; + return; + } + + conn->call_synchronous_helper("Network", "Destroy", {ipc::value(this->uid)}); + + this->uid = UINT64_MAX; +} + Napi::Value osn::Network::GetBindIP(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); diff --git a/obs-studio-client/source/network.hpp b/obs-studio-client/source/network.hpp index 6a3b14824..e21619e01 100644 --- a/obs-studio-client/source/network.hpp +++ b/obs-studio-client/source/network.hpp @@ -23,6 +23,7 @@ namespace osn { class Network : public Napi::ObjectWrap { public: uint64_t uid; + uint64_t connectionEpoch; public: static Napi::FunctionReference constructor; @@ -30,7 +31,7 @@ class Network : public Napi::ObjectWrap { Network(const Napi::CallbackInfo &info); static Napi::Value Create(const Napi::CallbackInfo &info); - + void Finalize(Napi::Env env); Napi::Value GetBindIP(const Napi::CallbackInfo &info); void SetBindIP(const Napi::CallbackInfo &info, const Napi::Value &value); Napi::Value GetNetworkInterfaces(const Napi::CallbackInfo &info); @@ -41,4 +42,4 @@ class Network : public Napi::ObjectWrap { Napi::Value GetEnableLowLatency(const Napi::CallbackInfo &info); void SetEnableLowLatency(const Napi::CallbackInfo &info, const Napi::Value &value); }; -} \ No newline at end of file +} diff --git a/obs-studio-client/source/reconnect.cpp b/obs-studio-client/source/reconnect.cpp index 237ba54ab..9f29e9c31 100644 --- a/obs-studio-client/source/reconnect.cpp +++ b/obs-studio-client/source/reconnect.cpp @@ -46,6 +46,7 @@ osn::Reconnect::Reconnect(const Napi::CallbackInfo &info) : Napi::ObjectWrapuid = 0; + this->connectionEpoch = 0; if (length <= 0 || !info[0].IsNumber()) { Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); @@ -53,6 +54,7 @@ osn::Reconnect::Reconnect(const Napi::CallbackInfo &info) : Napi::ObjectWrapuid = (uint64_t)info[0].ToNumber().Int64Value(); + this->connectionEpoch = Controller::GetInstance().GetConnectionEpoch(); } Napi::Value osn::Reconnect::Create(const Napi::CallbackInfo &info) @@ -71,6 +73,26 @@ Napi::Value osn::Reconnect::Create(const Napi::CallbackInfo &info) return instance; } +void osn::Reconnect::Finalize(Napi::Env env) +{ + if (this->uid != UINT64_MAX) { + // If OBS was restarted/disconnected, skip IPC cleanup for this stale wrapper. + if (this->connectionEpoch != Controller::GetInstance().GetConnectionEpoch()) { + this->uid = UINT64_MAX; + return; + } + + auto conn = Controller::GetInstance().GetConnection(); + if (!conn) { + this->uid = UINT64_MAX; + return; + } + + conn->call_synchronous_helper("Reconnect", "Destroy", {ipc::value(this->uid)}); + this->uid = UINT64_MAX; + } +} + Napi::Value osn::Reconnect::GetEnabled(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); diff --git a/obs-studio-client/source/reconnect.hpp b/obs-studio-client/source/reconnect.hpp index 29ebfe273..93c798ea4 100644 --- a/obs-studio-client/source/reconnect.hpp +++ b/obs-studio-client/source/reconnect.hpp @@ -23,6 +23,7 @@ namespace osn { class Reconnect : public Napi::ObjectWrap { public: uint64_t uid; + uint64_t connectionEpoch; public: static Napi::FunctionReference constructor; @@ -30,6 +31,7 @@ class Reconnect : public Napi::ObjectWrap { Reconnect(const Napi::CallbackInfo &info); static Napi::Value Create(const Napi::CallbackInfo &info); + void Finalize(Napi::Env env); Napi::Value GetEnabled(const Napi::CallbackInfo &info); void SetEnabled(const Napi::CallbackInfo &info, const Napi::Value &value); @@ -38,4 +40,4 @@ class Reconnect : public Napi::ObjectWrap { Napi::Value GetMaxRetries(const Napi::CallbackInfo &info); void SetMaxRetries(const Napi::CallbackInfo &info, const Napi::Value &value); }; -} \ No newline at end of file +} diff --git a/obs-studio-client/source/recording.cpp b/obs-studio-client/source/recording.cpp index 656aed422..62d607461 100644 --- a/obs-studio-client/source/recording.cpp +++ b/obs-studio-client/source/recording.cpp @@ -21,23 +21,27 @@ #include "video-encoder.hpp" Napi::Value osn::Recording::GetVideoEncoder(const Napi::CallbackInfo &info) +{ + return videoEncoderRef.IsEmpty() ? info.Env().Undefined() : videoEncoderRef.Value(); +} + +void osn::Recording::SetVideoEncoder(const Napi::CallbackInfo &info, const Napi::Value &value) { auto conn = GetConnection(info); if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetVideoEncoder", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); + return; - auto instance = osn::VideoEncoder::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + if (value.IsNull() || value.IsUndefined()) { + if (!videoEncoderRef.IsEmpty()) + videoEncoderRef.Reset(); + conn->call(className, "SetVideoEncoder", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); + return; + } - return instance; -} + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::VideoEncoder::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a VideoEncoder").ThrowAsJavaScriptException(); -void osn::Recording::SetVideoEncoder(const Napi::CallbackInfo &info, const Napi::Value &value) -{ osn::VideoEncoder *encoder = Napi::ObjectWrap::Unwrap(value.ToObject()); if (!encoder) { @@ -45,11 +49,12 @@ void osn::Recording::SetVideoEncoder(const Napi::CallbackInfo &info, const Napi: return; } - auto conn = GetConnection(info); - if (!conn) - return; - conn->call(className, "SetVideoEncoder", {ipc::value(this->uid), ipc::value(encoder->uid)}); + + if (!videoEncoderRef.IsEmpty()) + videoEncoderRef.Reset(); + + videoEncoderRef = Napi::Persistent(obj); } Napi::Value osn::Recording::GetSignalHandler(const Napi::CallbackInfo &info) diff --git a/obs-studio-client/source/recording.hpp b/obs-studio-client/source/recording.hpp index 84af80883..b597b7222 100644 --- a/obs-studio-client/source/recording.hpp +++ b/obs-studio-client/source/recording.hpp @@ -29,6 +29,9 @@ class Recording : public WorkerSignals, public FileOutput { protected: Napi::Function signalHandler; std::string className; + Napi::Reference videoEncoderRef; + Napi::Reference streamingRef; + Napi::Reference audioEncoderRef; Napi::Value GetVideoEncoder(const Napi::CallbackInfo &info); void SetVideoEncoder(const Napi::CallbackInfo &info, const Napi::Value &value); diff --git a/obs-studio-client/source/replay-buffer.cpp b/obs-studio-client/source/replay-buffer.cpp index f459cd4b5..de019905c 100644 --- a/obs-studio-client/source/replay-buffer.cpp +++ b/obs-studio-client/source/replay-buffer.cpp @@ -91,24 +91,17 @@ void osn::ReplayBuffer::SetSuffix(const Napi::CallbackInfo &info, const Napi::Va Napi::Value osn::ReplayBuffer::GetUsesStream(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetUsesStream", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - return Napi::Boolean::New(info.Env(), response[1].value_union.ui32); + return Napi::Boolean::New(info.Env(), usesStream); } void osn::ReplayBuffer::SetUsesStream(const Napi::CallbackInfo &info, const Napi::Value &value) { + //todo should be depricated auto conn = GetConnection(info); if (!conn) return; + usesStream = value.ToBoolean().Value(); conn->call_synchronous_helper(className, "SetUsesStream", {ipc::value(this->uid), ipc::value(value.ToBoolean().Value())}); } diff --git a/obs-studio-client/source/replay-buffer.hpp b/obs-studio-client/source/replay-buffer.hpp index 5250b5d75..8fbac491f 100644 --- a/obs-studio-client/source/replay-buffer.hpp +++ b/obs-studio-client/source/replay-buffer.hpp @@ -29,6 +29,8 @@ class ReplayBuffer : public WorkerSignals, public FileOutput { protected: Napi::Function signalHandler; std::string className; + Napi::Reference parentOutputRef; + bool usesStream = false; Napi::Value GetDuration(const Napi::CallbackInfo &info); void SetDuration(const Napi::CallbackInfo &info, const Napi::Value &value); diff --git a/obs-studio-client/source/simple-recording.cpp b/obs-studio-client/source/simple-recording.cpp index f80456ec1..148efe52a 100644 --- a/obs-studio-client/source/simple-recording.cpp +++ b/obs-studio-client/source/simple-recording.cpp @@ -85,6 +85,23 @@ osn::SimpleRecording::SimpleRecording(const Napi::CallbackInfo &info) : Napi::Ob this->className = std::string("SimpleRecording"); } +void osn::SimpleRecording::Finalize(Napi::Env) +{ + ReleaseObjects(); +} + +void osn::SimpleRecording::ReleaseObjects() +{ + if (!videoEncoderRef.IsEmpty()) + videoEncoderRef.Reset(); + + if (!streamingRef.IsEmpty()) + streamingRef.Reset(); + + if (!audioEncoderRef.IsEmpty()) + audioEncoderRef.Reset(); +} + Napi::Value osn::SimpleRecording::Create(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); @@ -111,6 +128,8 @@ void osn::SimpleRecording::Destroy(const Napi::CallbackInfo &info) recording->stopWorker(); recording->cb.Reset(); + recording->ReleaseObjects(); + auto conn = GetConnection(info); if (!conn) return; @@ -145,22 +164,27 @@ void osn::SimpleRecording::SetQuality(const Napi::CallbackInfo &info, const Napi } Napi::Value osn::SimpleRecording::GetAudioEncoder(const Napi::CallbackInfo &info) +{ + return audioEncoderRef.IsEmpty() ? info.Env().Undefined() : audioEncoderRef.Value(); +} + +void osn::SimpleRecording::SetAudioEncoder(const Napi::CallbackInfo &info, const Napi::Value &value) { auto conn = GetConnection(info); if (!conn) - return info.Env().Undefined(); + return; - std::vector response = conn->call_synchronous_helper("SimpleRecording", "GetAudioEncoder", {ipc::value(this->uid)}); + if (value.IsNull() || value.IsUndefined()) { + if (!audioEncoderRef.IsEmpty()) + audioEncoderRef.Reset(); + conn->call(className, "SetAudioEncoder", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); + return; + } - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::AudioEncoder::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a AudioEncoder").ThrowAsJavaScriptException(); - auto instance = osn::AudioEncoder::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - return instance; -} - -void osn::SimpleRecording::SetAudioEncoder(const Napi::CallbackInfo &info, const Napi::Value &value) -{ osn::AudioEncoder *encoder = Napi::ObjectWrap::Unwrap(value.ToObject()); if (!encoder) { @@ -168,11 +192,12 @@ void osn::SimpleRecording::SetAudioEncoder(const Napi::CallbackInfo &info, const return; } - auto conn = GetConnection(info); - if (!conn) - return; - conn->call(className, "SetAudioEncoder", {ipc::value(this->uid), ipc::value(encoder->uid)}); + + if (!audioEncoderRef.IsEmpty()) + audioEncoderRef.Reset(); + + audioEncoderRef = Napi::Persistent(obj); } Napi::Value osn::SimpleRecording::GetLowCPU(const Napi::CallbackInfo &info) @@ -234,22 +259,27 @@ void osn::SimpleRecording::SetLegacySettings(const Napi::CallbackInfo &info, con } Napi::Value osn::SimpleRecording::GetStreaming(const Napi::CallbackInfo &info) +{ + return streamingRef.IsEmpty() ? info.Env().Undefined() : streamingRef.Value(); +} + +void osn::SimpleRecording::SetStreaming(const Napi::CallbackInfo &info, const Napi::Value &value) { auto conn = GetConnection(info); if (!conn) - return info.Env().Undefined(); - - std::vector 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::SimpleStreaming::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - return instance; -} + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::SimpleStreaming::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a SimpleStreaming").ThrowAsJavaScriptException(); -void osn::SimpleRecording::SetStreaming(const Napi::CallbackInfo &info, const Napi::Value &value) -{ osn::SimpleStreaming *streaming = Napi::ObjectWrap::Unwrap(value.ToObject()); if (!streaming) { @@ -257,9 +287,10 @@ void osn::SimpleRecording::SetStreaming(const Napi::CallbackInfo &info, const Na 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); } \ No newline at end of file diff --git a/obs-studio-client/source/simple-recording.hpp b/obs-studio-client/source/simple-recording.hpp index f1913234a..7d6b28851 100644 --- a/obs-studio-client/source/simple-recording.hpp +++ b/obs-studio-client/source/simple-recording.hpp @@ -26,7 +26,8 @@ class SimpleRecording : public Napi::ObjectWrap, public os static Napi::FunctionReference constructor; static Napi::Object Init(Napi::Env env, Napi::Object exports); SimpleRecording(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); diff --git a/obs-studio-client/source/simple-replay-buffer.cpp b/obs-studio-client/source/simple-replay-buffer.cpp index e238a6f9a..d6163f96e 100644 --- a/obs-studio-client/source/simple-replay-buffer.cpp +++ b/obs-studio-client/source/simple-replay-buffer.cpp @@ -77,6 +77,17 @@ osn::SimpleReplayBuffer::SimpleReplayBuffer(const Napi::CallbackInfo &info) : Na this->className = std::string("SimpleReplayBuffer"); } +void osn::SimpleReplayBuffer::Finalize(Napi::Env) +{ + ReleaseObjects(); +} + +void osn::SimpleReplayBuffer::ReleaseObjects() +{ + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); +} + Napi::Value osn::SimpleReplayBuffer::Create(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); @@ -102,6 +113,7 @@ void osn::SimpleReplayBuffer::Destroy(const Napi::CallbackInfo &info) replayBuffer->stopWorker(); replayBuffer->cb.Reset(); + replayBuffer->ReleaseObjects(); auto conn = GetConnection(info); if (!conn) @@ -150,62 +162,82 @@ void osn::SimpleReplayBuffer::SetLegacySettings(const Napi::CallbackInfo &info, Napi::Value osn::SimpleReplayBuffer::GetStreaming(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetStreaming", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) + if (usesStream) { + return parentOutputRef.IsEmpty() ? info.Env().Undefined() : parentOutputRef.Value(); + } else { return info.Env().Undefined(); - - auto instance = osn::SimpleStreaming::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - return instance; + } } void osn::SimpleReplayBuffer::SetStreaming(const Napi::CallbackInfo &info, const Napi::Value &value) { - osn::SimpleStreaming *encoder = Napi::ObjectWrap::Unwrap(value.ToObject()); + auto conn = GetConnection(info); + if (!conn) + return; - if (!encoder) { - Napi::TypeError::New(info.Env(), "Invalid streaming argument").ThrowAsJavaScriptException(); + if (value.IsNull() || value.IsUndefined()) { + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); + conn->call(className, "SetStreaming", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); + usesStream = false; return; } - auto conn = GetConnection(info); - if (!conn) + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::SimpleStreaming::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a SimpleStreaming").ThrowAsJavaScriptException(); + + osn::SimpleStreaming *streaming = Napi::ObjectWrap::Unwrap(value.ToObject()); + + if (!streaming) { + Napi::TypeError::New(info.Env(), "Invalid streaming argument").ThrowAsJavaScriptException(); return; + } + + conn->call(className, "SetStreaming", {ipc::value(this->uid), ipc::value(streaming->uid)}); + usesStream = true; + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); - conn->call(className, "SetStreaming", {ipc::value(this->uid), ipc::value(encoder->uid)}); + parentOutputRef = Napi::Persistent(obj); } Napi::Value osn::SimpleReplayBuffer::GetRecording(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetRecording", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) + if (usesStream) { return info.Env().Undefined(); - - auto instance = osn::SimpleRecording::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - return instance; + } else { + return parentOutputRef.IsEmpty() ? info.Env().Undefined() : parentOutputRef.Value(); + } } void osn::SimpleReplayBuffer::SetRecording(const Napi::CallbackInfo &info, const Napi::Value &value) { - osn::SimpleRecording *recording = Napi::ObjectWrap::Unwrap(value.ToObject()); + auto conn = GetConnection(info); + if (!conn) + return; - if (!recording) { - Napi::TypeError::New(info.Env(), "Invalid recording argument").ThrowAsJavaScriptException(); + if (value.IsNull() || value.IsUndefined()) { + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); + conn->call(className, "SetRecording", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); + usesStream = false; return; } - auto conn = GetConnection(info); - if (!conn) + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::SimpleRecording::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a Valid Recording").ThrowAsJavaScriptException(); + + osn::SimpleRecording *recording = Napi::ObjectWrap::Unwrap(obj); + if (!recording) { + Napi::TypeError::New(info.Env(), "Invalid recording argument").ThrowAsJavaScriptException(); return; + } conn->call(className, "SetRecording", {ipc::value(this->uid), ipc::value(recording->uid)}); + usesStream = false; + if (!parentOutputRef.IsEmpty()) + parentOutputRef.Reset(); + parentOutputRef = Napi::Persistent(obj); } \ No newline at end of file diff --git a/obs-studio-client/source/simple-replay-buffer.hpp b/obs-studio-client/source/simple-replay-buffer.hpp index e1467d050..e8f8c364a 100644 --- a/obs-studio-client/source/simple-replay-buffer.hpp +++ b/obs-studio-client/source/simple-replay-buffer.hpp @@ -26,7 +26,8 @@ class SimpleReplayBuffer : public Napi::ObjectWrap, pub static Napi::FunctionReference constructor; static Napi::Object Init(Napi::Env env, Napi::Object exports); SimpleReplayBuffer(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); diff --git a/obs-studio-client/source/simple-streaming-base.cpp b/obs-studio-client/source/simple-streaming-base.cpp new file mode 100644 index 000000000..cc7eaed06 --- /dev/null +++ b/obs-studio-client/source/simple-streaming-base.cpp @@ -0,0 +1,104 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#include "simple-streaming-base.hpp" + +#include "audio-encoder.hpp" + +Napi::Value osn::SimpleStreamingBase::GetAudioEncoder(const Napi::CallbackInfo &info) +{ + return audioEncoderRef.IsEmpty() ? info.Env().Undefined() : audioEncoderRef.Value(); +} + +Napi::Value osn::SimpleStreamingBase::GetUseAdvanced(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("SimpleStreaming", "GetUseAdvanced", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + return Napi::Boolean::New(info.Env(), response[1].value_union.ui32); +} + +void osn::SimpleStreamingBase::SetUseAdvanced(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call_synchronous_helper("SimpleStreaming", "SetUseAdvanced", {ipc::value(this->uid), ipc::value(value.ToBoolean().Value())}); +} + +Napi::Value osn::SimpleStreamingBase::GetCustomEncSettings(const Napi::CallbackInfo &info) +{ + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + std::vector response = conn->call_synchronous_helper("SimpleStreaming", "GetCustomEncSettings", {ipc::value(this->uid)}); + + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); + + return Napi::String::New(info.Env(), response[1].value_str.c_str()); +} + +void osn::SimpleStreamingBase::SetCustomEncSettings(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + auto conn = GetConnection(info); + if (!conn) + return; + + conn->call_synchronous_helper("SimpleStreaming", "SetCustomEncSettings", {ipc::value(this->uid), ipc::value(value.ToString().Utf8Value())}); +} + +void osn::SimpleStreamingBase::SetAudioEncoder(const Napi::CallbackInfo &info, const Napi::Value &value) +{ + auto conn = GetConnection(info); + if (!conn) + return; + + if (value.IsNull() || value.IsUndefined()) { + if (!audioEncoderRef.IsEmpty()) + audioEncoderRef.Reset(); + conn->call(className, "SetAudioEncoder", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); + return; + } + + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::AudioEncoder::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a AudioEncoder").ThrowAsJavaScriptException(); + + osn::AudioEncoder *encoder = Napi::ObjectWrap::Unwrap(value.ToObject()); + + if (!encoder) { + Napi::TypeError::New(info.Env(), "Invalid encoder argument").ThrowAsJavaScriptException(); + return; + } + + conn->call(className, "SetAudioEncoder", {ipc::value(this->uid), ipc::value(encoder->uid)}); + + if (!audioEncoderRef.IsEmpty()) + audioEncoderRef.Reset(); + + audioEncoderRef = Napi::Persistent(obj); +} diff --git a/obs-studio-server/source/osn-output-signals.hpp b/obs-studio-client/source/simple-streaming-base.hpp similarity index 56% rename from obs-studio-server/source/osn-output-signals.hpp rename to obs-studio-client/source/simple-streaming-base.hpp index 50da406ed..b92a38463 100644 --- a/obs-studio-server/source/osn-output-signals.hpp +++ b/obs-studio-client/source/simple-streaming-base.hpp @@ -17,46 +17,19 @@ ******************************************************************************/ #pragma once -#include -#include -#include -#include +#include +#include "streaming.hpp" namespace osn { -struct signalInfo { - std::string signal; - int code; - std::string errorMessage; -}; - -class OutputSignals { -public: - OutputSignals() - { - output = nullptr; - canvas = nullptr; - } - virtual ~OutputSignals() {} +class SimpleStreamingBase : public osn::Streaming { public: - std::mutex signalsMtx; - std::queue signalsReceived; - std::vector signals; - obs_output_t *output; - obs_video_info *canvas; - - void ConnectSignals(); - -public: - std::condition_variable cvStop; - std::mutex mtxOutputStop; - void createOutput(const std::string &type, const std::string &name); - void deleteOutput(); - void startOutput(); + Napi::Value GetAudioEncoder(const Napi::CallbackInfo &info); + void SetAudioEncoder(const Napi::CallbackInfo &info, const Napi::Value &value); + Napi::Value GetUseAdvanced(const Napi::CallbackInfo &info); + void SetUseAdvanced(const Napi::CallbackInfo &info, const Napi::Value &value); + Napi::Value GetCustomEncSettings(const Napi::CallbackInfo &info); + void SetCustomEncSettings(const Napi::CallbackInfo &info, const Napi::Value &value); }; -struct cbData { - std::string signal; - OutputSignals *outputClass; -}; -} +} \ No newline at end of file diff --git a/obs-studio-client/source/simple-streaming.cpp b/obs-studio-client/source/simple-streaming.cpp index 8c3e56246..e28f8f3a0 100644 --- a/obs-studio-client/source/simple-streaming.cpp +++ b/obs-studio-client/source/simple-streaming.cpp @@ -35,7 +35,6 @@ Napi::Object osn::SimpleStreaming::Init(Napi::Env env, Napi::Object exports) StaticMethod("destroy", &osn::SimpleStreaming::Destroy), InstanceAccessor("videoEncoder", &osn::SimpleStreaming::GetVideoEncoder, &osn::SimpleStreaming::SetVideoEncoder), - InstanceAccessor("audioEncoder", &osn::SimpleStreaming::GetAudioEncoder, &osn::SimpleStreaming::SetAudioEncoder), InstanceAccessor("service", &osn::SimpleStreaming::GetService, &osn::SimpleStreaming::SetService), InstanceAccessor("enforceServiceBitrate", &osn::SimpleStreaming::GetEnforceServiceBirate, &osn::SimpleStreaming::SetEnforceServiceBirate), InstanceAccessor("enableTwitchVOD", &osn::SimpleStreaming::GetEnableTwitchVOD, &osn::SimpleStreaming::SetEnableTwitchVOD), @@ -79,6 +78,16 @@ osn::SimpleStreaming::SimpleStreaming(const Napi::CallbackInfo &info) : Napi::Ob this->className = std::string("SimpleStreaming"); } +void osn::SimpleStreaming::Finalize(Napi::Env) +{ + ReleaseObjects(); +} + +void osn::SimpleStreaming::ReleaseObjects() +{ + osn::Streaming::ReleaseObjects(); +} + Napi::Value osn::SimpleStreaming::Create(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); @@ -105,91 +114,16 @@ void osn::SimpleStreaming::Destroy(const Napi::CallbackInfo &info) stream->stopWorker(); stream->cb.Reset(); - auto conn = GetConnection(info); - if (!conn) - return; - - std::vector response = conn->call_synchronous_helper("SimpleStreaming", "Destroy", {ipc::value(stream->uid)}); - - if (!ValidateResponse(info, response)) - return; -} - -Napi::Value osn::SimpleStreaming::GetAudioEncoder(const Napi::CallbackInfo &info) -{ - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("SimpleStreaming", "GetAudioEncoder", {ipc::value(this->uid)}); + stream->ReleaseObjects(); - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - auto instance = osn::AudioEncoder::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - return instance; -} - -Napi::Value osn::SimpleStreaming::GetUseAdvanced(const Napi::CallbackInfo &info) -{ - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("SimpleStreaming", "GetUseAdvanced", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - return Napi::Boolean::New(info.Env(), response[1].value_union.ui32); -} - -void osn::SimpleStreaming::SetUseAdvanced(const Napi::CallbackInfo &info, const Napi::Value &value) -{ auto conn = GetConnection(info); if (!conn) return; - conn->call_synchronous_helper("SimpleStreaming", "SetUseAdvanced", {ipc::value(this->uid), ipc::value(value.ToBoolean().Value())}); -} - -Napi::Value osn::SimpleStreaming::GetCustomEncSettings(const Napi::CallbackInfo &info) -{ - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("SimpleStreaming", "GetCustomEncSettings", {ipc::value(this->uid)}); + std::vector response = conn->call_synchronous_helper("SimpleStreaming", "Destroy", {ipc::value(stream->uid)}); if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - return Napi::String::New(info.Env(), response[1].value_str.c_str()); -} - -void osn::SimpleStreaming::SetCustomEncSettings(const Napi::CallbackInfo &info, const Napi::Value &value) -{ - auto conn = GetConnection(info); - if (!conn) - return; - - conn->call_synchronous_helper("SimpleStreaming", "SetCustomEncSettings", {ipc::value(this->uid), ipc::value(value.ToString().Utf8Value())}); -} - -void osn::SimpleStreaming::SetAudioEncoder(const Napi::CallbackInfo &info, const Napi::Value &value) -{ - osn::AudioEncoder *encoder = Napi::ObjectWrap::Unwrap(value.ToObject()); - - if (!encoder) { - Napi::TypeError::New(info.Env(), "Invalid encoder argument").ThrowAsJavaScriptException(); - return; - } - - auto conn = GetConnection(info); - if (!conn) return; - - conn->call(className, "SetAudioEncoder", {ipc::value(this->uid), ipc::value(encoder->uid)}); } Napi::Value osn::SimpleStreaming::GetLegacySettings(const Napi::CallbackInfo &info) diff --git a/obs-studio-client/source/simple-streaming.hpp b/obs-studio-client/source/simple-streaming.hpp index 95edee1ab..75b09ed4c 100644 --- a/obs-studio-client/source/simple-streaming.hpp +++ b/obs-studio-client/source/simple-streaming.hpp @@ -18,25 +18,20 @@ #pragma once #include -#include "streaming.hpp" +#include "simple-streaming-base.hpp" namespace osn { -class SimpleStreaming : public Napi::ObjectWrap, public osn::Streaming { + +class SimpleStreaming : public Napi::ObjectWrap, public SimpleStreamingBase { public: static Napi::FunctionReference constructor; static Napi::Object Init(Napi::Env env, Napi::Object exports); SimpleStreaming(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); - Napi::Value GetAudioEncoder(const Napi::CallbackInfo &info); - void SetAudioEncoder(const Napi::CallbackInfo &info, const Napi::Value &value); - Napi::Value GetUseAdvanced(const Napi::CallbackInfo &info); - void SetUseAdvanced(const Napi::CallbackInfo &info, const Napi::Value &value); - Napi::Value GetCustomEncSettings(const Napi::CallbackInfo &info); - void SetCustomEncSettings(const Napi::CallbackInfo &info, const Napi::Value &value); - static Napi::Value GetLegacySettings(const Napi::CallbackInfo &info); static void SetLegacySettings(const Napi::CallbackInfo &info, const Napi::Value &value); }; diff --git a/obs-studio-client/source/streaming.cpp b/obs-studio-client/source/streaming.cpp index c51b906a6..831cdc783 100644 --- a/obs-studio-client/source/streaming.cpp +++ b/obs-studio-client/source/streaming.cpp @@ -25,36 +25,61 @@ #include "network.hpp" #include "video.hpp" -Napi::Value osn::Streaming::GetService(const Napi::CallbackInfo &info) +void osn::Streaming::ReleaseObjects() { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); + if (!videoEncoderRef.IsEmpty()) + videoEncoderRef.Reset(); - std::vector response = conn->call_synchronous_helper(className, "GetService", {ipc::value(this->uid)}); + if (!serviceRef.IsEmpty()) + serviceRef.Reset(); - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); + if (!delayRef.IsEmpty()) + delayRef.Reset(); - auto instance = osn::Service::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + if (!reconnectRef.IsEmpty()) + reconnectRef.Reset(); - return instance; + if (!networkRef.IsEmpty()) + networkRef.Reset(); + + if (!audioEncoderRef.IsEmpty()) + audioEncoderRef.Reset(); +} + +Napi::Value osn::Streaming::GetService(const Napi::CallbackInfo &info) +{ + return serviceRef.IsEmpty() ? info.Env().Undefined() : serviceRef.Value(); } void osn::Streaming::SetService(const Napi::CallbackInfo &info, const Napi::Value &value) { - osn::Service *service = Napi::ObjectWrap::Unwrap(value.ToObject()); + auto conn = GetConnection(info); + if (!conn) + return; - if (!service) { - Napi::TypeError::New(info.Env(), "Invalid service argument").ThrowAsJavaScriptException(); + if (value.IsNull() || value.IsUndefined()) { + if (!serviceRef.IsEmpty()) + serviceRef.Reset(); + conn->call(className, "SetService", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); return; } - auto conn = GetConnection(info); - if (!conn) + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::Service::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a Service").ThrowAsJavaScriptException(); + + osn::Service *service = Napi::ObjectWrap::Unwrap(obj); + if (!service) { + Napi::TypeError::New(info.Env(), "Invalid service argument").ThrowAsJavaScriptException(); return; + } conn->call(className, "SetService", {ipc::value(this->uid), ipc::value(service->uid)}); + + if (!serviceRef.IsEmpty()) + serviceRef.Reset(); + + serviceRef = Napi::Persistent(obj); } Napi::Value osn::Streaming::GetCanvas(const Napi::CallbackInfo &info) @@ -88,24 +113,29 @@ void osn::Streaming::SetCanvas(const Napi::CallbackInfo &info, const Napi::Value conn->call(className, "SetVideoCanvas", {ipc::value(this->uid), ipc::value(canvas->canvasId)}); } + Napi::Value osn::Streaming::GetVideoEncoder(const Napi::CallbackInfo &info) +{ + return videoEncoderRef.IsEmpty() ? info.Env().Undefined() : videoEncoderRef.Value(); +} + +void osn::Streaming::SetVideoEncoder(const Napi::CallbackInfo &info, const Napi::Value &value) { auto conn = GetConnection(info); if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetVideoEncoder", {ipc::value(this->uid)}); + return; - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); + if (value.IsNull() || value.IsUndefined()) { + if (!videoEncoderRef.IsEmpty()) + videoEncoderRef.Reset(); + conn->call(className, "SetVideoEncoder", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); + return; + } - auto instance = osn::VideoEncoder::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::VideoEncoder::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a VideoEncoder").ThrowAsJavaScriptException(); - return instance; -} - -void osn::Streaming::SetVideoEncoder(const Napi::CallbackInfo &info, const Napi::Value &value) -{ osn::VideoEncoder *encoder = Napi::ObjectWrap::Unwrap(value.ToObject()); if (!encoder) { @@ -113,11 +143,12 @@ void osn::Streaming::SetVideoEncoder(const Napi::CallbackInfo &info, const Napi: return; } - auto conn = GetConnection(info); - if (!conn) - return; - conn->call(className, "SetVideoEncoder", {ipc::value(this->uid), ipc::value(encoder->uid)}); + + if (!videoEncoderRef.IsEmpty()) + videoEncoderRef.Reset(); + + videoEncoderRef = Napi::Persistent(obj); } Napi::Value osn::Streaming::GetEnforceServiceBirate(const Napi::CallbackInfo &info) @@ -168,98 +199,110 @@ void osn::Streaming::SetEnableTwitchVOD(const Napi::CallbackInfo &info, const Na Napi::Value osn::Streaming::GetDelay(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetDelay", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - auto instance = osn::Delay::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - - return instance; + return delayRef.IsEmpty() ? info.Env().Undefined() : delayRef.Value(); } void osn::Streaming::SetDelay(const Napi::CallbackInfo &info, const Napi::Value &value) { - osn::Delay *delay = Napi::ObjectWrap::Unwrap(value.ToObject()); + auto conn = GetConnection(info); + if (!conn) + return; - if (!delay) { - Napi::TypeError::New(info.Env(), "Invalid delay argument").ThrowAsJavaScriptException(); + if (value.IsNull() || value.IsUndefined()) { + if (!delayRef.IsEmpty()) + delayRef.Reset(); + conn->call(className, "SetDelay", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); return; } - auto conn = GetConnection(info); - if (!conn) + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::Delay::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a Delay").ThrowAsJavaScriptException(); + + osn::Delay *delay = Napi::ObjectWrap::Unwrap(obj); + if (!delay) { + Napi::TypeError::New(info.Env(), "Invalid delay argument").ThrowAsJavaScriptException(); return; + } conn->call(className, "SetDelay", {ipc::value(this->uid), ipc::value(delay->uid)}); + + if (!delayRef.IsEmpty()) + delayRef.Reset(); + + delayRef = Napi::Persistent(obj); } Napi::Value osn::Streaming::GetReconnect(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetReconnect", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - auto instance = osn::Reconnect::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - - return instance; + return reconnectRef.IsEmpty() ? info.Env().Undefined() : reconnectRef.Value(); } void osn::Streaming::SetReconnect(const Napi::CallbackInfo &info, const Napi::Value &value) { - osn::Reconnect *reconnect = Napi::ObjectWrap::Unwrap(value.ToObject()); + auto conn = GetConnection(info); + if (!conn) + return; - if (!reconnect) { - Napi::TypeError::New(info.Env(), "Invalid reconnect argument").ThrowAsJavaScriptException(); + if (value.IsNull() || value.IsUndefined()) { + if (!reconnectRef.IsEmpty()) + reconnectRef.Reset(); + conn->call(className, "SetReconnect", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); return; } - auto conn = GetConnection(info); - if (!conn) + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::Reconnect::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a Reconnect").ThrowAsJavaScriptException(); + + osn::Reconnect *reconnect = Napi::ObjectWrap::Unwrap(obj); + if (!reconnect) { + Napi::TypeError::New(info.Env(), "Invalid reconnect argument").ThrowAsJavaScriptException(); return; + } conn->call(className, "SetReconnect", {ipc::value(this->uid), ipc::value(reconnect->uid)}); + + if (!reconnectRef.IsEmpty()) + reconnectRef.Reset(); + + reconnectRef = Napi::Persistent(obj); } Napi::Value osn::Streaming::GetNetwork(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper(className, "GetNetwork", {ipc::value(this->uid)}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); - - auto instance = osn::Network::constructor.New({Napi::Number::New(info.Env(), static_cast(response[1].value_union.ui64))}); - - return instance; + return networkRef.IsEmpty() ? info.Env().Undefined() : networkRef.Value(); } void osn::Streaming::SetNetwork(const Napi::CallbackInfo &info, const Napi::Value &value) { - osn::Network *network = Napi::ObjectWrap::Unwrap(value.ToObject()); + auto conn = GetConnection(info); + if (!conn) + return; - if (!network) { - Napi::TypeError::New(info.Env(), "Invalid network argument").ThrowAsJavaScriptException(); + if (value.IsNull() || value.IsUndefined()) { + if (!networkRef.IsEmpty()) + networkRef.Reset(); + conn->call(className, "SetNetwork", {ipc::value(this->uid), ipc::value(UINT64_MAX)}); return; } - auto conn = GetConnection(info); - if (!conn) + Napi::Object obj = value.As(); + if (!obj.InstanceOf(osn::Network::constructor.Value())) + Napi::TypeError::New(info.Env(), "Object is not a Network").ThrowAsJavaScriptException(); + + osn::Network *network = Napi::ObjectWrap::Unwrap(obj); + if (!network) { + Napi::TypeError::New(info.Env(), "Invalid network argument").ThrowAsJavaScriptException(); return; + } conn->call(className, "SetNetwork", {ipc::value(this->uid), ipc::value(network->uid)}); + + if (!networkRef.IsEmpty()) + networkRef.Reset(); + + networkRef = Napi::Persistent(obj); } Napi::Value osn::Streaming::GetSignalHandler(const Napi::CallbackInfo &info) @@ -290,7 +333,8 @@ void osn::Streaming::Start(const Napi::CallbackInfo &info) startWorker(info.Env(), this->cb.Value(), className, this->uid); - conn->call(className, "Start", {ipc::value(this->uid)}); + std::vector response = conn->call_synchronous_helper(className, "Start", {ipc::value(this->uid)}); + ValidateResponse(info, response); } void osn::Streaming::Stop(const Napi::CallbackInfo &info) diff --git a/obs-studio-client/source/streaming.hpp b/obs-studio-client/source/streaming.hpp index bb5e6cee6..3e1fbe9be 100644 --- a/obs-studio-client/source/streaming.hpp +++ b/obs-studio-client/source/streaming.hpp @@ -30,6 +30,14 @@ class Streaming : public WorkerSignals { Napi::Function signalHandler; std::string className; + Napi::Reference videoEncoderRef; + Napi::Reference audioEncoderRef; + Napi::Reference serviceRef; + Napi::Reference delayRef; + Napi::Reference reconnectRef; + Napi::Reference networkRef; + void ReleaseObjects(); + Napi::Value GetService(const Napi::CallbackInfo &info); void SetService(const Napi::CallbackInfo &info, const Napi::Value &value); Napi::Value GetCanvas(const Napi::CallbackInfo &info); diff --git a/obs-studio-client/source/utility.hpp b/obs-studio-client/source/utility.hpp index d2df25d54..6f0040388 100644 --- a/obs-studio-client/source/utility.hpp +++ b/obs-studio-client/source/utility.hpp @@ -76,15 +76,15 @@ #define dstr(s) #s #define vstr(s) dstr(s) -static bool ValidateResponse(const Napi::CallbackInfo &info, std::vector &response) +static bool ValidateResponse(const Napi::Env &env, std::vector &response) { if (response.size() == 0) { - Napi::Error::New(info.Env(), "Failed to make IPC call, verify IPC status.").ThrowAsJavaScriptException(); + Napi::Error::New(env, "Failed to make IPC call, verify IPC status.").ThrowAsJavaScriptException(); return false; } if ((response.size() == 1) && (response[0].type == ipc::type::Null)) { - Napi::Error::New(info.Env(), response[0].value_str).ThrowAsJavaScriptException(); + Napi::Error::New(env, response[0].value_str).ThrowAsJavaScriptException(); return false; } @@ -94,40 +94,50 @@ static bool ValidateResponse(const Napi::CallbackInfo &info, std::vector GetConnection(const Napi::CallbackInfo &info) +static bool ValidateResponse(const Napi::CallbackInfo &info, std::vector &response) +{ + return ValidateResponse(info.Env(), response); +} + +static FORCE_INLINE std::shared_ptr GetConnection(const Napi::Env &env) { auto conn = Controller::GetInstance().GetConnection(); if (!conn) { - std::cerr << "Failed to obtain IPC connection." << std::endl; + Napi::Error::New(env, "Failed to obtain IPC connection.").ThrowAsJavaScriptException(); exit(1); } return conn; } +static FORCE_INLINE std::shared_ptr GetConnection(const Napi::CallbackInfo &info) +{ + return GetConnection(info.Env()); +} + namespace utility { template inline std::string TypeOf(T v) { diff --git a/obs-studio-client/source/video-encoder.cpp b/obs-studio-client/source/video-encoder.cpp index 37a591fff..8e4cd187d 100644 --- a/obs-studio-client/source/video-encoder.cpp +++ b/obs-studio-client/source/video-encoder.cpp @@ -53,7 +53,9 @@ osn::VideoEncoder::VideoEncoder(const Napi::CallbackInfo &info) : Napi::ObjectWr Napi::Env env = info.Env(); Napi::HandleScope scope(env); size_t length = info.Length(); - this->uid = 0; + this->uid = UINT64_MAX; + this->encoderInitialized = false; + this->connectionEpoch = 0; if (length <= 0 || !info[0].IsNumber()) { Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); @@ -61,6 +63,8 @@ osn::VideoEncoder::VideoEncoder(const Napi::CallbackInfo &info) : Napi::ObjectWr } this->uid = (uint64_t)info[0].ToNumber().Int64Value(); + this->encoderInitialized = true; + this->connectionEpoch = Controller::GetInstance().GetConnectionEpoch(); } Napi::Value osn::VideoEncoder::Create(const Napi::CallbackInfo &info) @@ -92,6 +96,30 @@ Napi::Value osn::VideoEncoder::Create(const Napi::CallbackInfo &info) return instance; } +void osn::VideoEncoder::Finalize(Napi::Env env) +{ + if (!this->encoderInitialized) + return; + + // If OBS was restarted/disconnected, skip IPC cleanup for this stale wrapper. + if (this->connectionEpoch != Controller::GetInstance().GetConnectionEpoch()) { + this->encoderInitialized = false; + this->uid = UINT64_MAX; + return; + } + + auto conn = Controller::GetInstance().GetConnection(); + if (!conn) { + this->encoderInitialized = false; + this->uid = UINT64_MAX; + return; + } + + conn->call_synchronous_helper("VideoEncoder", "Finalize", {ipc::value(this->uid)}); + this->encoderInitialized = false; + this->uid = UINT64_MAX; +} + Napi::Value osn::VideoEncoder::GetTypes(const Napi::CallbackInfo &info) { auto conn = GetConnection(info); @@ -193,11 +221,18 @@ Napi::Value osn::VideoEncoder::GetLastError(const Napi::CallbackInfo &info) void osn::VideoEncoder::Release(const Napi::CallbackInfo &info) { + if (!this->encoderInitialized) + return; + auto conn = GetConnection(info); if (!conn) return; - conn->call("VideoEncoder", "Release", {}); + this->encoderInitialized = false; + std::vector response = conn->call_synchronous_helper("VideoEncoder", "Release", {ipc::value(this->uid)}); + this->uid = UINT64_MAX; + if (!ValidateResponse(info, response)) + return; } void osn::VideoEncoder::Update(const Napi::CallbackInfo &info) diff --git a/obs-studio-client/source/video-encoder.hpp b/obs-studio-client/source/video-encoder.hpp index 5a3f699ee..a057794b6 100644 --- a/obs-studio-client/source/video-encoder.hpp +++ b/obs-studio-client/source/video-encoder.hpp @@ -24,6 +24,8 @@ namespace osn { class VideoEncoder : public Napi::ObjectWrap { public: uint64_t uid; + bool encoderInitialized; + uint64_t connectionEpoch; public: static Napi::FunctionReference constructor; @@ -31,6 +33,7 @@ class VideoEncoder : public Napi::ObjectWrap { VideoEncoder(const Napi::CallbackInfo &info); static Napi::Value Create(const Napi::CallbackInfo &info); + void Finalize(Napi::Env env); static Napi::Value GetTypes(const Napi::CallbackInfo &info); Napi::Value GetName(const Napi::CallbackInfo &info); @@ -45,4 +48,4 @@ class VideoEncoder : public Napi::ObjectWrap { Napi::Value GetProperties(const Napi::CallbackInfo &info); Napi::Value GetSettings(const Napi::CallbackInfo &info); }; -} \ No newline at end of file +} diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index 3f91439fc..9399093c8 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -292,9 +292,11 @@ SET(osn-server_SOURCES "${PROJECT_SOURCE_DIR}/source/osn-multitrack-video-data-model.hpp" "${PROJECT_SOURCE_DIR}/source/osn-multitrack-video-output.cpp" "${PROJECT_SOURCE_DIR}/source/osn-multitrack-video-output.hpp" + "${PROJECT_SOURCE_DIR}/source/osn-multitrack-video-system-info.hpp" + "${PROJECT_SOURCE_DIR}/source/osn-multitrack-video.cpp" + "${PROJECT_SOURCE_DIR}/source/osn-multitrack-video.hpp" "${PROJECT_SOURCE_DIR}/source/osn-audio-bitrate.cpp" "${PROJECT_SOURCE_DIR}/source/osn-audio-bitrate.hpp" - "${PROJECT_SOURCE_DIR}/source/osn-multitrack-video-system-info.hpp" "${PROJECT_SOURCE_DIR}/source/osn-output.cpp" "${PROJECT_SOURCE_DIR}/source/osn-output.hpp" "${PROJECT_SOURCE_DIR}/source/osn-properties.cpp" @@ -321,6 +323,10 @@ SET(osn-server_SOURCES "${PROJECT_SOURCE_DIR}/source/osn-simple-streaming.hpp" "${PROJECT_SOURCE_DIR}/source/osn-advanced-streaming.cpp" "${PROJECT_SOURCE_DIR}/source/osn-advanced-streaming.hpp" + "${PROJECT_SOURCE_DIR}/source/osn-enhanced-broadcasting-advanced-streaming.cpp" + "${PROJECT_SOURCE_DIR}/source/osn-enhanced-broadcasting-advanced-streaming.hpp" + "${PROJECT_SOURCE_DIR}/source/osn-enhanced-broadcasting-simple-streaming.cpp" + "${PROJECT_SOURCE_DIR}/source/osn-enhanced-broadcasting-simple-streaming.hpp" "${PROJECT_SOURCE_DIR}/source/osn-delay.cpp" "${PROJECT_SOURCE_DIR}/source/osn-delay.hpp" "${PROJECT_SOURCE_DIR}/source/osn-reconnect.cpp" @@ -345,8 +351,6 @@ SET(osn-server_SOURCES "${PROJECT_SOURCE_DIR}/source/osn-file-output.hpp" "${PROJECT_SOURCE_DIR}/source/osn-advanced-replay-buffer.cpp" "${PROJECT_SOURCE_DIR}/source/osn-advanced-replay-buffer.hpp" - "${PROJECT_SOURCE_DIR}/source/osn-output-signals.cpp" - "${PROJECT_SOURCE_DIR}/source/osn-output-signals.hpp" ###### utlity graphics ###### "${PROJECT_SOURCE_DIR}/source/gs-limits.h" diff --git a/obs-studio-server/source/main.cpp b/obs-studio-server/source/main.cpp index 6e7f7b71b..52d02cbaf 100644 --- a/obs-studio-server/source/main.cpp +++ b/obs-studio-server/source/main.cpp @@ -61,6 +61,8 @@ #include "osn-simple-replay-buffer.hpp" #include "osn-advanced-replay-buffer.hpp" #include "osn-file-output.hpp" +#include "osn-enhanced-broadcasting-simple-streaming.hpp" +#include "osn-enhanced-broadcasting-advanced-streaming.hpp" #include "util-crashmanager.h" #include "shared.hpp" @@ -273,6 +275,8 @@ int main(int argc, char *argv[]) osn::ISimpleReplayBuffer::Register(myServer); osn::IAdvancedReplayBuffer::Register(myServer); osn::IFileOutput::Register(myServer); + osn::IEnhancedBroadcastingSimpleStreaming::Register(myServer); + osn::IEnhancedBroadcastingAdvancedStreaming::Register(myServer); OBS_API::CreateCrashHandlerExitPipe(); diff --git a/obs-studio-server/source/nodeobs_service.cpp b/obs-studio-server/source/nodeobs_service.cpp index cbab155e0..e1fa6779b 100644 --- a/obs-studio-server/source/nodeobs_service.cpp +++ b/obs-studio-server/source/nodeobs_service.cpp @@ -28,9 +28,7 @@ #include #include "osn-vcam.hpp" -#include "osn-multitrack-video-configuration.hpp" -#include "osn-audio-bitrate.hpp" -#include "osn-multitrack-video-output.hpp" +#include "osn-multitrack-video.hpp" #ifdef __APPLE__ #include @@ -92,13 +90,7 @@ std::mutex signalMutex; std::queue outputSignal; std::thread releaseWorker; -struct EnhancedBroadcastOutputObjects { - OBSOutputAutoRelease obsOutput; - std::shared_ptr videoEncoderGroup; - std::vector audioEncoders; - OBSServiceAutoRelease multitrackVideoService; -}; -static std::optional enhancedBroadcastContext; +static std::optional enhancedBroadcastContext; OBS_service::OBS_service() {} OBS_service::~OBS_service() {} @@ -1266,18 +1258,6 @@ const char *OBS_service::getStreamOutputType(const obs_service_t *service) return nullptr; } -static int GetAudioBitrate() -{ - const char *audio_encoder_id = config_get_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", "StreamAudioEncoder"); - - const int bitrate = (int)config_get_uint(ConfigManager::getInstance().getBasic(), "SimpleOutput", "ABitrate"); - - if (audio_encoder_id && strcmp(audio_encoder_id, "opus") == 0) - return osn::FindClosestAvailableSimpleOpusBitrate(bitrate); - - return osn::FindClosestAvailableSimpleAACBitrate(bitrate); -} - static inline bool ServiceSupportsVodTrack(const char *service) { static const char *vodTrackServices[] = {"Twitch"}; @@ -1307,11 +1287,6 @@ static bool IsVodTrackEnabled(obs_service_t *service) return vodTrackEnabledAdv && ServiceSupportsVodTrack(name); } -static bool IsMultitrackVideoEnabled() -{ - return config_get_bool(ConfigManager::getInstance().getBasic(), "EnhancedBroadcasting", "EnableMultitrackVideo"); -} - bool OBS_service::startSingleTrackStreaming(StreamServiceId serviceId) { const char *type = getStreamOutputType(services[serviceId]); @@ -1398,13 +1373,23 @@ bool OBS_service::startMultiTrackStreaming(StreamServiceId serviceId, bool dualS auto vod_track_mixer = IsVodTrackEnabled(services[serviceId]) ? std::optional{vodTrackIndex} : std::nullopt; - auto go_live_post = osn::constructGoLivePost(serviceId, dualStreamingMode, key, std::nullopt, std::nullopt, vod_track_mixer.has_value()); + std::vector canvases; + canvases.push_back(videoInfo[serviceId]); + if (dualStreamingMode) { + if (serviceId == StreamServiceId::Main) { + canvases.push_back(videoInfo[StreamServiceId::Second]); + } else if (serviceId == StreamServiceId::Second) { + canvases.push_back(videoInfo[StreamServiceId::Main]); + } + } + + auto go_live_post = osn::constructGoLivePost(canvases, key, std::nullopt, std::nullopt, vod_track_mixer.has_value()); std::optional go_live_config = osn::DownloadGoLiveConfig(auto_config_url, go_live_post); if (!go_live_config.has_value()) { throw std::runtime_error("startStreaming - go live config is empty"); } - const auto audio_bitrate = GetAudioBitrate(); + const auto audio_bitrate = osn::GetMultitrackAudioBitrate(); const auto audio_encoder_id = osn::GetSimpleAACEncoderForBitrate(audio_bitrate); std::vector audio_encoders; @@ -1429,7 +1414,7 @@ bool OBS_service::startMultiTrackStreaming(StreamServiceId serviceId, bool dualS isStreaming[serviceId] = obs_output_start(output); - enhancedBroadcastContext.emplace(EnhancedBroadcastOutputObjects{ + enhancedBroadcastContext.emplace(osn::EnhancedBroadcastOutputObjects{ std::move(output), video_encoder_group, std::move(audio_encoders), @@ -1447,7 +1432,7 @@ bool OBS_service::startStreaming(StreamServiceId serviceId, bool dualStreamingMo } try { - if (isTwitchStream(serviceId) && (IsMultitrackVideoEnabled() || dualStreamingMode)) { + if (isTwitchStream(serviceId) && (osn::IsMultitrackVideoEnabled() || dualStreamingMode)) { return startMultiTrackStreaming(serviceId, dualStreamingMode); } @@ -1634,7 +1619,7 @@ bool OBS_service::updateRecordingEncoders(bool isSimpleMode, StreamServiceId ser bool simpleUsesStream = false; bool advancedUsesStream = false; - if (!IsMultitrackVideoEnabled()) { + if (!osn::IsMultitrackVideoEnabled()) { simpleUsesStream = isSimpleMode && simpleQuality.compare("Stream") == 0; advancedUsesStream = !isSimpleMode && (advancedQuality.compare("") == 0 || advancedQuality.compare("none") == 0); } @@ -1766,7 +1751,7 @@ void OBS_service::stopStreaming(bool forceStop, StreamServiceId serviceId) obs_output_stop(streamingOutput[serviceId]); /* Unregister the BPM (Broadcast Performance Metrics) callback and destroy the allocated metrics data. */ - if (isTwitchStream(serviceId) && IsMultitrackVideoEnabled()) { + if (isTwitchStream(serviceId) && osn::IsMultitrackVideoEnabled()) { obs_output_remove_packet_callback(streamingOutput[serviceId], bpm_inject, NULL); bpm_destroy(streamingOutput[serviceId]); } diff --git a/obs-studio-server/source/osn-advanced-recording.cpp b/obs-studio-server/source/osn-advanced-recording.cpp index a4a949849..5b30755f0 100644 --- a/obs-studio-server/source/osn-advanced-recording.cpp +++ b/obs-studio-server/source/osn-advanced-recording.cpp @@ -186,9 +186,9 @@ bool osn::AdvancedRecording::UpdateEncoders() return false; if (obs_get_multiple_rendering()) { - obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(canvas, OBS_RECORDING_VIDEO_RENDERING)); + obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(this->GetCanvas(), OBS_RECORDING_VIDEO_RENDERING)); } else { - obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(canvas, OBS_MAIN_VIDEO_RENDERING)); + obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(this->GetCanvas(), OBS_MAIN_VIDEO_RENDERING)); } return true; @@ -201,10 +201,10 @@ void osn::IAdvancedRecording::Start(void *data, const int64_t id, const std::vec PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple recording reference is not valid."); } - if (!recording->output) - recording->createOutput("ffmpeg_muxer", "recording"); + if (!recording->GetOutput()) + recording->CreateOutput("ffmpeg_muxer", "recording"); - if (!recording->output) { + if (!recording->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Error while creating the recording output."); } @@ -213,9 +213,9 @@ void osn::IAdvancedRecording::Start(void *data, const int64_t id, const std::vec osn::AudioTrack *audioTrack = osn::IAudioTrack::audioTracks[i]; if ((recording->mixer & (1 << i)) != 0 && audioTrack && audioTrack->audioEnc) { obs_encoder_set_audio(audioTrack->audioEnc, obs_get_audio()); - obs_output_set_audio_encoder(recording->output, audioTrack->audioEnc, idx); + obs_output_set_audio_encoder(recording->GetOutput(), audioTrack->audioEnc, idx); - obs_encoder_set_video_mix(audioTrack->audioEnc, obs_video_mix_get(recording->canvas, OBS_RECORDING_VIDEO_RENDERING)); + obs_encoder_set_video_mix(audioTrack->audioEnc, obs_video_mix_get(recording->GetCanvas(), OBS_RECORDING_VIDEO_RENDERING)); idx++; } } @@ -224,7 +224,7 @@ void osn::IAdvancedRecording::Start(void *data, const int64_t id, const std::vec PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid video encoder."); } - obs_output_set_video_encoder(recording->output, recording->videoEncoder); + obs_output_set_video_encoder(recording->GetOutput(), recording->videoEncoder); std::string path = recording->path; @@ -232,8 +232,8 @@ void osn::IAdvancedRecording::Start(void *data, const int64_t id, const std::vec if (lastChar != '/' && lastChar != '\\') path += "/"; - path += GenerateSpecifiedFilename(recording->format, recording->noSpace, recording->fileFormat, recording->canvas->base_width, - recording->canvas->base_height); + path += GenerateSpecifiedFilename(recording->format, recording->noSpace, recording->fileFormat, recording->GetCanvas()->base_width, + recording->GetCanvas()->base_height); if (!recording->overwrite) FindBestFilename(path, recording->noSpace); @@ -241,13 +241,13 @@ void osn::IAdvancedRecording::Start(void *data, const int64_t id, const std::vec obs_data_t *settings = obs_data_create(); obs_data_set_string(settings, "path", path.c_str()); obs_data_set_string(settings, "muxer_settings", recording->muxerSettings.c_str()); - obs_output_update(recording->output, settings); + obs_output_update(recording->GetOutput(), settings); obs_data_release(settings); if (recording->enableFileSplit) recording->ConfigureRecFileSplitting(); - recording->startOutput(); + recording->StartOutput(); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -260,7 +260,7 @@ void osn::IAdvancedRecording::Stop(void *data, const int64_t id, const std::vect PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple recording reference is not valid."); } - obs_output_stop(recording->output); + obs_output_stop(recording->GetOutput()); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -440,15 +440,8 @@ void osn::IAdvancedRecording::SetLegacySettings(void *data, const int64_t id, co void osn::IAdvancedRecording::GetStreaming(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - AdvancedRecording *recording = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!recording) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); - } - - uint64_t uid = osn::IAdvancedStreaming::Manager::GetInstance().find(recording->streaming); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -459,6 +452,13 @@ void osn::IAdvancedRecording::SetStreaming(void *data, const int64_t id, const s PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + recording->streaming = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + AdvancedStreaming *streaming = static_cast(osn::IAdvancedStreaming::Manager::GetInstance().find(args[1].value_union.ui64)); if (!streaming) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); diff --git a/obs-studio-server/source/osn-advanced-replay-buffer.cpp b/obs-studio-server/source/osn-advanced-replay-buffer.cpp index 1d8a7ee5d..bb07d3861 100644 --- a/obs-studio-server/source/osn-advanced-replay-buffer.cpp +++ b/obs-studio-server/source/osn-advanced-replay-buffer.cpp @@ -123,15 +123,15 @@ void osn::IAdvancedReplayBuffer::Start(void *data, const int64_t id, const std:: PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple replay buffer reference is not valid."); } - if (!replayBuffer->output) - replayBuffer->createOutput("replay_buffer", "replay-buffer"); + if (!replayBuffer->GetOutput()) + replayBuffer->CreateOutput("replay_buffer", "replay-buffer"); int idx = 0; for (int i = 0; i < MAX_AUDIO_MIXES; i++) { osn::AudioTrack *audioTrack = osn::IAudioTrack::audioTracks[i]; if ((replayBuffer->mixer & (1 << i)) != 0 && audioTrack && audioTrack->audioEnc) { obs_encoder_set_audio(audioTrack->audioEnc, obs_get_audio()); - obs_output_set_audio_encoder(replayBuffer->output, audioTrack->audioEnc, idx); + obs_output_set_audio_encoder(replayBuffer->GetOutput(), audioTrack->audioEnc, idx); idx++; } } @@ -154,7 +154,7 @@ void osn::IAdvancedReplayBuffer::Start(void *data, const int64_t id, const std:: PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid video encoder."); } - obs_output_set_video_encoder(replayBuffer->output, videoEncoder); + obs_output_set_video_encoder(replayBuffer->GetOutput(), videoEncoder); if (!replayBuffer->path.size()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid recording path."); @@ -185,10 +185,10 @@ void osn::IAdvancedReplayBuffer::Start(void *data, const int64_t id, const std:: obs_data_set_bool(settings, "allow_spaces", !replayBuffer->noSpace); obs_data_set_int(settings, "max_time_sec", replayBuffer->duration); obs_data_set_int(settings, "max_size_mb", rbSize); - obs_output_update(replayBuffer->output, settings); + obs_output_update(replayBuffer->GetOutput(), settings); obs_data_release(settings); - replayBuffer->startOutput(); + replayBuffer->StartOutput(); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -200,11 +200,11 @@ void osn::IAdvancedReplayBuffer::Stop(void *data, const int64_t id, const std::v if (!replayBuffer) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple replay buffer reference is not valid."); } - if (!replayBuffer->output) { + if (!replayBuffer->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid replay buffer output."); } - obs_output_stop(replayBuffer->output); + obs_output_stop(replayBuffer->GetOutput()); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -267,15 +267,8 @@ void osn::IAdvancedReplayBuffer::SetLegacySettings(void *data, const int64_t id, void osn::IAdvancedReplayBuffer::GetStreaming(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - AdvancedReplayBuffer *replayBuffer = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!replayBuffer) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Advanced replay buffer reference is not valid."); - } - - uint64_t uid = osn::IAdvancedStreaming::Manager::GetInstance().find(replayBuffer->streaming); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -286,6 +279,13 @@ void osn::IAdvancedReplayBuffer::SetStreaming(void *data, const int64_t id, cons PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Advanced replay buffer reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + replayBuffer->streaming = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + AdvancedStreaming *streaming = static_cast(osn::IAdvancedStreaming::Manager::GetInstance().find(args[1].value_union.ui64)); if (!streaming) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); @@ -299,15 +299,8 @@ void osn::IAdvancedReplayBuffer::SetStreaming(void *data, const int64_t id, cons void osn::IAdvancedReplayBuffer::GetRecording(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - AdvancedReplayBuffer *replayBuffer = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!replayBuffer) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Advanced replay buffer reference is not valid."); - } - - uint64_t uid = osn::IAdvancedRecording::Manager::GetInstance().find(replayBuffer->recording); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -318,6 +311,13 @@ void osn::IAdvancedReplayBuffer::SetRecording(void *data, const int64_t id, cons PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Advanced replay buffer reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + replayBuffer->streaming = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + AdvancedRecording *recording = static_cast(osn::IAdvancedRecording::Manager::GetInstance().find(args[1].value_union.ui64)); if (!recording) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); diff --git a/obs-studio-server/source/osn-advanced-streaming.cpp b/obs-studio-server/source/osn-advanced-streaming.cpp index 265ac9d49..e01fc209d 100644 --- a/obs-studio-server/source/osn-advanced-streaming.cpp +++ b/obs-studio-server/source/osn-advanced-streaming.cpp @@ -241,8 +241,8 @@ static bool setAudioEncoder(osn::AdvancedStreaming *streaming) return false; obs_encoder_set_audio(audioTrack->audioEnc, obs_get_audio()); - obs_output_set_audio_encoder(streaming->output, audioTrack->audioEnc, 0); - obs_encoder_set_video_mix(audioTrack->audioEnc, obs_video_mix_get(streaming->canvas, OBS_STREAMING_VIDEO_RENDERING)); + obs_output_set_audio_encoder(streaming->GetOutput(), audioTrack->audioEnc, 0); + obs_encoder_set_video_mix(audioTrack->audioEnc, obs_video_mix_get(streaming->GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); return true; } @@ -289,8 +289,8 @@ static void SetupTwitchSoundtrackAudio(osn::AdvancedStreaming *streaming) obs_encoder_set_audio(streaming->streamArchive, obs_get_audio()); } - obs_output_set_audio_encoder(streaming->output, streaming->streamArchive, kSoundtrackArchiveEncoderIdx); - obs_encoder_set_video_mix(streaming->streamArchive, obs_video_mix_get(streaming->canvas, OBS_STREAMING_VIDEO_RENDERING)); + obs_output_set_audio_encoder(streaming->GetOutput(), streaming->streamArchive, kSoundtrackArchiveEncoderIdx); + obs_encoder_set_video_mix(streaming->streamArchive, obs_video_mix_get(streaming->GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); osn::AudioTrack *audioTrack = osn::IAudioTrack::audioTracks[streaming->twitchTrack]; if (!audioTrack) @@ -356,9 +356,9 @@ void osn::AdvancedStreaming::UpdateEncoders() obs_data_release(settings); if (obs_get_multiple_rendering()) { - obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(canvas, OBS_STREAMING_VIDEO_RENDERING)); + obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(this->GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); } else { - obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(canvas, OBS_MAIN_VIDEO_RENDERING)); + obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(this->GetCanvas(), OBS_MAIN_VIDEO_RENDERING)); } } @@ -366,34 +366,34 @@ void osn::IAdvancedStreaming::Start(void *data, const int64_t id, const std::vec { AdvancedStreaming *streaming = static_cast(osn::IAdvancedStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); if (!streaming) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple streaming reference is not valid."); + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Advanced streaming reference is not valid."); + } + + if (!streaming->service) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid service."); } if (!streaming->videoEncoder) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid video encoder."); } - streaming->UpdateEncoders(); - - if (!streaming->service) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid service."); + if (!streaming->GetCanvas()) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid main canvas."); } + streaming->UpdateEncoders(); + const char *type = OBS_service::getStreamOutputType(streaming->service); if (!type) type = "rtmp_output"; - if (!streaming->output || strcmp(obs_output_get_id(streaming->output), type) != 0) - streaming->createOutput(type, "stream"); + if (!streaming->GetOutput() || strcmp(obs_output_get_id(streaming->GetOutput()), type) != 0) + streaming->CreateOutput(type, "stream"); - if (!streaming->output) { + if (!streaming->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Error while creating the streaming output."); } - if (!streaming->videoEncoder) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Error while creating the video encoder."); - } - if (!setAudioEncoder(streaming)) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Error while creating the audio encoder."); } @@ -401,7 +401,7 @@ void osn::IAdvancedStreaming::Start(void *data, const int64_t id, const std::vec if (streaming->rescaling) obs_encoder_set_scaled_size(streaming->videoEncoder, streaming->outputWidth, streaming->outputHeight); - obs_output_set_video_encoder(streaming->output, streaming->videoEncoder); + obs_output_set_video_encoder(streaming->GetOutput(), streaming->videoEncoder); if (streaming->enableTwitchVOD) { streaming->twitchVODSupported = streaming->isTwitchVODSupported(); @@ -409,19 +409,19 @@ void osn::IAdvancedStreaming::Start(void *data, const int64_t id, const std::vec SetupTwitchSoundtrackAudio(streaming); } - obs_output_set_service(streaming->output, streaming->service); + obs_output_set_service(streaming->GetOutput(), streaming->service); if (!streaming->delay) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid delay."); } - obs_output_set_delay(streaming->output, streaming->delay->enabled ? uint32_t(streaming->delay->delaySec) : 0, + obs_output_set_delay(streaming->GetOutput(), streaming->delay->enabled ? uint32_t(streaming->delay->delaySec) : 0, streaming->delay->preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); if (!streaming->reconnect) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid reconnect."); } uint32_t maxReties = streaming->reconnect->enabled ? streaming->reconnect->maxRetries : 0; - obs_output_set_reconnect_settings(streaming->output, maxReties, streaming->reconnect->retryDelay); + obs_output_set_reconnect_settings(streaming->GetOutput(), maxReties, streaming->reconnect->retryDelay); if (!streaming->network) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid network."); @@ -432,10 +432,10 @@ void osn::IAdvancedStreaming::Start(void *data, const int64_t id, const std::vec obs_data_set_bool(settings, "dyn_bitrate", streaming->network->enableDynamicBitrate); obs_data_set_bool(settings, "new_socket_loop_enabled", streaming->network->enableOptimizations); obs_data_set_bool(settings, "low_latency_mode_enabled", streaming->network->enableLowLatency); - obs_output_update(streaming->output, settings); + obs_output_update(streaming->GetOutput(), settings); obs_data_release(settings); - streaming->startOutput(); + streaming->StartOutput(); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -443,21 +443,22 @@ void osn::IAdvancedStreaming::Start(void *data, const int64_t id, const std::vec void osn::IAdvancedStreaming::Stop(void *data, const int64_t id, const std::vector &args, std::vector &rval) { + blog(LOG_INFO, "IAdvancedStreaming::Stop"); Streaming *streaming = osn::IStreaming::Manager::GetInstance().find(args[0].value_union.ui64); if (!streaming) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple streaming reference is not valid."); + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); } - if (!streaming->output) { + if (!streaming->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid streaming output."); } bool force = args[1].value_union.ui32; if (force) - obs_output_force_stop(streaming->output); + obs_output_force_stop(streaming->GetOutput()); else - obs_output_stop(streaming->output); + obs_output_stop(streaming->GetOutput()); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; diff --git a/obs-studio-server/source/osn-audio-encoder.cpp b/obs-studio-server/source/osn-audio-encoder.cpp index 4ab7f211a..e49d41865 100644 --- a/obs-studio-server/source/osn-audio-encoder.cpp +++ b/obs-studio-server/source/osn-audio-encoder.cpp @@ -25,6 +25,9 @@ void osn::AudioEncoder::Register(ipc::server &srv) std::shared_ptr cls = std::make_shared("AudioEncoder"); cls->register_function( std::make_shared("Create", std::vector{ipc::type::String, ipc::type::String, ipc::type::String}, Create)); + + cls->register_function(std::make_shared("Release", std::vector{ipc::type::UInt64}, Release)); + cls->register_function(std::make_shared("Finalize", std::vector{ipc::type::UInt64}, Finalize)); cls->register_function(std::make_shared("GetName", std::vector{ipc::type::UInt64}, GetName)); cls->register_function(std::make_shared("SetName", std::vector{ipc::type::UInt64, ipc::type::String}, SetName)); cls->register_function(std::make_shared("GetBitrate", std::vector{ipc::type::UInt64}, GetBitrate)); @@ -35,7 +38,10 @@ void osn::AudioEncoder::Register(ipc::server &srv) void osn::AudioEncoder::Create(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - obs_encoder_t *audioEncoder = obs_audio_encoder_create("ffmpeg_aac", "audio", nullptr, 0, nullptr); + std::string encoderId = args[0].value_str; + std::string name = args[1].value_str; + + obs_encoder_t *audioEncoder = obs_audio_encoder_create(encoderId.c_str(), name.c_str(), nullptr, 0, nullptr); if (!audioEncoder) { PRETTY_ERROR_RETURN(ErrorCode::Error, "Failed to create the audio encoder."); @@ -51,6 +57,36 @@ void osn::AudioEncoder::Create(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + obs_encoder_t *encoder = osn::AudioEncoder::Manager::GetInstance().find(args[0].value_union.ui64); + blog(LOG_INFO, "Release encoder %p, %d", encoder, args[0].value_union.ui64); + if (!encoder) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Release. Audio encoder reference is not valid."); + } + + obs_encoder_release(encoder); + osn::AudioEncoder::Manager::GetInstance().free(encoder); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + +void osn::AudioEncoder::Finalize(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + obs_encoder_t *encoder = osn::AudioEncoder::Manager::GetInstance().find(args[0].value_union.ui64); + blog(LOG_INFO, "Finalize encoder %p, %d", encoder, args[0].value_union.ui64); + if (!encoder) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Finalize. Audio encoder reference is not valid."); + } + + obs_encoder_release(encoder); + osn::AudioEncoder::Manager::GetInstance().free(encoder); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + void osn::AudioEncoder::GetName(void *data, const int64_t id, const std::vector &args, std::vector &rval) { obs_encoder_t *audioEncoder = osn::AudioEncoder::Manager::GetInstance().find(args[0].value_union.ui64); diff --git a/obs-studio-server/source/osn-audio-encoder.hpp b/obs-studio-server/source/osn-audio-encoder.hpp index 41bba0c03..a2d838b0a 100644 --- a/obs-studio-server/source/osn-audio-encoder.hpp +++ b/obs-studio-server/source/osn-audio-encoder.hpp @@ -46,5 +46,7 @@ class AudioEncoder { static void SetName(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetBitrate(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void SetBitrate(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Release(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Finalize(void *data, const int64_t id, const std::vector &args, std::vector &rval); }; } diff --git a/obs-studio-server/source/osn-delay.cpp b/obs-studio-server/source/osn-delay.cpp index 259c87e11..8bf3b2361 100644 --- a/obs-studio-server/source/osn-delay.cpp +++ b/obs-studio-server/source/osn-delay.cpp @@ -25,6 +25,7 @@ void osn::IDelay::Register(ipc::server &srv) std::shared_ptr cls = std::make_shared("Delay"); cls->register_function(std::make_shared("Create", std::vector{}, Create)); + cls->register_function(std::make_shared("Destroy", std::vector{}, Destroy)); cls->register_function(std::make_shared("GetEnabled", std::vector{ipc::type::UInt64}, GetEnabled)); cls->register_function(std::make_shared("SetEnabled", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetEnabled)); @@ -49,6 +50,20 @@ void osn::IDelay::Create(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + uint64_t uid = args[0].value_union.ui64; + Delay *delay = osn::IDelay::Manager::GetInstance().find(uid); + if (!delay) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Delay reference is not valid."); + } + + osn::IDelay::Manager::GetInstance().free(uid); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + void osn::IDelay::GetEnabled(void *data, const int64_t id, const std::vector &args, std::vector &rval) { Delay *delay = osn::IDelay::Manager::GetInstance().find(args[0].value_union.ui64); diff --git a/obs-studio-server/source/osn-delay.hpp b/obs-studio-server/source/osn-delay.hpp index a3515e39c..0a38c1730 100644 --- a/obs-studio-server/source/osn-delay.hpp +++ b/obs-studio-server/source/osn-delay.hpp @@ -58,6 +58,7 @@ class IDelay { static void Register(ipc::server &); static void Create(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Destroy(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetEnabled(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void SetEnabled(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetDelaySec(void *data, const int64_t id, const std::vector &args, std::vector &rval); diff --git a/obs-studio-server/source/osn-enhanced-broadcasting-advanced-streaming.cpp b/obs-studio-server/source/osn-enhanced-broadcasting-advanced-streaming.cpp new file mode 100644 index 000000000..ec5e3e202 --- /dev/null +++ b/obs-studio-server/source/osn-enhanced-broadcasting-advanced-streaming.cpp @@ -0,0 +1,177 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#include "osn-enhanced-broadcasting-advanced-streaming.hpp" +#include "osn-video-encoder.hpp" +#include "osn-service.hpp" +#include "osn-error.hpp" +#include "shared.hpp" +#include "nodeobs_audio_encoders.h" +#include "osn-audio-track.hpp" +#include + +void osn::IEnhancedBroadcastingAdvancedStreaming::Register(ipc::server &srv) +{ + std::shared_ptr cls = std::make_shared("EnhancedBroadcastingAdvancedStreaming"); + cls->register_function(std::make_shared("Create", std::vector{}, Create)); + cls->register_function(std::make_shared("Destroy", std::vector{ipc::type::UInt64}, Destroy)); + cls->register_function(std::make_shared("GetService", std::vector{ipc::type::UInt64}, GetService)); + cls->register_function(std::make_shared("SetService", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetService)); + cls->register_function(std::make_shared("GetVideoEncoder", std::vector{ipc::type::UInt64}, GetVideoEncoder)); + cls->register_function( + std::make_shared("SetVideoEncoder", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetVideoEncoder)); + cls->register_function(std::make_shared("GetVideoCanvas", std::vector{ipc::type::UInt64}, GetVideoCanvas)); + cls->register_function(std::make_shared("SetVideoCanvas", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetVideoCanvas)); + cls->register_function(std::make_shared("GetEnforceServiceBirate", std::vector{ipc::type::UInt64}, GetEnforceServiceBirate)); + cls->register_function(std::make_shared("SetEnforceServiceBirate", std::vector{ipc::type::UInt64, ipc::type::UInt32}, + SetEnforceServiceBirate)); + cls->register_function(std::make_shared("GetEnableTwitchVOD", std::vector{ipc::type::UInt64}, GetEnableTwitchVOD)); + cls->register_function( + std::make_shared("SetEnableTwitchVOD", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetEnableTwitchVOD)); + cls->register_function(std::make_shared("GetAudioTrack", std::vector{ipc::type::UInt64}, GetAudioTrack)); + cls->register_function(std::make_shared("SetAudioTrack", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetAudioTrack)); + cls->register_function(std::make_shared("GetTwitchTrack", std::vector{ipc::type::UInt64}, GetTwitchTrack)); + cls->register_function(std::make_shared("SetTwitchTrack", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetTwitchTrack)); + cls->register_function(std::make_shared("GetRescaling", std::vector{ipc::type::UInt64}, GetRescaling)); + cls->register_function(std::make_shared("SetRescaling", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetRescaling)); + cls->register_function(std::make_shared("GetOutputWidth", std::vector{ipc::type::UInt64}, GetOutputWidth)); + cls->register_function(std::make_shared("SetOutputWidth", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetOutputWidth)); + cls->register_function(std::make_shared("GetOutputHeight", std::vector{ipc::type::UInt64}, GetOutputHeight)); + cls->register_function( + std::make_shared("SetOutputHeight", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetOutputHeight)); + cls->register_function(std::make_shared("GetDelay", std::vector{ipc::type::UInt64}, GetDelay)); + cls->register_function(std::make_shared("SetDelay", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetDelay)); + cls->register_function(std::make_shared("GetReconnect", std::vector{ipc::type::UInt64}, GetReconnect)); + cls->register_function(std::make_shared("SetReconnect", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetReconnect)); + cls->register_function(std::make_shared("GetNetwork", std::vector{ipc::type::UInt64}, GetNetwork)); + cls->register_function(std::make_shared("SetNetwork", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetNetwork)); + cls->register_function(std::make_shared("Start", std::vector{ipc::type::UInt64}, Start)); + cls->register_function(std::make_shared("Stop", std::vector{ipc::type::UInt64, ipc::type::UInt32}, Stop)); + cls->register_function(std::make_shared("Query", std::vector{ipc::type::UInt64}, Query)); + cls->register_function(std::make_shared("GetLegacySettings", std::vector{}, GetLegacySettings)); + cls->register_function(std::make_shared("SetLegacySettings", std::vector{ipc::type::UInt64}, SetLegacySettings)); + cls->register_function(std::make_shared("GetDroppedFrames", std::vector{ipc::type::UInt64}, GetDroppedFrames)); + cls->register_function(std::make_shared("GetTotalFrames", std::vector{ipc::type::UInt64}, GetTotalFrames)); + cls->register_function(std::make_shared("GetKBitsPerSec", std::vector{ipc::type::UInt64}, GetKBitsPerSec)); + cls->register_function(std::make_shared("GetDataOutput", std::vector{ipc::type::UInt64}, GetDataOutput)); + + cls->register_function( + std::make_shared("GetAdditionalVideoCanvas", std::vector{ipc::type::UInt64}, GetAdditionalVideoCanvas)); + cls->register_function(std::make_shared("SetAdditionalVideoCanvas", std::vector{ipc::type::UInt64, ipc::type::UInt64}, + SetAdditionalVideoCanvas)); + + srv.register_collection(cls); +} + +void osn::IEnhancedBroadcastingAdvancedStreaming::Create(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + uint64_t uid = osn::IEnhancedBroadcastingAdvancedStreaming::Manager::GetInstance().allocate(new EnhancedBroadcastingAdvancedStreaming()); + if (uid == UINT64_MAX) { + PRETTY_ERROR_RETURN(ErrorCode::CriticalError, "Index list is full."); + } + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + rval.push_back(ipc::value(uid)); + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingAdvancedStreaming::Destroy(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + EnhancedBroadcastingAdvancedStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingAdvancedStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + osn::IEnhancedBroadcastingAdvancedStreaming::Manager::GetInstance().free(streaming); + delete streaming; + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingAdvancedStreaming::Start(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + EnhancedBroadcastingAdvancedStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingAdvancedStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + if (!streaming->service) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid service."); + } + + auto vod_track_mixer = (streaming->twitchVODSupported && streaming->enableTwitchVOD) ? std::optional{streaming->twitchTrack} : std::nullopt; + streaming->StartEnhancedBroadcastingStream(vod_track_mixer); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingAdvancedStreaming::Stop(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + EnhancedBroadcastingAdvancedStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingAdvancedStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + streaming->StopEnhancedBroadcastingStream(); + + IAdvancedStreaming::Stop(data, id, args, rval); + + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingAdvancedStreaming::GetAdditionalVideoCanvas(void *data, const int64_t id, const std::vector &args, + std::vector &rval) +{ + EnhancedBroadcastingAdvancedStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingAdvancedStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + const uint64_t uid = osn::Video::Manager::GetInstance().find(streaming->GetAdditionalVideoCanvas()); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + rval.push_back(ipc::value(uid)); + + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingAdvancedStreaming::SetAdditionalVideoCanvas(void *data, const int64_t id, const std::vector &args, + std::vector &rval) +{ + EnhancedBroadcastingAdvancedStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingAdvancedStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + obs_video_info *canvas = osn::Video::Manager::GetInstance().find(args[1].value_union.ui64); + if (!canvas) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Canvas reference is not valid."); + } + + streaming->SetAdditionalVideoCanvas(canvas); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} diff --git a/obs-studio-server/source/osn-enhanced-broadcasting-advanced-streaming.hpp b/obs-studio-server/source/osn-enhanced-broadcasting-advanced-streaming.hpp new file mode 100644 index 000000000..b5400dc39 --- /dev/null +++ b/obs-studio-server/source/osn-enhanced-broadcasting-advanced-streaming.hpp @@ -0,0 +1,54 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once +#include +#include "utility.hpp" +#include "osn-delay.hpp" +#include "osn-advanced-streaming.hpp" +#include "osn-reconnect.hpp" +#include "osn-network.hpp" +#include "osn-output.hpp" +#include "osn-video-encoder.hpp" +#include "osn-multitrack-video.hpp" +#include "osn-enhanced-broadcasting.hpp" + +#include "nodeobs_configManager.hpp" + +namespace osn { + +class EnhancedBroadcastingAdvancedStreaming : public EnhancedBroadcasting { +public: + EnhancedBroadcastingAdvancedStreaming() : EnhancedBroadcasting() {} + ~EnhancedBroadcastingAdvancedStreaming() {} +}; + +class IEnhancedBroadcastingAdvancedStreaming : public IAdvancedStreaming { +public: + static void Register(ipc::server &); + + static void Create(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Destroy(void *data, const int64_t id, const std::vector &args, std::vector &rval); + + static void Start(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Stop(void *data, const int64_t id, const std::vector &args, std::vector &rval); + + static void GetAdditionalVideoCanvas(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void SetAdditionalVideoCanvas(void *data, const int64_t id, const std::vector &args, std::vector &rval); +}; +} diff --git a/obs-studio-server/source/osn-enhanced-broadcasting-simple-streaming.cpp b/obs-studio-server/source/osn-enhanced-broadcasting-simple-streaming.cpp new file mode 100644 index 000000000..681470add --- /dev/null +++ b/obs-studio-server/source/osn-enhanced-broadcasting-simple-streaming.cpp @@ -0,0 +1,174 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#include "osn-enhanced-broadcasting-simple-streaming.hpp" +#include "osn-video-encoder.hpp" +#include "osn-service.hpp" +#include "osn-error.hpp" +#include "shared.hpp" +#include "nodeobs_audio_encoders.h" +#include "osn-audio-track.hpp" +#include + +void osn::IEnhancedBroadcastingSimpleStreaming::Register(ipc::server &srv) +{ + std::shared_ptr cls = std::make_shared("EnhancedBroadcastingSimpleStreaming"); + cls->register_function(std::make_shared("Create", std::vector{}, Create)); + cls->register_function(std::make_shared("Destroy", std::vector{ipc::type::UInt64}, Destroy)); + cls->register_function(std::make_shared("GetService", std::vector{ipc::type::UInt64}, GetService)); + cls->register_function(std::make_shared("SetService", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetService)); + cls->register_function(std::make_shared("GetVideoEncoder", std::vector{ipc::type::UInt64}, GetVideoEncoder)); + cls->register_function( + std::make_shared("SetVideoEncoder", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetVideoEncoder)); + cls->register_function(std::make_shared("GetVideoCanvas", std::vector{ipc::type::UInt64}, GetVideoCanvas)); + cls->register_function(std::make_shared("SetVideoCanvas", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetVideoCanvas)); + cls->register_function(std::make_shared("GetAudioEncoder", std::vector{ipc::type::UInt64}, GetAudioEncoder)); + cls->register_function( + std::make_shared("SetAudioEncoder", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetAudioEncoder)); + cls->register_function(std::make_shared("GetUseAdvanced", std::vector{ipc::type::UInt64}, GetUseAdvanced)); + cls->register_function(std::make_shared("SetUseAdvanced", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetUseAdvanced)); + cls->register_function(std::make_shared("GetCustomEncSettings", std::vector{ipc::type::UInt64}, GetCustomEncSettings)); + cls->register_function( + std::make_shared("SetCustomEncSettings", std::vector{ipc::type::UInt64, ipc::type::String}, SetCustomEncSettings)); + cls->register_function(std::make_shared("GetEnforceServiceBirate", std::vector{ipc::type::UInt64}, GetEnforceServiceBirate)); + cls->register_function(std::make_shared("SetEnforceServiceBirate", std::vector{ipc::type::UInt64, ipc::type::UInt32}, + SetEnforceServiceBirate)); + cls->register_function(std::make_shared("GetEnableTwitchVOD", std::vector{ipc::type::UInt64}, GetEnableTwitchVOD)); + cls->register_function( + std::make_shared("SetEnableTwitchVOD", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetEnableTwitchVOD)); + cls->register_function(std::make_shared("GetDelay", std::vector{ipc::type::UInt64}, GetDelay)); + cls->register_function(std::make_shared("SetDelay", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetDelay)); + cls->register_function(std::make_shared("GetReconnect", std::vector{ipc::type::UInt64}, GetReconnect)); + cls->register_function(std::make_shared("SetReconnect", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetReconnect)); + cls->register_function(std::make_shared("GetNetwork", std::vector{ipc::type::UInt64}, GetNetwork)); + cls->register_function(std::make_shared("SetNetwork", std::vector{ipc::type::UInt64, ipc::type::UInt64}, SetNetwork)); + cls->register_function(std::make_shared("Start", std::vector{ipc::type::UInt64}, Start)); + cls->register_function(std::make_shared("Stop", std::vector{ipc::type::UInt64, ipc::type::UInt32}, Stop)); + cls->register_function(std::make_shared("Query", std::vector{ipc::type::UInt64}, Query)); + cls->register_function(std::make_shared("GetLegacySettings", std::vector{}, GetLegacySettings)); + cls->register_function(std::make_shared("SetLegacySettings", std::vector{ipc::type::UInt64}, SetLegacySettings)); + cls->register_function(std::make_shared("GetDroppedFrames", std::vector{ipc::type::UInt64}, GetDroppedFrames)); + cls->register_function(std::make_shared("GetTotalFrames", std::vector{ipc::type::UInt64}, GetTotalFrames)); + cls->register_function(std::make_shared("GetKBitsPerSec", std::vector{ipc::type::UInt64}, GetKBitsPerSec)); + cls->register_function(std::make_shared("GetDataOutput", std::vector{ipc::type::UInt64}, GetDataOutput)); + + cls->register_function( + std::make_shared("GetAdditionalVideoCanvas", std::vector{ipc::type::UInt64}, GetAdditionalVideoCanvas)); + cls->register_function(std::make_shared("SetAdditionalVideoCanvas", std::vector{ipc::type::UInt64, ipc::type::UInt64}, + SetAdditionalVideoCanvas)); + + srv.register_collection(cls); +} + +void osn::IEnhancedBroadcastingSimpleStreaming::Create(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + uint64_t uid = osn::IEnhancedBroadcastingSimpleStreaming::Manager::GetInstance().allocate(new EnhancedBroadcastingSimpleStreaming()); + if (uid == UINT64_MAX) { + PRETTY_ERROR_RETURN(ErrorCode::CriticalError, "Index list is full."); + } + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + rval.push_back(ipc::value(uid)); + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingSimpleStreaming::Destroy(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + EnhancedBroadcastingSimpleStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingSimpleStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + osn::IEnhancedBroadcastingSimpleStreaming::Manager::GetInstance().free(streaming); + delete streaming; + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingSimpleStreaming::Start(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + EnhancedBroadcastingSimpleStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingSimpleStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + if (!streaming->service) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid service."); + } + + // Note: unlike advanced mode, there is no Twitch VOD track for simple mode + streaming->StartEnhancedBroadcastingStream(); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingSimpleStreaming::Stop(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + EnhancedBroadcastingSimpleStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingSimpleStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + streaming->StopEnhancedBroadcastingStream(); + + ISimpleStreaming::Stop(data, id, args, rval); + + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingSimpleStreaming::GetAdditionalVideoCanvas(void *data, const int64_t id, const std::vector &args, + std::vector &rval) +{ + EnhancedBroadcastingSimpleStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingSimpleStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + const uint64_t uid = osn::Video::Manager::GetInstance().find(streaming->GetAdditionalVideoCanvas()); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + rval.push_back(ipc::value(uid)); + + AUTO_DEBUG; +} + +void osn::IEnhancedBroadcastingSimpleStreaming::SetAdditionalVideoCanvas(void *data, const int64_t id, const std::vector &args, + std::vector &rval) +{ + EnhancedBroadcastingSimpleStreaming *streaming = static_cast( + osn::IEnhancedBroadcastingSimpleStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); + if (!streaming) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); + } + + obs_video_info *canvas = osn::Video::Manager::GetInstance().find(args[1].value_union.ui64); + if (!canvas) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Canvas reference is not valid."); + } + + streaming->SetAdditionalVideoCanvas(canvas); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} diff --git a/obs-studio-server/source/osn-enhanced-broadcasting-simple-streaming.hpp b/obs-studio-server/source/osn-enhanced-broadcasting-simple-streaming.hpp new file mode 100644 index 000000000..4320d366a --- /dev/null +++ b/obs-studio-server/source/osn-enhanced-broadcasting-simple-streaming.hpp @@ -0,0 +1,54 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once +#include +#include "utility.hpp" +#include "osn-delay.hpp" +#include "osn-simple-streaming.hpp" +#include "osn-reconnect.hpp" +#include "osn-network.hpp" +#include "osn-output.hpp" +#include "osn-video-encoder.hpp" +#include "osn-multitrack-video.hpp" +#include "osn-enhanced-broadcasting.hpp" + +#include "nodeobs_configManager.hpp" + +namespace osn { + +class EnhancedBroadcastingSimpleStreaming : public EnhancedBroadcasting { +public: + EnhancedBroadcastingSimpleStreaming() : EnhancedBroadcasting() {} + ~EnhancedBroadcastingSimpleStreaming() {} +}; + +class IEnhancedBroadcastingSimpleStreaming : public ISimpleStreaming { +public: + static void Register(ipc::server &); + + static void Create(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Destroy(void *data, const int64_t id, const std::vector &args, std::vector &rval); + + static void Start(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Stop(void *data, const int64_t id, const std::vector &args, std::vector &rval); + + static void GetAdditionalVideoCanvas(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void SetAdditionalVideoCanvas(void *data, const int64_t id, const std::vector &args, std::vector &rval); +}; +} diff --git a/obs-studio-server/source/osn-enhanced-broadcasting.hpp b/obs-studio-server/source/osn-enhanced-broadcasting.hpp new file mode 100644 index 000000000..2cb43493b --- /dev/null +++ b/obs-studio-server/source/osn-enhanced-broadcasting.hpp @@ -0,0 +1,142 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once +#include + +#include +#include + +namespace osn { + +template class EnhancedBroadcasting : public BaseStreaming { +public: + EnhancedBroadcasting() : BaseStreaming() {} + + virtual ~EnhancedBroadcasting() {} + + void StartEnhancedBroadcastingStream(std::optional vod_track_mixer = std::nullopt) + { + blog(LOG_INFO, "StartEnhancedBroadcastingStream - service id: %s", obs_service_get_id(this->service)); + + if (vod_track_mixer.has_value()) { + blog(LOG_INFO, "vod_track_mixer: %d", vod_track_mixer.value()); + } + + const bool is_custom = strncmp("rtmp_custom", obs_service_get_type(this->service), 11) == 0; + + OBSDataAutoRelease settings = obs_service_get_settings(this->service); + const std::string key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + auto server = obs_data_get_string(settings, "server"); + if (strcmp(server, "auto") != 0) { + custom_rtmp_url = server; + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %s server '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto auto_config_url = osn::MultitrackVideoAutoConfigURL(this->service); + blog(LOG_INFO, "Auto config URL: %s", auto_config_url.c_str()); + + std::vector canvases{this->GetCanvas()}; + if (this->GetAdditionalVideoCanvas()) { + canvases.push_back(this->GetAdditionalVideoCanvas()); + } + + auto go_live_post = osn::constructGoLivePost(canvases, key, std::nullopt, std::nullopt, vod_track_mixer.has_value()); + std::optional go_live_config = osn::DownloadGoLiveConfig(auto_config_url, go_live_post); + if (!go_live_config.has_value()) { + throw std::runtime_error("startStreaming - go live config is empty"); + } + + const auto audio_bitrate = osn::GetMultitrackAudioBitrate(); + const auto audio_encoder_id = osn::GetSimpleAACEncoderForBitrate(audio_bitrate); + + std::vector audio_encoders; + std::shared_ptr video_encoder_group; + auto output = osn::SetupOBSOutput("Enhanced Broadcasting", go_live_config.value(), audio_encoders, video_encoder_group, audio_encoder_id, 0, + vod_track_mixer); + if (!output) { + throw std::runtime_error("startStreaming - failed to create multitrack output"); + } + + // Stream key is defined by config from Twitch + auto multitrack_video_service = osn::create_service(*go_live_config, std::nullopt, ""); + if (!multitrack_video_service) { + throw std::runtime_error("startStreaming - failed to create multitrack video service"); + } + + this->SetOutput(output.Get()); + obs_output_set_service(output, multitrack_video_service); + + // Register the BPM (Broadcast Performance Metrics) callback + obs_output_add_packet_callback(output, bpm_inject, NULL); + + this->StartOutput(); + + enhancedBroadcastingContext.emplace(EnhancedBroadcastOutputObjects{ + std::move(output), + video_encoder_group, + std::move(audio_encoders), + std::move(multitrack_video_service), + }); + } + + void StopEnhancedBroadcastingStream() + { + blog(LOG_INFO, "StopEnhancedBroadcastingStream - service id: %s", obs_service_get_id(this->service)); + + auto output = this->GetOutput(); + if (!output) { + blog(LOG_WARNING, "StopEnhancedBroadcastingStream - empty output"); + return; + } + + obs_output_remove_packet_callback(output, bpm_inject, NULL); + bpm_destroy(output); + } + + obs_video_info *GetAdditionalVideoCanvas() { return additionalVideoContext; } + + void SetAdditionalVideoCanvas(obs_video_info *video) { additionalVideoContext = video; } + + void DeleteOutput() override + { + BaseStreaming::DeleteOutput(); + enhancedBroadcastingContext.reset(); + } + +private: + std::optional enhancedBroadcastingContext; + + // If set, this context makes enhanced broadcasting stream the dual streaming modde + obs_video_info *additionalVideoContext = nullptr; +}; + +} \ No newline at end of file diff --git a/obs-studio-server/source/osn-file-output.cpp b/obs-studio-server/source/osn-file-output.cpp index 483637d6e..90b74947c 100644 --- a/obs-studio-server/source/osn-file-output.cpp +++ b/obs-studio-server/source/osn-file-output.cpp @@ -75,7 +75,7 @@ void osn::IFileOutput::GetVideoCanvas(void *data, const int64_t id, const std::v PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "File output reference is not valid."); } - uint64_t uid = osn::Video::Manager::GetInstance().find(fileOutput->canvas); + uint64_t uid = osn::Video::Manager::GetInstance().find(fileOutput->GetCanvas()); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); rval.push_back(ipc::value(uid)); @@ -94,7 +94,7 @@ void osn::IFileOutput::SetVideoCanvas(void *data, const int64_t id, const std::v PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Canvas reference is not valid."); } - fileOutput->canvas = canvas; + fileOutput->SetCanvas(canvas); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -233,7 +233,7 @@ void osn::IFileOutput::GetLastFile(void *data, const int64_t id, const std::vect } calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(fileOutput->output); + proc_handler_t *ph = obs_output_get_proc_handler(fileOutput->GetOutput()); proc_handler_call(ph, "get_last_file", &cd); const char *path = calldata_string(&cd, "path"); path = path ? path : ""; diff --git a/obs-studio-server/source/osn-file-output.hpp b/obs-studio-server/source/osn-file-output.hpp index 89c77c817..4697d461c 100644 --- a/obs-studio-server/source/osn-file-output.hpp +++ b/obs-studio-server/source/osn-file-output.hpp @@ -19,12 +19,12 @@ #pragma once #include #include "utility.hpp" -#include "osn-output-signals.hpp" +#include "osn-output.hpp" namespace osn { -class FileOutput : public OutputSignals { +class FileOutput : public Output { public: - FileOutput() : OutputSignals() + FileOutput(const std::vector &signals) : Output(signals) { path = ""; format = "mp4"; diff --git a/obs-studio-server/source/osn-multitrack-video-configuration.cpp b/obs-studio-server/source/osn-multitrack-video-configuration.cpp index 577f2903b..a8d366ce4 100644 --- a/obs-studio-server/source/osn-multitrack-video-configuration.cpp +++ b/obs-studio-server/source/osn-multitrack-video-configuration.cpp @@ -218,7 +218,7 @@ std::string MultitrackVideoAutoConfigURL(obs_service_t *service) return url; } -PostData constructGoLivePost(StreamServiceId serviceId, bool dualStreamingMode, std::string streamKey, const std::optional &maximum_aggregate_bitrate, +PostData constructGoLivePost(std::vector canvases, std::string streamKey, const std::optional &maximum_aggregate_bitrate, const std::optional &maximum_video_tracks, bool vod_track_enabled) { PostData post_data{}; @@ -255,23 +255,13 @@ PostData constructGoLivePost(StreamServiceId serviceId, bool dualStreamingMode, if (obs_get_video_info(&ovi)) preferences.composition_gpu_index = ovi.adapter; - const size_t contexts = obs_get_video_info_count(); - for (size_t i = 0; i < contexts; i++) { - if (obs_get_video_info_by_index(i, &ovi)) { - const bool isHorizontalCanvas = ovi.base_width >= ovi.base_height; - if (!dualStreamingMode && isHorizontalCanvas && serviceId != StreamServiceId::Main) { - blog(LOG_INFO, "constructGoLivePost - skipping horizontal canvas"); - continue; - } - - if (!dualStreamingMode && !isHorizontalCanvas && serviceId != StreamServiceId::Second) { - blog(LOG_INFO, "constructGoLivePost - skipping vertical canvas"); - continue; - } - - preferences.canvases.emplace_back( - Canvas{ovi.output_width, ovi.output_height, ovi.base_width, ovi.base_height, {ovi.fps_num, ovi.fps_den}}); + for (const auto &c : canvases) { + if (!c) { + blog(LOG_WARNING, "constructGoLivePost - empty canvas, skipping"); + continue; } + + preferences.canvases.emplace_back(Canvas{c->output_width, c->output_height, c->base_width, c->base_height, {c->fps_num, c->fps_den}}); } obs_audio_info2 oai2; diff --git a/obs-studio-server/source/osn-multitrack-video-configuration.hpp b/obs-studio-server/source/osn-multitrack-video-configuration.hpp index 3741694bb..4662a6744 100644 --- a/obs-studio-server/source/osn-multitrack-video-configuration.hpp +++ b/obs-studio-server/source/osn-multitrack-video-configuration.hpp @@ -7,6 +7,7 @@ #include #include +#include namespace osn { @@ -14,7 +15,7 @@ std::string MultitrackVideoAutoConfigURL(obs_service_t *service); Config DownloadGoLiveConfig(std::string url, const PostData &post_data); -PostData constructGoLivePost(StreamServiceId serviceId, bool dualStreamingMode, std::string streamKey, const std::optional &maximum_aggregate_bitrate, +PostData constructGoLivePost(std::vector canvases, std::string streamKey, const std::optional &maximum_aggregate_bitrate, const std::optional &maximum_video_tracks, bool vod_track_enabled); } \ No newline at end of file diff --git a/obs-studio-server/source/osn-multitrack-video-system-info.cpp b/obs-studio-server/source/osn-multitrack-video-system-info.cpp index dd74c229d..04167a41b 100644 --- a/obs-studio-server/source/osn-multitrack-video-system-info.cpp +++ b/obs-studio-server/source/osn-multitrack-video-system-info.cpp @@ -241,7 +241,6 @@ void system_info(Capabilities &capabilities) system_data.build = ver.build; system_data.release = win_release_id; system_data.revision = std::to_string(ver.revis); - ; system_data.bits = is_64_bit_windows() ? 64 : 32; system_data.arm = is_arm64_windows(); system_data.armEmulation = os_get_emulation_status(); diff --git a/obs-studio-server/source/osn-multitrack-video.cpp b/obs-studio-server/source/osn-multitrack-video.cpp new file mode 100644 index 000000000..c68e3dd95 --- /dev/null +++ b/obs-studio-server/source/osn-multitrack-video.cpp @@ -0,0 +1,26 @@ +#include "osn-multitrack-video.hpp" + +#include "osn-audio-bitrate.hpp" + +#include "nodeobs_configManager.hpp" +#include + +namespace osn { + +bool IsMultitrackVideoEnabled() +{ + return config_get_bool(ConfigManager::getInstance().getBasic(), "EnhancedBroadcasting", "EnableMultitrackVideo"); +} + +int GetMultitrackAudioBitrate() +{ + const char *audio_encoder_id = config_get_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", "StreamAudioEncoder"); + const int bitrate = (int)config_get_uint(ConfigManager::getInstance().getBasic(), "SimpleOutput", "ABitrate"); + + if (audio_encoder_id && strcmp(audio_encoder_id, "opus") == 0) + return osn::FindClosestAvailableSimpleOpusBitrate(bitrate); + + return osn::FindClosestAvailableSimpleAACBitrate(bitrate); +} + +} \ No newline at end of file diff --git a/obs-studio-server/source/osn-multitrack-video.hpp b/obs-studio-server/source/osn-multitrack-video.hpp new file mode 100644 index 000000000..b81ab8005 --- /dev/null +++ b/obs-studio-server/source/osn-multitrack-video.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "osn-multitrack-video-configuration.hpp" +#include "osn-audio-bitrate.hpp" +#include "osn-multitrack-video-output.hpp" + +#include "obs.hpp" + +namespace osn { + +struct EnhancedBroadcastOutputObjects { + OBSOutputAutoRelease obsOutput; + std::shared_ptr videoEncoderGroup; + std::vector audioEncoders; + OBSServiceAutoRelease multitrackVideoService; +}; + +bool IsMultitrackVideoEnabled(); + +int GetMultitrackAudioBitrate(); + +} \ No newline at end of file diff --git a/obs-studio-server/source/osn-network.cpp b/obs-studio-server/source/osn-network.cpp index 6e4a335d4..426b598b4 100644 --- a/obs-studio-server/source/osn-network.cpp +++ b/obs-studio-server/source/osn-network.cpp @@ -24,6 +24,7 @@ void osn::INetwork::Register(ipc::server &srv) std::shared_ptr cls = std::make_shared("Network"); cls->register_function(std::make_shared("Create", std::vector{}, Create)); + cls->register_function(std::make_shared("Destroy", std::vector{}, Destroy)); cls->register_function(std::make_shared("GetBindIP", std::vector{ipc::type::UInt64}, GetBindIP)); cls->register_function(std::make_shared("SetBindIP", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetBindIP)); @@ -53,6 +54,20 @@ void osn::INetwork::Create(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + uint64_t uid = args[0].value_union.ui64; + Network *network = osn::INetwork::Manager::GetInstance().find(uid); + if (!network) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Network reference is not valid."); + } + + osn::INetwork::Manager::GetInstance().free(uid); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + void osn::INetwork::GetBindIP(void *data, const int64_t id, const std::vector &args, std::vector &rval) { Network *network = osn::INetwork::Manager::GetInstance().find(args[0].value_union.ui64); diff --git a/obs-studio-server/source/osn-network.hpp b/obs-studio-server/source/osn-network.hpp index 95281a5ff..82bd7c8d3 100644 --- a/obs-studio-server/source/osn-network.hpp +++ b/obs-studio-server/source/osn-network.hpp @@ -60,6 +60,7 @@ class INetwork { static void Register(ipc::server &); static void Create(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Destroy(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetBindIP(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void SetBindIP(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetNetworkInterfaces(void *data, const int64_t id, const std::vector &args, std::vector &rval); diff --git a/obs-studio-server/source/osn-output-signals.cpp b/obs-studio-server/source/osn-output-signals.cpp deleted file mode 100644 index 5f52c7eac..000000000 --- a/obs-studio-server/source/osn-output-signals.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/****************************************************************************** - Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -******************************************************************************/ - -#include "osn-output-signals.hpp" -#include "nodeobs_api.h" - -void osn::OutputSignals::createOutput(const std::string &type, const std::string &name) -{ - deleteOutput(); - output = obs_output_create(type.c_str(), name.c_str(), nullptr, nullptr); - - auto onStopped = [](void *data, calldata_t *) { - osn::OutputSignals *context = reinterpret_cast(data); - std::unique_lock lock(context->mtxOutputStop); - context->cvStop.notify_one(); - }; - - signal_handler *sh = obs_output_get_signal_handler(output); - signal_handler_connect(sh, "stop", onStopped, this); - - ConnectSignals(); -} - -void osn::OutputSignals::deleteOutput() -{ - if (!output) - return; - - if (obs_output_active(output)) { - obs_output_stop(output); - std::unique_lock lock(mtxOutputStop); - cvStop.wait_for(lock, std::chrono::seconds(20)); - } - obs_output_release(output); - output = nullptr; -} - -static void callback(void *data, calldata_t *params) -{ - auto info = reinterpret_cast(data); - - if (!info) - return; - - std::string signal = info->signal; - auto outputClass = info->outputClass; - - if (!outputClass->output) - return; - - const char *error = obs_output_get_last_error(outputClass->output); - - std::unique_lock ulock(outputClass->signalsMtx); - outputClass->signalsReceived.push({signal, (int)calldata_int(params, "code"), error ? std::string(error) : ""}); -} - -void osn::OutputSignals::ConnectSignals() -{ - if (!output) - return; - - signal_handler *handler = obs_output_get_signal_handler(output); - for (const auto &signal : this->signals) { - osn::cbData *cd = new cbData(); - cd->signal = signal; - cd->outputClass = this; - signal_handler_connect(handler, signal.c_str(), callback, cd); - } -} - -void osn::OutputSignals::startOutput() -{ - if (!output) - return; - - outdated_driver_error::instance()->set_active(true); - bool result = obs_output_start(output); - outdated_driver_error::instance()->set_active(false); - - if (result) - return; - - int code = 0; - std::string errorMessage = ""; - std::string outdated_driver_error = outdated_driver_error::instance()->get_error(); - - if (outdated_driver_error.size() != 0) { - errorMessage = outdated_driver_error; - code = OBS_OUTPUT_OUTDATED_DRIVER; - } else { - const char *error = obs_output_get_last_error(output); - if (error) { - errorMessage = error; - blog(LOG_INFO, "Last streaming error: %s", error); - } - code = OBS_OUTPUT_ERROR; - } - - std::unique_lock ulock(signalsMtx); - signalsReceived.push({"stop", code, errorMessage}); -} \ No newline at end of file diff --git a/obs-studio-server/source/osn-output.cpp b/obs-studio-server/source/osn-output.cpp index 4d82e05db..24b479c77 100644 --- a/obs-studio-server/source/osn-output.cpp +++ b/obs-studio-server/source/osn-output.cpp @@ -17,3 +17,166 @@ ******************************************************************************/ #include "osn-Output.hpp" + +#include "nodeobs_api.h" + +namespace { + +struct CallbackData { + std::string signal; + osn::Output *outputClass; +}; + +} // namespace + +osn::Output::Output(const std::vector &signals) : m_signals(signals) {} + +osn::Output::~Output() {} + +void osn::Output::InitOutput(obs_output_t *output) +{ + auto onStopped = [](void *data, calldata_t *) { + osn::Output *context = reinterpret_cast(data); + std::unique_lock lock(context->m_mtxOutputStop); + context->m_cvStop.notify_one(); + }; + + signal_handler *sh = obs_output_get_signal_handler(output); + signal_handler_connect(sh, "stop", onStopped, this); + + ConnectSignals(); +} + +void osn::Output::CreateOutput(const std::string &type, const std::string &name) +{ + DeleteOutput(); + m_output = obs_output_create(type.c_str(), name.c_str(), nullptr, nullptr); + + InitOutput(m_output); +} + +void osn::Output::SetOutput(obs_output_t *output) +{ + DeleteOutput(); + m_output = obs_output_get_ref(output); + InitOutput(m_output); +} + +void osn::Output::DeleteOutput() +{ + if (!m_output) + return; + + blog(LOG_INFO, "osn::Output::DeleteOutput"); + + if (obs_output_active(m_output)) { + obs_output_stop(m_output); + std::unique_lock lock(m_mtxOutputStop); + m_cvStop.wait_for(lock, std::chrono::seconds(20)); + } + obs_output_release(m_output); + m_output = nullptr; +} + +void osn::OutputSignalCallback(void *data, calldata_t *params) +{ + auto info = reinterpret_cast(data); + + if (!info) + return; + + std::string signal = info->signal; + auto outputClass = info->outputClass; + + if (!outputClass->m_output) + return; + + const char *error = obs_output_get_last_error(outputClass->m_output); + + std::unique_lock ulock(outputClass->m_signalsMtx); + outputClass->m_signalsReceived.push({signal, (int)calldata_int(params, "code"), error ? std::string(error) : ""}); +} + +void osn::Output::ConnectSignals() +{ + if (!m_output) + return; + + signal_handler *handler = obs_output_get_signal_handler(m_output); + for (const auto &signal : m_signals) { + auto *cd = new CallbackData(); + cd->signal = signal; + cd->outputClass = this; + signal_handler_connect(handler, signal.c_str(), osn::OutputSignalCallback, cd); + } +} + +void osn::Output::StartOutput() +{ + if (!m_output) + return; + + outdated_driver_error::instance()->set_active(true); + bool result = obs_output_start(m_output); + outdated_driver_error::instance()->set_active(false); + + if (result) + return; + + int code = 0; + std::string errorMessage = ""; + std::string outdated_driver_error = outdated_driver_error::instance()->get_error(); + + if (outdated_driver_error.size() != 0) { + errorMessage = outdated_driver_error; + code = OBS_OUTPUT_OUTDATED_DRIVER; + } else { + const char *error = obs_output_get_last_error(m_output); + if (error) { + errorMessage = error; + blog(LOG_INFO, "Last streaming error: %s", error); + } + code = OBS_OUTPUT_ERROR; + } + + std::unique_lock ulock(m_signalsMtx); + m_signalsReceived.push({"stop", code, errorMessage}); +} + +std::optional osn::Output::PopReceivedSignal() +{ + std::unique_lock ulock(m_signalsMtx); + + if (m_signalsReceived.empty()) { + return {}; + } + + const auto result = m_signalsReceived.front(); + m_signalsReceived.pop(); + return result; +} + +void osn::Output::SetCanvas(obs_video_info *canvas) +{ + m_canvas = canvas; +} + +obs_video_info *osn::Output::GetCanvas() +{ + return m_canvas; +} + +const obs_video_info *osn::Output::GetCanvas() const +{ + return m_canvas; +} + +obs_output_t *osn::Output::GetOutput() +{ + return m_output; +} + +const obs_output_t *osn::Output::GetOutput() const +{ + return m_output; +} \ No newline at end of file diff --git a/obs-studio-server/source/osn-output.hpp b/obs-studio-server/source/osn-output.hpp index 13250cfb6..f417dd68d 100644 --- a/obs-studio-server/source/osn-output.hpp +++ b/obs-studio-server/source/osn-output.hpp @@ -17,3 +17,59 @@ ******************************************************************************/ #pragma once + +#include + +#include +#include +#include +#include + +namespace osn { + +class Output { +public: + struct SignalInfo { + std::string signal; + int code = 0; + std::string errorMessage; + }; + +public: + Output(const std::vector &signals); + virtual ~Output(); + + void ConnectSignals(); + void CreateOutput(const std::string &type, const std::string &name); + void SetOutput(obs_output_t *output); + virtual void DeleteOutput(); + void StartOutput(); + + // If no signal, will return an empty optional. Thread safe. + std::optional PopReceivedSignal(); + + void SetCanvas(obs_video_info *canvas); + obs_video_info *GetCanvas(); + const obs_video_info *GetCanvas() const; + + obs_output_t *GetOutput(); + const obs_output_t *GetOutput() const; + +private: + friend void OutputSignalCallback(void *data, calldata_t *params); + + void InitOutput(obs_output_t *output); + + obs_video_info *m_canvas = nullptr; + obs_output_t *m_output = nullptr; + + std::mutex m_signalsMtx; + std::queue m_signalsReceived; + + std::condition_variable m_cvStop; + std::mutex m_mtxOutputStop; + + const std::vector m_signals; +}; + +} \ No newline at end of file diff --git a/obs-studio-server/source/osn-reconnect.cpp b/obs-studio-server/source/osn-reconnect.cpp index 4b405e9e2..c3f5fe088 100644 --- a/obs-studio-server/source/osn-reconnect.cpp +++ b/obs-studio-server/source/osn-reconnect.cpp @@ -25,6 +25,7 @@ void osn::IReconnect::Register(ipc::server &srv) std::shared_ptr cls = std::make_shared("Reconnect"); cls->register_function(std::make_shared("Create", std::vector{}, Create)); + cls->register_function(std::make_shared("Destroy", std::vector{}, Destroy)); cls->register_function(std::make_shared("GetEnabled", std::vector{ipc::type::UInt64}, GetEnabled)); cls->register_function(std::make_shared("SetEnabled", std::vector{ipc::type::UInt64, ipc::type::UInt32}, SetEnabled)); @@ -48,6 +49,20 @@ void osn::IReconnect::Create(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + uint64_t uid = args[0].value_union.ui64; + Reconnect *reconnect = osn::IReconnect::Manager::GetInstance().find(uid); + if (!reconnect) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Reconnect reference is not valid."); + } + + osn::IReconnect::Manager::GetInstance().free(uid); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + void osn::IReconnect::GetEnabled(void *data, const int64_t id, const std::vector &args, std::vector &rval) { Reconnect *reconnect = osn::IReconnect::Manager::GetInstance().find(args[0].value_union.ui64); diff --git a/obs-studio-server/source/osn-reconnect.hpp b/obs-studio-server/source/osn-reconnect.hpp index f93507060..f6902d505 100644 --- a/obs-studio-server/source/osn-reconnect.hpp +++ b/obs-studio-server/source/osn-reconnect.hpp @@ -58,6 +58,7 @@ class IReconnect { static void Register(ipc::server &); static void Create(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Destroy(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetEnabled(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void SetEnabled(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetRetryDelay(void *data, const int64_t id, const std::vector &args, std::vector &rval); diff --git a/obs-studio-server/source/osn-recording.cpp b/obs-studio-server/source/osn-recording.cpp index 77da3582d..462b080ba 100644 --- a/obs-studio-server/source/osn-recording.cpp +++ b/obs-studio-server/source/osn-recording.cpp @@ -26,20 +26,13 @@ extern char *osn_generate_formatted_filename(const char *extension, bool space, osn::Recording::~Recording() { - deleteOutput(); + DeleteOutput(); } void osn::IRecording::GetVideoEncoder(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - Recording *recording = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!recording) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple recording reference is not valid."); - } - - uint64_t uid = osn::VideoEncoder::Manager::GetInstance().find(recording->videoEncoder); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -50,6 +43,13 @@ void osn::IRecording::SetVideoEncoder(void *data, const int64_t id, const std::v PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple recording reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + recording->videoEncoder = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + obs_encoder_t *encoder = osn::VideoEncoder::Manager::GetInstance().find(args[1].value_union.ui64); if (!encoder) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Encoder reference is not valid."); @@ -68,22 +68,18 @@ void osn::IRecording::Query(void *data, const int64_t id, const std::vector ulock(recording->signalsMtx); - if (recording->signalsReceived.empty()) { + auto signalOpt = recording->PopReceivedSignal(); + if (!signalOpt.has_value()) { rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; return; } rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - - auto signal = recording->signalsReceived.front(); rval.push_back(ipc::value("recording")); - rval.push_back(ipc::value(signal.signal)); - rval.push_back(ipc::value(signal.code)); - rval.push_back(ipc::value(signal.errorMessage)); - - recording->signalsReceived.pop(); + rval.push_back(ipc::value(signalOpt.value().signal)); + rval.push_back(ipc::value(signalOpt.value().code)); + rval.push_back(ipc::value(signalOpt.value().errorMessage)); AUTO_DEBUG; } @@ -153,13 +149,13 @@ obs_encoder_t *osn::IRecording::duplicate_encoder(obs_encoder_t *src, uint64_t t void osn::IRecording::SplitFile(void *data, const int64_t id, const std::vector &args, std::vector &rval) { Recording *recording = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!recording || !recording->output) { + if (!recording || !recording->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); } calldata_t cd = {0}; - proc_handler_t *ph = obs_output_get_proc_handler(recording->output); + proc_handler_t *ph = obs_output_get_proc_handler(recording->GetOutput()); proc_handler_call(ph, "split_file", &cd); calldata_free(&cd); @@ -310,6 +306,6 @@ void osn::Recording::ConfigureRecFileSplitting() obs_data_set_bool(settings, "reset_timestamps", fileResetTimestamps); - obs_output_update(output, settings); + obs_output_update(this->GetOutput(), settings); obs_data_release(settings); } diff --git a/obs-studio-server/source/osn-recording.hpp b/obs-studio-server/source/osn-recording.hpp index 5a5f0930d..27afc8231 100644 --- a/obs-studio-server/source/osn-recording.hpp +++ b/obs-studio-server/source/osn-recording.hpp @@ -27,10 +27,10 @@ enum SplitFileType : uint32_t { TIME, SIZE, MANUAL }; class Recording : public FileOutput { public: - Recording() : FileOutput() + Recording() : FileOutput({"start", "stop", "stopping", "wrote"}) { videoEncoder = nullptr; - signals = {"start", "stop", "stopping", "wrote"}; + //signals = {"start", "stop", "stopping", "wrote"}; enableFileSplit = false; splitType = SplitFileType::TIME; splitTime = 15; diff --git a/obs-studio-server/source/osn-replay-buffer.cpp b/obs-studio-server/source/osn-replay-buffer.cpp index 3a4b55d89..c72d034d7 100644 --- a/obs-studio-server/source/osn-replay-buffer.cpp +++ b/obs-studio-server/source/osn-replay-buffer.cpp @@ -24,7 +24,7 @@ osn::ReplayBuffer::~ReplayBuffer() { - deleteOutput(); + DeleteOutput(); } void osn::IReplayBuffer::GetDuration(void *data, const int64_t id, const std::vector &args, std::vector &rval) @@ -134,22 +134,18 @@ void osn::IReplayBuffer::Query(void *data, const int64_t id, const std::vector ulock(replayBuffer->signalsMtx); - if (replayBuffer->signalsReceived.empty()) { + auto signalOpt = replayBuffer->PopReceivedSignal(); + if (!signalOpt.has_value()) { rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; return; } rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - - auto signal = replayBuffer->signalsReceived.front(); rval.push_back(ipc::value("replay-buffer")); - rval.push_back(ipc::value(signal.signal)); - rval.push_back(ipc::value(signal.code)); - rval.push_back(ipc::value(signal.errorMessage)); - - replayBuffer->signalsReceived.pop(); + rval.push_back(ipc::value(signalOpt.value().signal)); + rval.push_back(ipc::value(signalOpt.value().code)); + rval.push_back(ipc::value(signalOpt.value().errorMessage)); AUTO_DEBUG; } diff --git a/obs-studio-server/source/osn-replay-buffer.hpp b/obs-studio-server/source/osn-replay-buffer.hpp index a296b5f32..8dbf1087d 100644 --- a/obs-studio-server/source/osn-replay-buffer.hpp +++ b/obs-studio-server/source/osn-replay-buffer.hpp @@ -25,13 +25,13 @@ namespace osn { class ReplayBuffer : public FileOutput { public: - ReplayBuffer() + ReplayBuffer() : FileOutput({"start", "stop", "stopping", "writing", "wrote", "writing_error"}) { duration = 20; prefix = "Replay"; suffix = ""; usesStream = false; - signals = {"start", "stop", "stopping", "writing", "wrote", "writing_error"}; + //signals = {"start", "stop", "stopping", "writing", "wrote", "writing_error"}; } virtual ~ReplayBuffer(); diff --git a/obs-studio-server/source/osn-simple-recording.cpp b/obs-studio-server/source/osn-simple-recording.cpp index 27addc6d3..c57fb6f9a 100644 --- a/obs-studio-server/source/osn-simple-recording.cpp +++ b/obs-studio-server/source/osn-simple-recording.cpp @@ -116,15 +116,8 @@ void osn::ISimpleRecording::SetQuality(void *data, const int64_t id, const std:: void osn::ISimpleRecording::GetAudioEncoder(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - SimpleRecording *recording = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!recording) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); - } - - uint64_t uid = osn::AudioEncoder::Manager::GetInstance().find(recording->audioEncoder); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -135,6 +128,13 @@ void osn::ISimpleRecording::SetAudioEncoder(void *data, const int64_t id, const PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + recording->audioEncoder = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + obs_encoder_t *encoder = osn::AudioEncoder::Manager::GetInstance().find(args[1].value_union.ui64); if (!encoder) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Encoder reference is not valid."); @@ -153,8 +153,8 @@ static void LoadLosslessPreset(osn::Recording *recording) obs_data_set_string(settings, "video_encoder", "utvideo"); obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); - obs_output_set_mixers(recording->output, 1); - obs_output_update(recording->output, settings); + obs_output_set_mixers(recording->GetOutput(), 1); + obs_output_update(recording->GetOutput(), settings); obs_data_release(settings); } @@ -293,9 +293,9 @@ static void UpdateRecordingSettings_crf(enum osn::RecQuality quality, osn::Simpl if (!settings) return; if (obs_get_multiple_rendering()) { - obs_encoder_set_video_mix(recording->videoEncoder, obs_video_mix_get(recording->canvas, OBS_STREAMING_VIDEO_RENDERING)); + obs_encoder_set_video_mix(recording->videoEncoder, obs_video_mix_get(recording->GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); } else { - obs_encoder_set_video_mix(recording->videoEncoder, obs_video_mix_get(recording->canvas, OBS_MAIN_VIDEO_RENDERING)); + obs_encoder_set_video_mix(recording->videoEncoder, obs_video_mix_get(recording->GetCanvas(), OBS_MAIN_VIDEO_RENDERING)); } obs_encoder_update(recording->videoEncoder, settings); obs_data_release(settings); @@ -319,6 +319,7 @@ void osn::SimpleRecording::UpdateEncoders() if (obs_get_multiple_rendering()) { obs_encoder_t *videoEncDup = osn::IRecording::duplicate_encoder(videoEncoder); videoEncoder = videoEncDup; + obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(this->GetCanvas(), OBS_RECORDING_VIDEO_RENDERING)); } break; } @@ -344,10 +345,10 @@ void osn::ISimpleRecording::Start(void *data, const int64_t id, const std::vecto } const char *ffmpegMuxer = "ffmpeg_muxer"; - if (!recording->output || strcmp(obs_output_get_id(recording->output), ffmpegMuxer) == 0) - recording->createOutput("ffmpeg_muxer", "recording"); + if (!recording->GetOutput() || strcmp(obs_output_get_id(recording->GetOutput()), ffmpegMuxer) == 0) + recording->CreateOutput("ffmpeg_muxer", "recording"); - if (!recording->output) { + if (!recording->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Error while creating the recording output."); } @@ -355,7 +356,7 @@ void osn::ISimpleRecording::Start(void *data, const int64_t id, const std::vecto std::string pathProperty = "path"; if (recording->quality == RecQuality::Lossless) { - recording->createOutput("ffmpeg_output", "recording"); + recording->CreateOutput("ffmpeg_output", "recording"); LoadLosslessPreset(recording); format = "avi"; pathProperty = "url"; @@ -371,10 +372,10 @@ void osn::ISimpleRecording::Start(void *data, const int64_t id, const std::vecto } obs_encoder_set_audio(recording->audioEncoder, obs_get_audio()); - obs_output_set_audio_encoder(recording->output, recording->audioEncoder, 0); - obs_encoder_set_video_mix(recording->audioEncoder, obs_video_mix_get(recording->canvas, OBS_RECORDING_VIDEO_RENDERING)); + obs_output_set_audio_encoder(recording->GetOutput(), recording->audioEncoder, 0); + obs_encoder_set_video_mix(recording->audioEncoder, obs_video_mix_get(recording->GetCanvas(), OBS_RECORDING_VIDEO_RENDERING)); - obs_output_set_video_encoder(recording->output, recording->videoEncoder); + obs_output_set_video_encoder(recording->GetOutput(), recording->videoEncoder); } if (!recording->path.size()) { @@ -387,7 +388,8 @@ void osn::ISimpleRecording::Start(void *data, const int64_t id, const std::vecto if (lastChar != '/' && lastChar != '\\') path += "/"; - path += GenerateSpecifiedFilename(format, recording->noSpace, recording->fileFormat, recording->canvas->base_width, recording->canvas->base_height); + path += GenerateSpecifiedFilename(format, recording->noSpace, recording->fileFormat, recording->GetCanvas()->base_width, + recording->GetCanvas()->base_height); if (!recording->overwrite) FindBestFilename(path, recording->noSpace); @@ -395,13 +397,13 @@ void osn::ISimpleRecording::Start(void *data, const int64_t id, const std::vecto obs_data_t *settings = obs_data_create(); obs_data_set_string(settings, pathProperty.c_str(), path.c_str()); obs_data_set_string(settings, "muxer_settings", recording->muxerSettings.c_str()); - obs_output_update(recording->output, settings); + obs_output_update(recording->GetOutput(), settings); obs_data_release(settings); if (recording->enableFileSplit) recording->ConfigureRecFileSplitting(); - recording->startOutput(); + recording->StartOutput(); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -413,11 +415,11 @@ void osn::ISimpleRecording::Stop(void *data, const int64_t id, const std::vector if (!recording) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple recording reference is not valid."); } - if (!recording->output) { + if (!recording->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid recording output."); } - obs_output_stop(recording->output); + obs_output_stop(recording->GetOutput()); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -551,15 +553,8 @@ void osn::ISimpleRecording::GetLegacySettings(void *data, const int64_t id, cons void osn::ISimpleRecording::GetStreaming(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - SimpleRecording *recording = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!recording) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); - } - - uint64_t uid = osn::ISimpleStreaming::Manager::GetInstance().find(recording->streaming); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -570,6 +565,13 @@ void osn::ISimpleRecording::SetStreaming(void *data, const int64_t id, const std PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + recording->streaming = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + SimpleStreaming *streaming = static_cast(osn::ISimpleStreaming::Manager::GetInstance().find(args[1].value_union.ui64)); if (!streaming) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); diff --git a/obs-studio-server/source/osn-simple-replay-buffer.cpp b/obs-studio-server/source/osn-simple-replay-buffer.cpp index 30af1d273..67ea788fc 100644 --- a/obs-studio-server/source/osn-simple-replay-buffer.cpp +++ b/obs-studio-server/source/osn-simple-replay-buffer.cpp @@ -95,8 +95,8 @@ void osn::ISimpleReplayBuffer::Start(void *data, const int64_t id, const std::ve PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple replay buffer reference is not valid."); } - if (!replayBuffer->output) - replayBuffer->createOutput("replay_buffer", "replay-buffer"); + if (!replayBuffer->GetOutput()) + replayBuffer->CreateOutput("replay_buffer", "replay-buffer"); obs_encoder_t *audioEncoder = nullptr; obs_encoder_t *videoEncoder = nullptr; @@ -120,13 +120,13 @@ void osn::ISimpleReplayBuffer::Start(void *data, const int64_t id, const std::ve } obs_encoder_set_audio(audioEncoder, obs_get_audio()); - obs_output_set_audio_encoder(replayBuffer->output, audioEncoder, 0); + obs_output_set_audio_encoder(replayBuffer->GetOutput(), audioEncoder, 0); if (!videoEncoder) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid video encoder."); } - obs_output_set_video_encoder(replayBuffer->output, videoEncoder); + obs_output_set_video_encoder(replayBuffer->GetOutput(), videoEncoder); if (!replayBuffer->path.size()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid recording path."); @@ -157,10 +157,10 @@ void osn::ISimpleReplayBuffer::Start(void *data, const int64_t id, const std::ve obs_data_set_bool(settings, "allow_spaces", !replayBuffer->noSpace); obs_data_set_int(settings, "max_time_sec", replayBuffer->duration); obs_data_set_int(settings, "max_size_mb", rbSize); - obs_output_update(replayBuffer->output, settings); + obs_output_update(replayBuffer->GetOutput(), settings); obs_data_release(settings); - replayBuffer->startOutput(); + replayBuffer->StartOutput(); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -172,11 +172,11 @@ void osn::ISimpleReplayBuffer::Stop(void *data, const int64_t id, const std::vec if (!replayBuffer) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple replay buffer reference is not valid."); } - if (!replayBuffer->output) { + if (!replayBuffer->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid replay buffer output."); } - obs_output_stop(replayBuffer->output); + obs_output_stop(replayBuffer->GetOutput()); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -235,15 +235,8 @@ void osn::ISimpleReplayBuffer::SetLegacySettings(void *data, const int64_t id, c void osn::ISimpleReplayBuffer::GetStreaming(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - SimpleReplayBuffer *replayBuffer = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!replayBuffer) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple replay buffer reference is not valid."); - } - - uint64_t uid = osn::ISimpleStreaming::Manager::GetInstance().find(replayBuffer->streaming); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -254,6 +247,13 @@ void osn::ISimpleReplayBuffer::SetStreaming(void *data, const int64_t id, const PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple replay buffer reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + replayBuffer->streaming = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + SimpleStreaming *streaming = static_cast(osn::ISimpleStreaming::Manager::GetInstance().find(args[1].value_union.ui64)); if (!streaming) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); @@ -267,15 +267,8 @@ void osn::ISimpleReplayBuffer::SetStreaming(void *data, const int64_t id, const void osn::ISimpleReplayBuffer::GetRecording(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - SimpleReplayBuffer *replayBuffer = static_cast(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!replayBuffer) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple replay buffer reference is not valid."); - } - - uint64_t uid = osn::ISimpleRecording::Manager::GetInstance().find(replayBuffer->recording); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -286,6 +279,13 @@ void osn::ISimpleReplayBuffer::SetRecording(void *data, const int64_t id, const PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple replay buffer reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + replayBuffer->recording = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + SimpleRecording *recording = static_cast(osn::ISimpleRecording::Manager::GetInstance().find(args[1].value_union.ui64)); if (!recording) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Recording reference is not valid."); diff --git a/obs-studio-server/source/osn-simple-streaming.cpp b/obs-studio-server/source/osn-simple-streaming.cpp index a50e3bbcc..cb81a39e1 100644 --- a/obs-studio-server/source/osn-simple-streaming.cpp +++ b/obs-studio-server/source/osn-simple-streaming.cpp @@ -96,15 +96,8 @@ void osn::ISimpleStreaming::Destroy(void *data, const int64_t id, const std::vec void osn::ISimpleStreaming::GetAudioEncoder(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - SimpleStreaming *streaming = static_cast(osn::ISimpleStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); - if (!streaming) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); - } - - uint64_t uid = osn::AudioEncoder::Manager::GetInstance().find(streaming->audioEncoder); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -115,6 +108,13 @@ void osn::ISimpleStreaming::SetAudioEncoder(void *data, const int64_t id, const PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + streaming->audioEncoder = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + obs_encoder_t *encoder = osn::AudioEncoder::Manager::GetInstance().find(args[1].value_union.ui64); if (!encoder) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Encoder reference is not valid."); @@ -218,8 +218,8 @@ static void SetupTwitchSoundtrackAudio(osn::SimpleStreaming *streaming) obs_encoder_set_audio(streaming->streamArchive, obs_get_audio()); } - obs_output_set_audio_encoder(streaming->output, streaming->streamArchive, kSoundtrackArchiveEncoderIdx); - obs_encoder_set_video_mix(streaming->streamArchive, obs_video_mix_get(streaming->canvas, OBS_STREAMING_VIDEO_RENDERING)); + obs_output_set_audio_encoder(streaming->GetOutput(), streaming->streamArchive, kSoundtrackArchiveEncoderIdx); + obs_encoder_set_video_mix(streaming->streamArchive, obs_video_mix_get(streaming->GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); obs_data_t *settings = obs_data_create(); @@ -317,9 +317,9 @@ void osn::SimpleStreaming::UpdateEncoders() obs_data_release(audioEncSettings); if (obs_get_multiple_rendering()) { - obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(canvas, OBS_STREAMING_VIDEO_RENDERING)); + obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(this->GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); } else { - obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(canvas, OBS_MAIN_VIDEO_RENDERING)); + obs_encoder_set_video_mix(videoEncoder, obs_video_mix_get(this->GetCanvas(), OBS_MAIN_VIDEO_RENDERING)); } } @@ -338,10 +338,10 @@ void osn::ISimpleStreaming::Start(void *data, const int64_t id, const std::vecto if (!type) type = "rtmp_output"; - if (!streaming->output || strcmp(obs_output_get_id(streaming->output), type) != 0) - streaming->createOutput(type, "stream"); + if (!streaming->GetOutput() || strcmp(obs_output_get_id(streaming->GetOutput()), type) != 0) + streaming->CreateOutput(type, "stream"); - if (!streaming->output) { + if (!streaming->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Error while creating the streaming output."); } @@ -353,12 +353,16 @@ void osn::ISimpleStreaming::Start(void *data, const int64_t id, const std::vecto PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid audio encoder."); } + if (!streaming->GetCanvas()) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid main canvas."); + } + streaming->UpdateEncoders(); obs_encoder_set_audio(streaming->audioEncoder, obs_get_audio()); - obs_output_set_audio_encoder(streaming->output, streaming->audioEncoder, 0); - obs_encoder_set_video_mix(streaming->audioEncoder, obs_video_mix_get(streaming->canvas, OBS_STREAMING_VIDEO_RENDERING)); + obs_output_set_audio_encoder(streaming->GetOutput(), streaming->audioEncoder, 0); + obs_encoder_set_video_mix(streaming->audioEncoder, obs_video_mix_get(streaming->GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); - obs_output_set_video_encoder(streaming->output, streaming->videoEncoder); + obs_output_set_video_encoder(streaming->GetOutput(), streaming->videoEncoder); if (streaming->enableTwitchVOD) { streaming->twitchVODSupported = streaming->isTwitchVODSupported(); @@ -366,19 +370,19 @@ void osn::ISimpleStreaming::Start(void *data, const int64_t id, const std::vecto SetupTwitchSoundtrackAudio(streaming); } - obs_output_set_service(streaming->output, streaming->service); + obs_output_set_service(streaming->GetOutput(), streaming->service); if (!streaming->delay) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid delay."); } - obs_output_set_delay(streaming->output, streaming->delay->enabled ? uint32_t(streaming->delay->delaySec) : 0, + obs_output_set_delay(streaming->GetOutput(), streaming->delay->enabled ? uint32_t(streaming->delay->delaySec) : 0, streaming->delay->preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); if (!streaming->reconnect) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid reconnect."); } uint32_t maxReties = streaming->reconnect->enabled ? streaming->reconnect->maxRetries : 0; - obs_output_set_reconnect_settings(streaming->output, maxReties, streaming->reconnect->retryDelay); + obs_output_set_reconnect_settings(streaming->GetOutput(), maxReties, streaming->reconnect->retryDelay); if (!streaming->network) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid network."); @@ -389,10 +393,10 @@ void osn::ISimpleStreaming::Start(void *data, const int64_t id, const std::vecto obs_data_set_bool(settings, "dyn_bitrate", streaming->network->enableDynamicBitrate); obs_data_set_bool(settings, "new_socket_loop_enabled", streaming->network->enableOptimizations); obs_data_set_bool(settings, "low_latency_mode_enabled", streaming->network->enableLowLatency); - obs_output_update(streaming->output, settings); + obs_output_update(streaming->GetOutput(), settings); obs_data_release(settings); - streaming->startOutput(); + streaming->StartOutput(); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -405,16 +409,16 @@ void osn::ISimpleStreaming::Stop(void *data, const int64_t id, const std::vector PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple streaming reference is not valid."); } - if (!streaming->output) { + if (!streaming->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid streaming output."); } bool force = args[1].value_union.ui32; if (force) - obs_output_force_stop(streaming->output); + obs_output_force_stop(streaming->GetOutput()); else - obs_output_stop(streaming->output); + obs_output_stop(streaming->GetOutput()); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; diff --git a/obs-studio-server/source/osn-streaming.cpp b/obs-studio-server/source/osn-streaming.cpp index f447c28c3..d24a29864 100644 --- a/obs-studio-server/source/osn-streaming.cpp +++ b/obs-studio-server/source/osn-streaming.cpp @@ -26,24 +26,22 @@ osn::Streaming::~Streaming() { - deleteOutput(); + DeleteOutput(); if (streamArchive && !obs_encoder_active(streamArchive)) { obs_encoder_release(streamArchive); streamArchive = nullptr; } } -void osn::IStreaming::GetService(void *data, const int64_t id, const std::vector &args, std::vector &rval) +void osn::Streaming::DeleteOutput() { - Streaming *streaming = osn::IStreaming::Manager::GetInstance().find(args[0].value_union.ui64); - if (!streaming) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Service reference is not valid."); - } - - uint64_t uid = osn::Service::Manager::GetInstance().find(streaming->service); + Output::DeleteOutput(); +} +void osn::IStreaming::GetService(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -54,6 +52,13 @@ void osn::IStreaming::SetService(void *data, const int64_t id, const std::vector PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + streaming->service = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + obs_service_t *service = osn::Service::Manager::GetInstance().find(args[1].value_union.ui64); if (!service) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Service reference is not valid."); @@ -72,7 +77,7 @@ void osn::IStreaming::GetVideoCanvas(void *data, const int64_t id, const std::ve PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); } - uint64_t uid = osn::Video::Manager::GetInstance().find(streaming->canvas); + uint64_t uid = osn::Video::Manager::GetInstance().find(streaming->GetCanvas()); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); rval.push_back(ipc::value(uid)); @@ -93,7 +98,7 @@ void osn::IStreaming::SetVideoCanvas(void *data, const int64_t id, const std::ve blog(LOG_INFO, "IStreaming::SetVideoCanvas - canvas: 0x%" PRIXPTR ", uid: %d", (uintptr_t)canvas, (int)args[1].value_union.ui64); - streaming->canvas = canvas; + streaming->SetCanvas(canvas); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; @@ -101,15 +106,8 @@ void osn::IStreaming::SetVideoCanvas(void *data, const int64_t id, const std::ve void osn::IStreaming::GetVideoEncoder(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - Streaming *streaming = osn::IStreaming::Manager::GetInstance().find(args[0].value_union.ui64); - if (!streaming) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); - } - - uint64_t uid = osn::VideoEncoder::Manager::GetInstance().find(streaming->videoEncoder); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -120,6 +118,13 @@ void osn::IStreaming::SetVideoEncoder(void *data, const int64_t id, const std::v PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + streaming->videoEncoder = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + obs_encoder_t *encoder = osn::VideoEncoder::Manager::GetInstance().find(args[1].value_union.ui64); if (!encoder) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Encoder reference is not valid."); @@ -183,15 +188,8 @@ void osn::IStreaming::SetEnableTwitchVOD(void *data, const int64_t id, const std void osn::IStreaming::GetDelay(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - Streaming *streaming = osn::IStreaming::Manager::GetInstance().find(args[0].value_union.ui64); - if (!streaming) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); - } - - uint64_t uid = osn::IDelay::Manager::GetInstance().find(streaming->delay); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -202,6 +200,13 @@ void osn::IStreaming::SetDelay(void *data, const int64_t id, const std::vectordelay = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + Delay *delay = osn::IDelay::Manager::GetInstance().find(args[1].value_union.ui64); if (!delay) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Encoder reference is not valid."); @@ -215,15 +220,8 @@ void osn::IStreaming::SetDelay(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - Streaming *streaming = osn::IStreaming::Manager::GetInstance().find(args[0].value_union.ui64); - if (!streaming) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); - } - - uint64_t uid = osn::IReconnect::Manager::GetInstance().find(streaming->reconnect); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -234,6 +232,13 @@ void osn::IStreaming::SetReconnect(void *data, const int64_t id, const std::vect PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + streaming->reconnect = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + Reconnect *reconnect = osn::IReconnect::Manager::GetInstance().find(args[1].value_union.ui64); if (!reconnect) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Reconnect reference is not valid."); @@ -247,15 +252,8 @@ void osn::IStreaming::SetReconnect(void *data, const int64_t id, const std::vect void osn::IStreaming::GetNetwork(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - Streaming *streaming = osn::IStreaming::Manager::GetInstance().find(args[0].value_union.ui64); - if (!streaming) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); - } - - uint64_t uid = osn::INetwork::Manager::GetInstance().find(streaming->network); - + blog(LOG_WARNING, "Function %s is deprecated", __func__); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value(uid)); AUTO_DEBUG; } @@ -266,6 +264,13 @@ void osn::IStreaming::SetNetwork(void *data, const int64_t id, const std::vector PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Streaming reference is not valid."); } + if (args[1].value_union.ui64 == UINT64_MAX) { + streaming->network = nullptr; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; + return; + } + Network *network = osn::INetwork::Manager::GetInstance().find(args[1].value_union.ui64); if (!network) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Network reference is not valid."); @@ -299,22 +304,18 @@ void osn::IStreaming::Query(void *data, const int64_t id, const std::vector ulock(streaming->signalsMtx); - if (streaming->signalsReceived.empty()) { + auto signalOpt = streaming->PopReceivedSignal(); + if (!signalOpt.has_value()) { rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; return; } rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - - auto signal = streaming->signalsReceived.front(); rval.push_back(ipc::value("streaming")); - rval.push_back(ipc::value(signal.signal)); - rval.push_back(ipc::value(signal.code)); - rval.push_back(ipc::value(signal.errorMessage)); - - streaming->signalsReceived.pop(); + rval.push_back(ipc::value(signalOpt.value().signal)); + rval.push_back(ipc::value(signalOpt.value().code)); + rval.push_back(ipc::value(signalOpt.value().errorMessage)); AUTO_DEBUG; } @@ -393,8 +394,8 @@ void osn::IStreaming::GetDroppedFrames(void *data, const int64_t id, const std:: int totalDropped = 0; - if (streaming->output && obs_output_active(streaming->output)) { - totalDropped = obs_output_get_frames_dropped(streaming->output); + if (streaming->GetOutput() && obs_output_active(streaming->GetOutput())) { + totalDropped = obs_output_get_frames_dropped(streaming->GetOutput()); } rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); @@ -411,8 +412,8 @@ void osn::IStreaming::GetTotalFrames(void *data, const int64_t id, const std::ve int totalFrames = 0; - if (streaming->output && obs_output_active(streaming->output)) { - totalFrames = obs_output_get_total_frames(streaming->output); + if (streaming->GetOutput() && obs_output_active(streaming->GetOutput())) { + totalFrames = obs_output_get_total_frames(streaming->GetOutput()); } rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); @@ -429,9 +430,9 @@ void osn::IStreaming::GetKBitsPerSec(void *data, const int64_t id, const std::ve double kbitsPerSec = 0; - if (streaming->output && obs_output_active(streaming->output)) { + if (streaming->GetOutput() && obs_output_active(streaming->GetOutput())) { - uint64_t bytesSent = obs_output_get_total_bytes(streaming->output); + uint64_t bytesSent = obs_output_get_total_bytes(streaming->GetOutput()); uint64_t bytesSentTime = os_gettime_ns(); if (bytesSent < streaming->lastBytesSent) @@ -466,9 +467,9 @@ void osn::IStreaming::GetDataOutput(void *data, const int64_t id, const std::vec double dataOutput = 0; - if (streaming->output && obs_output_active(streaming->output)) { + if (streaming->GetOutput() && obs_output_active(streaming->GetOutput())) { - uint64_t bytesSent = obs_output_get_total_bytes(streaming->output); + uint64_t bytesSent = obs_output_get_total_bytes(streaming->GetOutput()); uint64_t bytesSentTime = os_gettime_ns(); if (bytesSent < streaming->lastBytesSent) @@ -486,4 +487,4 @@ void osn::IStreaming::GetDataOutput(void *data, const int64_t id, const std::vec rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); rval.push_back(ipc::value(dataOutput)); AUTO_DEBUG; -} +} \ No newline at end of file diff --git a/obs-studio-server/source/osn-streaming.hpp b/obs-studio-server/source/osn-streaming.hpp index e1b0366fb..b94109ec7 100644 --- a/obs-studio-server/source/osn-streaming.hpp +++ b/obs-studio-server/source/osn-streaming.hpp @@ -22,15 +22,18 @@ #include "osn-delay.hpp" #include "osn-reconnect.hpp" #include "osn-network.hpp" -#include "osn-output-signals.hpp" +#include "osn-output.hpp" #include "osn-video-encoder.hpp" +#include "osn-multitrack-video.hpp" #include "nodeobs_configManager.hpp" +#include + namespace osn { -class Streaming : public OutputSignals { +class Streaming : public Output { public: - Streaming() + Streaming() : Output({"start", "stop", "starting", "stopping", "activate", "deactivate", "reconnect", "reconnect_success"}) { videoEncoder = nullptr; streamArchive = nullptr; @@ -40,7 +43,6 @@ class Streaming : public OutputSignals { twitchVODSupported = false; oldMixer_desktopSource1 = 0; oldMixer_desktopSource2 = 0; - signals = {"start", "stop", "starting", "stopping", "activate", "deactivate", "reconnect", "reconnect_success"}; delay = new Delay(); reconnect = new Reconnect(); network = new Network(); @@ -49,6 +51,8 @@ class Streaming : public OutputSignals { } virtual ~Streaming(); + void DeleteOutput() override; + public: obs_encoder_t *videoEncoder; obs_encoder_t *streamArchive; diff --git a/obs-studio-server/source/osn-video-encoder.cpp b/obs-studio-server/source/osn-video-encoder.cpp index 71a52e480..39b31a53b 100644 --- a/obs-studio-server/source/osn-video-encoder.cpp +++ b/obs-studio-server/source/osn-video-encoder.cpp @@ -33,6 +33,7 @@ void osn::VideoEncoder::Register(ipc::server &srv) cls->register_function(std::make_shared("GetId", std::vector{ipc::type::UInt64}, GetId)); cls->register_function(std::make_shared("GetLastError", std::vector{ipc::type::UInt64}, GetLastError)); cls->register_function(std::make_shared("Release", std::vector{ipc::type::UInt64}, Release)); + cls->register_function(std::make_shared("Finalize", std::vector{ipc::type::UInt64}, Finalize)); cls->register_function(std::make_shared("Update", std::vector{ipc::type::UInt64, ipc::type::String}, Update)); cls->register_function(std::make_shared("GetProperties", std::vector{ipc::type::UInt64}, GetProperties)); cls->register_function(std::make_shared("GetSettings", std::vector{ipc::type::UInt64}, GetSettings)); @@ -81,7 +82,7 @@ void osn::VideoEncoder::GetName(void *data, const int64_t id, const std::vector< { obs_encoder_t *encoder = osn::VideoEncoder::Manager::GetInstance().find(args[0].value_union.ui64); if (!encoder) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Video encoder reference is not valid."); + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "GetName. Video encoder reference is not valid."); } const char *name = obs_encoder_get_name(encoder); @@ -94,7 +95,7 @@ void osn::VideoEncoder::SetName(void *data, const int64_t id, const std::vector< { obs_encoder_t *encoder = osn::VideoEncoder::Manager::GetInstance().find(args[0].value_union.ui64); if (!encoder) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Video encoder reference is not valid."); + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "SetName. Video encoder reference is not valid."); } std::string name = args[1].value_str; @@ -107,7 +108,7 @@ void osn::VideoEncoder::GetType(void *data, const int64_t id, const std::vector< { obs_encoder_t *encoder = osn::VideoEncoder::Manager::GetInstance().find(args[0].value_union.ui64); if (!encoder) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Video encoder reference is not valid."); + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "GetType. Video encoder reference is not valid."); } uint32_t type = (uint32_t)obs_encoder_get_type(encoder); @@ -120,7 +121,7 @@ void osn::VideoEncoder::GetActive(void *data, const int64_t id, const std::vecto { obs_encoder_t *encoder = osn::VideoEncoder::Manager::GetInstance().find(args[0].value_union.ui64); if (!encoder) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Video encoder reference is not valid."); + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "GetActive. Video encoder reference is not valid."); } bool active = obs_encoder_active(encoder); @@ -133,7 +134,7 @@ void osn::VideoEncoder::GetId(void *data, const int64_t id, const std::vector &args, std::vector &rval) { obs_encoder_t *encoder = osn::VideoEncoder::Manager::GetInstance().find(args[0].value_union.ui64); + blog(LOG_INFO, "Release encoder %p, %d", encoder, args[0].value_union.ui64); if (!encoder) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Video encoder reference is not valid."); + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Release. Video encoder reference is not valid."); } obs_encoder_release(encoder); - encoder = nullptr; + osn::VideoEncoder::Manager::GetInstance().free(encoder); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + +void osn::VideoEncoder::Finalize(void *data, const int64_t id, const std::vector &args, std::vector &rval) +{ + obs_encoder_t *encoder = osn::VideoEncoder::Manager::GetInstance().find(args[0].value_union.ui64); + blog(LOG_INFO, "Finalize encoder %p, %d", encoder, args[0].value_union.ui64); + if (!encoder) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Finalize. Video encoder reference is not valid."); + } + + obs_encoder_release(encoder); + osn::VideoEncoder::Manager::GetInstance().free(encoder); + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; } @@ -172,7 +190,7 @@ void osn::VideoEncoder::Update(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetLastError(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void Release(void *data, const int64_t id, const std::vector &args, std::vector &rval); + static void Finalize(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void Update(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetProperties(void *data, const int64_t id, const std::vector &args, std::vector &rval); static void GetSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval); diff --git a/package.json b/package.json index f82886171..ec1bd4b9c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "local:config": "yarn install && git submodule update --init --recursive --force && cmake -Bbuild -H. -G\"Visual Studio 16 2019\" -A\"x64\" -DCMAKE_INSTALL_PREFIX=\"./obs-studio-node\" -DLIBOBS_BUILD_TYPE=\"debug\" -DCMAKE_PREFIX_PATH=%CD%/build/libobs-src/cmake/", "local:build": "cmake --build build --target install --config Debug", "local:clean": "rm -rf build/*", - "test": "electron-mocha -t 80000 -c true -r ts-node/register tests/osn-tests/src/**/*.ts --reporter tests/osn-tests/util/list-reporter.js" + "test": "electron-mocha -t 80000 --js-flags=\"--expose-gc\" -c true -r ts-node/register tests/osn-tests/src/**/*.ts --reporter tests/osn-tests/util/list-reporter.js" }, "devDependencies": { "@types/chai": "^4.1.7", @@ -35,6 +35,7 @@ "colors": "^1.4.0", "electron": "29.4.3", "electron-mocha": "^12.1.0", + "get-func-name": "^3.0.0", "mocha": "^7.1.0", "mocha-junit-reporter": "^1.22.0", "node-addon-api": "^7.1.1", @@ -45,7 +46,6 @@ "typedoc-plugin-markdown": "^2.2.17", "typescript": "^4.0.7", "uuid": "^3.0.1", - "get-func-name": "^3.0.0", "wait-queue": "^1.1.4" }, "keywords": [ @@ -68,6 +68,5 @@ "audio", "mixer", "beam" - ], - "dependencies": {} + ] } diff --git a/tests/osn-tests/media/bigbuckbunny.mp4 b/tests/osn-tests/media/bigbuckbunny.mp4 new file mode 100644 index 000000000..c70addbfd Binary files /dev/null and b/tests/osn-tests/media/bigbuckbunny.mp4 differ diff --git a/tests/osn-tests/src/test_audio_encoder.ts b/tests/osn-tests/src/test_audio_encoder.ts index 1de67cdf0..4633c8250 100644 --- a/tests/osn-tests/src/test_audio_encoder.ts +++ b/tests/osn-tests/src/test_audio_encoder.ts @@ -43,9 +43,9 @@ describe(testName, () => { }); it('Create an audio encoder', () => { - const encoder = osn.AudioEncoderFactory.create(); + const encoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-test-1"); expect(encoder).to.not.equal(undefined, 'Invalid audio encoder creation'); - expect(encoder.name).to.equal('audio', "Invalid default name value"); + expect(encoder.name).to.equal('audio-encoder-test-1', "Invalid default name value"); expect(encoder.bitrate).to.equal(128, "Invalid default bitrate value"); encoder.name = 'audio track'; @@ -53,5 +53,7 @@ describe(testName, () => { expect(encoder.name).to.equal('audio track', "Invalid name value"); expect(encoder.bitrate).to.equal(256, "Invalid bitrate value"); + + encoder.release(); }); }); diff --git a/tests/osn-tests/src/test_osn_advanced_recording.ts b/tests/osn-tests/src/test_osn_advanced_recording.ts index 59a3a56f1..059848e9d 100644 --- a/tests/osn-tests/src/test_osn_advanced_recording.ts +++ b/tests/osn-tests/src/test_osn_advanced_recording.ts @@ -82,7 +82,7 @@ describe(testName, () => { recording.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording.format = ERecordingFormat.MOV; recording.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-adv-recording-1'); recording.overwrite = true; recording.noSpace = false; recording.video = obs.defaultVideoContext; @@ -111,7 +111,9 @@ describe(testName, () => { expect(recording.useStreamEncoders).to.equal( false, "Invalid useStreamEncoders default value"); + const videoEncoder = recording.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording); + videoEncoder.release(); }); it('Start advanced recording - Stream', async function () { @@ -128,7 +130,7 @@ describe(testName, () => { recording.useStreamEncoders = true; const stream = osn.AdvancedStreamingFactory.create(); stream.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-adv-streaming-1'); stream.service = osn.ServiceFactory.legacySettings; stream.video = obs.defaultVideoContext; stream.signalHandler = (signal) => {obs.signals.push(signal)}; @@ -243,8 +245,10 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + const videoEncoder = stream.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording); osn.AdvancedStreamingFactory.destroy(stream); + videoEncoder.release(); }); it('Start advanced recording - Custom encoders', async function () { @@ -256,7 +260,7 @@ describe(testName, () => { recording.format = ERecordingFormat.MP4; recording.useStreamEncoders = false; recording.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-adv-recording-2'); recording.overwrite = false; recording.noSpace = false; recording.video = obs.defaultVideoContext; @@ -316,6 +320,8 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Wrote, GetErrorMessage(ETestErrorMsg.RecordingOutput)); + const videoEncoder = recording.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording); + videoEncoder.release(); }); }); diff --git a/tests/osn-tests/src/test_osn_advanced_replayBuffer.ts b/tests/osn-tests/src/test_osn_advanced_replayBuffer.ts index a20737460..582cfba9a 100644 --- a/tests/osn-tests/src/test_osn_advanced_replayBuffer.ts +++ b/tests/osn-tests/src/test_osn_advanced_replayBuffer.ts @@ -131,7 +131,7 @@ describe(testName, () => { recording.useStreamEncoders = false; recording.video = obs.defaultVideoContext; recording.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-adv-recording-1'); const track1 = osn.AudioTrackFactory.create(160, 'track1'); osn.AudioTrackFactory.setAtIndex(track1, 1); recording.overwrite = false; @@ -239,8 +239,10 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Wrote, GetErrorMessage(ETestErrorMsg.RecordingOutput)); + const streamEncoder = recording.videoEncoder; osn.AdvancedReplayBufferFactory.destroy(replayBuffer); osn.AdvancedRecordingFactory.destroy(recording); + streamEncoder.release(); }); it('Start advanced replay buffer - Use Stream through Recording', async function() { @@ -264,14 +266,14 @@ describe(testName, () => { recording.useStreamEncoders = true; recording.overwrite = false; recording.noSpace = false; - recording .video = obs.defaultVideoContext; + recording.video = obs.defaultVideoContext; recording.useStreamEncoders = true; recording.signalHandler = (signal) => {obs.signals.push(signal)}; const stream = osn.AdvancedStreamingFactory.create(); stream.video = obs.defaultVideoContext; stream.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-adv-stream-1'); stream.service = osn.ServiceFactory.legacySettings; const track1 = osn.AudioTrackFactory.create(160, 'track1'); osn.AudioTrackFactory.setAtIndex(track1, 1); @@ -433,8 +435,10 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + const videoEncoder = stream.videoEncoder; osn.AdvancedReplayBufferFactory.destroy(replayBuffer); osn.AdvancedRecordingFactory.destroy(recording); osn.AdvancedStreamingFactory.destroy(stream); + videoEncoder.release(); }); }); diff --git a/tests/osn-tests/src/test_osn_advanced_streaming.ts b/tests/osn-tests/src/test_osn_advanced_streaming.ts index 8c1ffea18..1586e29c4 100644 --- a/tests/osn-tests/src/test_osn_advanced_streaming.ts +++ b/tests/osn-tests/src/test_osn_advanced_streaming.ts @@ -6,13 +6,17 @@ import { ETestErrorMsg, GetErrorMessage } from '../util/error_messages'; import { OBSHandler } from '../util/obs_handler' import { deleteConfigFiles, sleep } from '../util/general'; import { EOBSInputTypes, EOBSOutputSignal, EOBSOutputType } from '../util/obs_enums'; -import { EFPSType } from '../osn'; +import * as inputSettings from '../util/input_settings'; + +import path = require('path'); const testName = 'osn-advanced-streaming'; describe(testName, () => { let obs: OBSHandler; let hasTestFailed: boolean = false; + const mediaPath = path.join(path.normalize(__dirname), '..', 'media'); + // Initialize OBS process before(async() => { logInfo(testName, 'Starting ' + testName + ' tests'); @@ -95,13 +99,70 @@ describe(testName, () => { osn.AdvancedStreamingFactory.destroy(stream); }); + it('Stream with missing video encoder', async function() { + if (obs.isDarwin()) { + this.skip(); + } + const stream = osn.AdvancedStreamingFactory.create(); + stream.service = osn.ServiceFactory.legacySettings; + stream.video = obs.defaultVideoContext; + const track1 = osn.AudioTrackFactory.create(160, 'track1'); + osn.AudioTrackFactory.setAtIndex(track1, 1); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + expect(() => { + stream.start(); + }).throw('Invalid video encoder'); + + + osn.AdvancedStreamingFactory.destroy(stream); + }); + + it('Stream with missing service', async function() { + if (obs.isDarwin()) { + this.skip(); + } + const stream = osn.AdvancedStreamingFactory.create(); + stream.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + stream.video = obs.defaultVideoContext; + const track1 = osn.AudioTrackFactory.create(160, 'track1'); + osn.AudioTrackFactory.setAtIndex(track1, 1); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + expect(() => { + stream.start(); + }).throw('Invalid service'); + + + osn.AdvancedStreamingFactory.destroy(stream); + }); + + it('Stream with missing canvas', async function() { + if (obs.isDarwin()) { + this.skip(); + } + const stream = osn.AdvancedStreamingFactory.create(); + stream.service = osn.ServiceFactory.legacySettings; + stream.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + const track1 = osn.AudioTrackFactory.create(160, 'track1'); + osn.AudioTrackFactory.setAtIndex(track1, 1); + + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + expect(() => { + stream.start(); + }).throw('Invalid main canvas'); + + osn.AdvancedStreamingFactory.destroy(stream); + }); + it('Start streaming', async function() { if (obs.isDarwin()) { this.skip(); } const stream = osn.AdvancedStreamingFactory.create(); stream.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-adv-streaming-1'); stream.service = osn.ServiceFactory.legacySettings; stream.delay = osn.DelayFactory.create(); @@ -180,7 +241,9 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + const videoEncoder = stream.videoEncoder; osn.AdvancedStreamingFactory.destroy(stream); + videoEncoder.release(); }); it('Stream with invalid stream key', async function() { @@ -189,7 +252,7 @@ describe(testName, () => { } const stream = osn.AdvancedStreamingFactory.create(); stream.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-adv-streaming-2'); stream.service = osn.ServiceFactory.legacySettings; stream.service.update({ key: 'invalid' }); stream.delay = @@ -218,10 +281,12 @@ describe(testName, () => { EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); expect(signalInfo.signal).to.equal( EOBSOutputSignal.Stop, GetErrorMessage(ETestErrorMsg.StreamOutput)); - expect(signalInfo.code).to.equal(-3, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.code).to.equal(-3, GetErrorMessage(ETestErrorMsg.StreamOutput)); stream.service.update({ key: obs.userStreamKey }); + const streamingEncoder = stream.videoEncoder; osn.AdvancedStreamingFactory.destroy(stream); + streamingEncoder.release(); }); }); diff --git a/tests/osn-tests/src/test_osn_dual_output.ts b/tests/osn-tests/src/test_osn_dual_output.ts index a145b0b5d..108fd1c85 100644 --- a/tests/osn-tests/src/test_osn_dual_output.ts +++ b/tests/osn-tests/src/test_osn_dual_output.ts @@ -123,7 +123,7 @@ describe(testName, () => { recording.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording.format = ERecordingFormat.MP4; recording.useStreamEncoders = false; - recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-1'); recording.overwrite = false; recording.noSpace = false; recording.video = obs.defaultVideoContext; @@ -135,7 +135,7 @@ describe(testName, () => { recording2.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording2.format = ERecordingFormat.MP4; recording2.useStreamEncoders = false; - recording2.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + recording2.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-2'); recording2.overwrite = false; recording2.noSpace = false; recording2.video = secondContext; @@ -161,9 +161,13 @@ describe(testName, () => { await handleStreamSignals(EOBSOutputType.Recording, EOBSOutputSignal.Stop, ETestErrorMsg.RecordingOutput); await handleStreamSignals(EOBSOutputType.Recording, EOBSOutputSignal.Wrote, ETestErrorMsg.RecordingOutput); + const recordingEncoder = recording.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording); + recordingEncoder.release(); + const recording2Encoder = recording2.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording2); + recording2Encoder.release(); }); it('Start Dual Output with recording and scene items', async function() { @@ -176,7 +180,7 @@ describe(testName, () => { recording.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording.format = ERecordingFormat.MP4; recording.useStreamEncoders = false; - recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-3'); recording.overwrite = false; recording.noSpace = false; recording.video = obs.defaultVideoContext; @@ -214,7 +218,7 @@ describe(testName, () => { recording2.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording2.format = ERecordingFormat.MP4; recording2.useStreamEncoders = false; - recording2.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + recording2.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-4'); recording2.overwrite = false; recording2.noSpace = false; recording2.video = secondContext; @@ -240,9 +244,13 @@ describe(testName, () => { await handleStreamSignals(EOBSOutputType.Recording, EOBSOutputSignal.Stop, ETestErrorMsg.RecordingOutput); await handleStreamSignals(EOBSOutputType.Recording, EOBSOutputSignal.Wrote, ETestErrorMsg.RecordingOutput); + const recordingEncoder = recording.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording); + recordingEncoder.release(); + const recording2Encoder = recording2.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording2); + recording2Encoder.release(); osn.Global.setOutputSource(0, returnSource); @@ -262,7 +270,7 @@ describe(testName, () => { recording.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording.format = ERecordingFormat.MP4; recording.useStreamEncoders = false; - recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-1'); + recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-5'); recording.overwrite = false; recording.noSpace = false; recording.mixer = 1; @@ -275,7 +283,7 @@ describe(testName, () => { recording2.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording2.format = ERecordingFormat.MP4; recording2.useStreamEncoders = false; - recording2.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-2'); + recording2.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-6'); recording2.overwrite = false; recording2.noSpace = false; recording2.mixer = 2; @@ -335,12 +343,17 @@ describe(testName, () => { await handleStreamSignals(EOBSOutputType.Recording, EOBSOutputSignal.Stop, ETestErrorMsg.RecordingOutput); await handleStreamSignals(EOBSOutputType.Recording, EOBSOutputSignal.Wrote, ETestErrorMsg.RecordingOutput); + const recordingEncoder = recording.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording); + const recording2Encoder = recording2.videoEncoder; osn.AdvancedRecordingFactory.destroy(recording2); osn.Global.setOutputSource(0, returnSource); + recordingEncoder.release(); + recording2Encoder.release(); + sceneItem1.source.release(); sceneItem1.remove(); @@ -360,8 +373,8 @@ describe(testName, () => { const recording = osn.SimpleRecordingFactory.create(); recording.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording.format = ERecordingFormat.MP4; - recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-1'); - recording.audioEncoder = osn.AudioEncoderFactory.create(); + recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-7'); + recording.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-test-recording-1"); recording.audioEncoder.name = 'audio-encoder-test-recording-1'; recording.audioEncoder.bitrate = 160; recording.overwrite = false; @@ -375,8 +388,8 @@ describe(testName, () => { const recording2 = osn.SimpleRecordingFactory.create(); recording2.path = path.join(path.normalize(__dirname), '..', 'osnData'); recording2.format = ERecordingFormat.MP4; - recording2.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-2'); - recording2.audioEncoder = osn.AudioEncoderFactory.create(); + recording2.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-test-recording-8'); + recording2.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-test-recording-2"); recording2.audioEncoder.name = 'audio-encoder-test-recording-2'; recording2.audioEncoder.bitrate = 160; recording2.overwrite = false; @@ -438,8 +451,12 @@ describe(testName, () => { await handleStreamSignals(EOBSOutputType.Recording, EOBSOutputSignal.Stop, ETestErrorMsg.RecordingOutput); await handleStreamSignals(EOBSOutputType.Recording, EOBSOutputSignal.Wrote, ETestErrorMsg.RecordingOutput); + const recordingEncoder = recording.videoEncoder; + const recordingAudioEncoder = recording.audioEncoder; osn.SimpleRecordingFactory.destroy(recording); + const recording2Encoder = recording2.videoEncoder; + const recording2AudioEncoder = recording2.audioEncoder; osn.SimpleRecordingFactory.destroy(recording2); osn.Global.setOutputSource(0, returnSource); @@ -451,6 +468,11 @@ describe(testName, () => { sceneItem2.remove(); scene.release(); + + recordingEncoder.release(); + recording2Encoder.release(); + recordingAudioEncoder.release(); + recording2AudioEncoder.release(); }); it('Start Dual Output with legacy streaming to two services', async function() { diff --git a/tests/osn-tests/src/test_osn_enhanced_broadcasting_advanced_streaming.ts b/tests/osn-tests/src/test_osn_enhanced_broadcasting_advanced_streaming.ts new file mode 100644 index 000000000..127ce4cb5 --- /dev/null +++ b/tests/osn-tests/src/test_osn_enhanced_broadcasting_advanced_streaming.ts @@ -0,0 +1,282 @@ +import 'mocha' +import { expect } from 'chai' +import * as osn from '../osn'; +import { logInfo, logEmptyLine } from '../util/logger'; +import { ETestErrorMsg, GetErrorMessage } from '../util/error_messages'; +import { OBSHandler } from '../util/obs_handler' +import { deleteConfigFiles, sleep } from '../util/general'; +import { EOBSInputTypes, EOBSOutputSignal, EOBSOutputType } from '../util/obs_enums'; +import * as inputSettings from '../util/input_settings'; + +import path = require('path'); + +const testName = 'osn-enhanced-broadcasting-advanced-streaming'; + +describe(testName, () => { + let obs: OBSHandler; + let hasTestFailed: boolean = false; + const mediaPath = path.join(path.normalize(__dirname), '..', 'media'); + let secondContext: osn.IVideo = null; + + // Initialize OBS process + before(async() => { + logInfo(testName, 'Starting ' + testName + ' tests'); + deleteConfigFiles(); + obs = new OBSHandler(testName); + obs.instantiateUserPool(testName); + + // Reserving user from pool + await obs.reserveUser(); + + secondContext = osn.VideoFactory.create(); + const secondVideoInfo: osn.IVideoInfo = { + fpsNum: 60, + fpsDen: 2, + baseWidth: 720, + baseHeight: 1280, + outputWidth: 720, + outputHeight: 1280, + outputFormat: osn.EVideoFormat.NV12, + colorspace: osn.EColorSpace.CS709, + range: osn.ERangeType.Full, + scaleType: osn.EScaleType.Lanczos, + fpsType: osn.EFPSType.Fractional + }; + secondContext.video = secondVideoInfo; + }); + + // Shutdown OBS process + after(async function() { + // Releasing user got from pool + await obs.releaseUser(); + + secondContext.destroy(); + obs.shutdown(); + + if (hasTestFailed === true) { + logInfo(testName, 'One or more test cases failed. Uploading cache'); + await obs.uploadTestCache(); + } + + obs = null; + deleteConfigFiles(); + logInfo(testName, 'Finished ' + testName + ' tests'); + logEmptyLine(); + }); + + afterEach(function() { + if (this.currentTest.state == 'failed') { + hasTestFailed = true; + } + }); + + // TODO: more tests: + // - vertical primary canvas + + it('Enhanced Broadcasting Advanced Streaming Single Canvas', async function() { + if (obs.isDarwin()) { + this.skip(); + } + + if (obs.isCI()) { + // Skipping this test because CI server doesn't have GPU, but you can run it locally + this.skip(); + } + + const stream = osn.EnhancedBroadcastingAdvancedStreamingFactory.create(); + expect(stream).to.not.be.null; + stream.service = osn.ServiceFactory.legacySettings; + // Note: no video encoder set, because it is automatically created by the enhanced broadcasting + stream.delay = osn.DelayFactory.create(); + stream.reconnect = osn.ReconnectFactory.create(); + stream.network = osn.NetworkFactory.create(); + stream.video = obs.defaultVideoContext; + const track1 = osn.AudioTrackFactory.create(160, 'track1'); + osn.AudioTrackFactory.setAtIndex(track1, 1); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + stream.start(); + + let signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Starting); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Starting, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Activate); + + if (signalInfo.signal == EOBSOutputSignal.Stop) { + throw Error(GetErrorMessage(ETestErrorMsg.StreamOutputDidNotStart, signalInfo.code.toString())); + } + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Activate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Start); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Start, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + // Scene setup + const scene = osn.SceneFactory.create('my_scene'); + expect(scene).to.not.be.null; + osn.Global.setOutputSource(0, scene); + + let settings = inputSettings.ffmpegSource; + settings['volume'] = 100; + settings['local_file'] = path.join(mediaPath, "bigbuckbunny.mp4"); + settings['looping'] = true; + const videoSource = osn.InputFactory.create(EOBSInputTypes.FFMPEGSource, 'video_source', settings); + expect(videoSource).to.not.be.null; + + const sceneItem = scene.add(videoSource); + expect(sceneItem).to.not.be.null; + + sceneItem.video = obs.defaultVideoContext; + sceneItem.visible = true; + sceneItem.position = { x: 0, y: 0 }; + + await sleep(1 * 1000); + + expect(stream.droppedFrames).to.not.equal(undefined, "Undefined droppedFrames"); + expect(stream.totalFrames).to.not.equal(undefined, "Undefined totalFrames"); + expect(stream.kbitsPerSec).to.not.equal(undefined, "Undefined kbitsPerSec"); + expect(stream.dataOutput).to.not.equal(undefined, "Undefined dataOutput"); + + stream.stop(); + + // Scene cleanup + osn.Global.setOutputSource(0, null); + sceneItem.remove(); + videoSource.release(); + scene.release(); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Stopping); + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Stopping, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Stop); + + if (signalInfo.code != 0) { + throw Error(GetErrorMessage( + ETestErrorMsg.StreamOutputStoppedWithError, + signalInfo.code.toString(), signalInfo.error)); + } + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Stop, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Deactivate); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + osn.EnhancedBroadcastingAdvancedStreamingFactory.destroy(stream); + }); + + it('Enhanced Broadcasting Advanced Streaming Dual Canvas', async function() { + if (obs.isDarwin()) { + this.skip(); + } + + if (obs.isCI()) { + // Skipping this test because CI server doesn't have GPU, but you can run it locally + this.skip(); + } + + const stream = osn.EnhancedBroadcastingAdvancedStreamingFactory.create(); + expect(stream).to.not.be.null; + stream.service = osn.ServiceFactory.legacySettings; + // Note: no video encoder set, because it is automatically created by the enhanced broadcasting + stream.delay = osn.DelayFactory.create(); + stream.reconnect = osn.ReconnectFactory.create(); + stream.network = osn.NetworkFactory.create(); + stream.video = obs.defaultVideoContext; + stream.additionalVideo = secondContext; + const track1 = osn.AudioTrackFactory.create(160, 'track1'); + osn.AudioTrackFactory.setAtIndex(track1, 1); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + stream.start(); + + let signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Starting); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Starting, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Activate); + + if (signalInfo.signal == EOBSOutputSignal.Stop) { + throw Error(GetErrorMessage(ETestErrorMsg.StreamOutputDidNotStart, signalInfo.code.toString())); + } + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Activate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Start); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Start, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + // Scene setup + const scene = osn.SceneFactory.create('my_scene'); + expect(scene).to.not.be.null; + osn.Global.setOutputSource(0, scene); + + let settings = inputSettings.ffmpegSource; + settings['volume'] = 100; + settings['local_file'] = path.join(mediaPath, "bigbuckbunny.mp4"); + settings['looping'] = true; + const videoSource = osn.InputFactory.create(EOBSInputTypes.FFMPEGSource, 'video_source', settings); + expect(videoSource).to.not.be.null; + + // Item on the horizontal canvas + const sceneItem1 = scene.add(videoSource); + expect(sceneItem1).to.not.be.null; + sceneItem1.video = obs.defaultVideoContext; + sceneItem1.visible = true; + sceneItem1.position = { x: 0, y: 0 }; + + // Item on the vertical canvas + const sceneItem2 = scene.add(videoSource); + expect(sceneItem2).to.not.be.null; + sceneItem2.video = secondContext; + sceneItem2.visible = true; + sceneItem2.position = { x: 20, y: 20 }; + sceneItem2.scale = {x: 0.5, y: 0.5}; + + await sleep(1 * 1000); + + expect(stream.droppedFrames).to.not.equal(undefined, "Undefined droppedFrames"); + expect(stream.totalFrames).to.not.equal(undefined, "Undefined totalFrames"); + expect(stream.kbitsPerSec).to.not.equal(undefined, "Undefined kbitsPerSec"); + expect(stream.dataOutput).to.not.equal(undefined, "Undefined dataOutput"); + + stream.stop(); + + // Scene cleanup + osn.Global.setOutputSource(0, null); + sceneItem1.remove(); + sceneItem2.remove(); + videoSource.release(); + scene.release(); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Stopping); + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Stopping, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Stop); + + if (signalInfo.code != 0) { + throw Error(GetErrorMessage( + ETestErrorMsg.StreamOutputStoppedWithError, + signalInfo.code.toString(), signalInfo.error)); + } + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Stop, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Deactivate); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + osn.EnhancedBroadcastingAdvancedStreamingFactory.destroy(stream); + }); +}); + diff --git a/tests/osn-tests/src/test_osn_enhanced_broadcasting_simple_streaming.ts b/tests/osn-tests/src/test_osn_enhanced_broadcasting_simple_streaming.ts new file mode 100644 index 000000000..4223bc061 --- /dev/null +++ b/tests/osn-tests/src/test_osn_enhanced_broadcasting_simple_streaming.ts @@ -0,0 +1,276 @@ +import 'mocha' +import { expect } from 'chai' +import * as osn from '../osn'; +import { logInfo, logEmptyLine } from '../util/logger'; +import { ETestErrorMsg, GetErrorMessage } from '../util/error_messages'; +import { OBSHandler } from '../util/obs_handler' +import { deleteConfigFiles, sleep } from '../util/general'; +import { EOBSInputTypes, EOBSOutputSignal, EOBSOutputType } from '../util/obs_enums'; +import * as inputSettings from '../util/input_settings'; + +import path = require('path'); + +const testName = 'osn-enhanced-broadcasting-simple-streaming'; + +describe(testName, () => { + let obs: OBSHandler; + let hasTestFailed: boolean = false; + const mediaPath = path.join(path.normalize(__dirname), '..', 'media'); + let secondContext: osn.IVideo = null; + + // Initialize OBS process + before(async() => { + logInfo(testName, 'Starting ' + testName + ' tests'); + deleteConfigFiles(); + obs = new OBSHandler(testName); + + obs.instantiateUserPool(testName); + + // Reserving user from pool + await obs.reserveUser(); + + secondContext = osn.VideoFactory.create(); + const secondVideoInfo: osn.IVideoInfo = { + fpsNum: 60, + fpsDen: 2, + baseWidth: 720, + baseHeight: 1280, + outputWidth: 720, + outputHeight: 1280, + outputFormat: osn.EVideoFormat.NV12, + colorspace: osn.EColorSpace.CS709, + range: osn.ERangeType.Full, + scaleType: osn.EScaleType.Lanczos, + fpsType: osn.EFPSType.Fractional + }; + secondContext.video = secondVideoInfo; + }); + + // Shutdown OBS process + after(async function() { + // Releasing user got from pool + await obs.releaseUser(); + + secondContext.destroy(); + obs.shutdown(); + + if (hasTestFailed === true) { + logInfo(testName, 'One or more test cases failed. Uploading cache'); + await obs.uploadTestCache(); + } + + obs = null; + deleteConfigFiles(); + logInfo(testName, 'Finished ' + testName + ' tests'); + logEmptyLine(); + }); + + afterEach(function() { + if (this.currentTest.state == 'failed') { + hasTestFailed = true; + } + }); + + it('Enhanced Broadcasting Simple Streaming Single Canvas', async function() { + if (obs.isDarwin()) { + this.skip(); + } + + if (obs.isCI()) { + // Skipping this test because CI server doesn't have GPU, but you can run it locally + this.skip(); + } + + const stream = osn.EnhancedBroadcastingSimpleStreamingFactory.create(); + // Note: no video encoder set, because it is automatically created by the enhanced broadcasting + stream.service = osn.ServiceFactory.legacySettings; + stream.delay = osn.DelayFactory.create(); + stream.reconnect = osn.ReconnectFactory.create(); + stream.network = osn.NetworkFactory.create(); + stream.video = obs.defaultVideoContext; + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-1"); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + stream.start(); + + let signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Starting); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Starting, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Activate); + + if (signalInfo.signal == EOBSOutputSignal.Stop) { + throw Error(GetErrorMessage( + ETestErrorMsg.StreamOutputDidNotStart, signalInfo.code.toString(), signalInfo.error)); + } + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Activate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Start); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Start, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + // Scene setup + const scene = osn.SceneFactory.create('my_scene'); + expect(scene).to.not.be.null; + osn.Global.setOutputSource(0, scene); + + let settings = inputSettings.ffmpegSource; + settings['volume'] = 100; + settings['local_file'] = path.join(mediaPath, "bigbuckbunny.mp4"); + settings['looping'] = true; + const videoSource = osn.InputFactory.create(EOBSInputTypes.FFMPEGSource, 'video_source', settings); + expect(videoSource).to.not.be.null; + + const sceneItem = scene.add(videoSource); + expect(sceneItem).to.not.be.null; + sceneItem.video = obs.defaultVideoContext; + sceneItem.visible = true; + sceneItem.position = { x: 0, y: 0 }; + + await sleep(1 * 1000); + + expect(stream.droppedFrames).to.not.equal(undefined, "Undefined droppedFrames"); + expect(stream.totalFrames).to.not.equal(undefined, "Undefined totalFrames"); + expect(stream.kbitsPerSec).to.not.equal(undefined, "Undefined kbitsPerSec"); + expect(stream.dataOutput).to.not.equal(undefined, "Undefined dataOutput"); + + stream.stop(); + + // Scene cleanup + osn.Global.setOutputSource(0, null); + sceneItem.remove(); + videoSource.release(); + scene.release(); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Stopping); + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Stopping, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Stop); + + if (signalInfo.code != 0) { + throw Error(GetErrorMessage( + ETestErrorMsg.StreamOutputStoppedWithError, + signalInfo.code.toString(), signalInfo.error)); + } + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Stop, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Deactivate); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + osn.EnhancedBroadcastingSimpleStreamingFactory.destroy(stream); + }); + + it('Enhanced Broadcasting Simple Streaming Dual Canvas', async function() { + if (obs.isDarwin()) { + this.skip(); + } + + if (obs.isCI()) { + // Skipping this test because CI server doesn't have GPU, but you can run it locally + this.skip(); + } + + const stream = osn.EnhancedBroadcastingSimpleStreamingFactory.create(); + expect(stream).to.not.be.null; + stream.service = osn.ServiceFactory.legacySettings; + // Note: no video encoder set, because it is automatically created by the enhanced broadcasting + stream.delay = osn.DelayFactory.create(); + stream.reconnect = osn.ReconnectFactory.create(); + stream.network = osn.NetworkFactory.create(); + stream.video = obs.defaultVideoContext; + stream.additionalVideo = secondContext; + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-2"); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + stream.start(); + + let signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Starting); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Starting, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Activate); + + if (signalInfo.signal == EOBSOutputSignal.Stop) { + throw Error(GetErrorMessage(ETestErrorMsg.StreamOutputDidNotStart, signalInfo.code.toString())); + } + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Activate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Start); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Start, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + // Scene setup + const scene = osn.SceneFactory.create('my_scene'); + expect(scene).to.not.be.null; + osn.Global.setOutputSource(0, scene); + + let settings = inputSettings.ffmpegSource; + settings['volume'] = 100; + settings['local_file'] = path.join(mediaPath, "bigbuckbunny.mp4"); + settings['looping'] = true; + const videoSource = osn.InputFactory.create(EOBSInputTypes.FFMPEGSource, 'video_source', settings); + expect(videoSource).to.not.be.null; + + // Item on the horizontal canvas + const sceneItem1 = scene.add(videoSource); + expect(sceneItem1).to.not.be.null; + sceneItem1.video = obs.defaultVideoContext; + sceneItem1.visible = true; + sceneItem1.position = { x: 0, y: 0 }; + + // Item on the vertical canvas + const sceneItem2 = scene.add(videoSource); + expect(sceneItem2).to.not.be.null; + sceneItem2.video = secondContext; + sceneItem2.visible = true; + sceneItem2.position = { x: 20, y: 20 }; + sceneItem2.scale = {x: 0.5, y: 0.5}; + + await sleep(1 * 1000); + + expect(stream.droppedFrames).to.not.equal(undefined, "Undefined droppedFrames"); + expect(stream.totalFrames).to.not.equal(undefined, "Undefined totalFrames"); + expect(stream.kbitsPerSec).to.not.equal(undefined, "Undefined kbitsPerSec"); + expect(stream.dataOutput).to.not.equal(undefined, "Undefined dataOutput"); + + stream.stop(); + + // Scene cleanup + osn.Global.setOutputSource(0, null); + sceneItem1.remove(); + sceneItem2.remove(); + videoSource.release(); + scene.release(); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Stopping); + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Stopping, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Stop); + + if (signalInfo.code != 0) { + throw Error(GetErrorMessage( + ETestErrorMsg.StreamOutputStoppedWithError, + signalInfo.code.toString(), signalInfo.error)); + } + + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Stop, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + signalInfo = await obs.getNextSignalInfo(EOBSOutputType.Streaming, EOBSOutputSignal.Deactivate); + expect(signalInfo.type).to.equal(EOBSOutputType.Streaming, GetErrorMessage(ETestErrorMsg.StreamOutput)); + expect(signalInfo.signal).to.equal(EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + + osn.EnhancedBroadcastingSimpleStreamingFactory.destroy(stream); + }); +}); diff --git a/tests/osn-tests/src/test_osn_simple_recording.ts b/tests/osn-tests/src/test_osn_simple_recording.ts index d427d5641..3be49b7d1 100644 --- a/tests/osn-tests/src/test_osn_simple_recording.ts +++ b/tests/osn-tests/src/test_osn_simple_recording.ts @@ -79,9 +79,9 @@ describe(testName, () => { recording.quality = ERecordingQuality.HighQuality; recording.video = obs.defaultVideoContext; recording.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-recording-1'); recording.lowCPU = true; - recording.audioEncoder = osn.AudioEncoderFactory.create(); + recording.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-recording-1"); recording.overwrite = true; recording.noSpace = false; @@ -98,7 +98,11 @@ describe(testName, () => { expect(recording.noSpace).to.equal( false, "Invalid noSpace value"); + const videoEncoder = recording.videoEncoder; + const audioEncoder = recording.audioEncoder; osn.SimpleRecordingFactory.destroy(recording); + videoEncoder.release(); + audioEncoder.release(); }); it('Start simple recording - Stream', async function () { @@ -118,9 +122,9 @@ describe(testName, () => { const stream = osn.SimpleStreamingFactory.create(); stream.video = obs.defaultVideoContext; stream.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-stream-1'); stream.service = osn.ServiceFactory.legacySettings; - stream.audioEncoder = osn.AudioEncoderFactory.create(); + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-1"); stream.signalHandler = (signal) => {obs.signals.push(signal)}; recording.streaming = stream; @@ -230,8 +234,12 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + const streamEncoder = stream.videoEncoder; + const audioEncoder = stream.audioEncoder; osn.SimpleRecordingFactory.destroy(recording); osn.SimpleStreamingFactory.destroy(stream); + streamEncoder.release(); + audioEncoder.release(); }); it('Start simple recording - HighQuality', async function () { @@ -244,9 +252,9 @@ describe(testName, () => { recording.quality = ERecordingQuality.HighQuality; recording.video = obs.defaultVideoContext; recording.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-recording-2'); recording.lowCPU = false; - recording.audioEncoder = osn.AudioEncoderFactory.create(); + recording.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-recording-2"); recording.overwrite = false; recording.noSpace = false; recording.signalHandler = (signal) => {obs.signals.push(signal)}; @@ -303,7 +311,147 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Wrote, GetErrorMessage(ETestErrorMsg.RecordingOutput)); + const videoEncoder = recording.videoEncoder; + const audioEncoder = recording.audioEncoder; osn.SimpleRecordingFactory.destroy(recording); + videoEncoder.release(); + audioEncoder.release(); + }); + + it('Start simple recording - mpegts', async function () { + if (obs.isDarwin()) { + this.skip(); + } + + const formats: ERecordingFormat[] = [ + ERecordingFormat.MP4, + ERecordingFormat.MOV, + ERecordingFormat.MKV, + ERecordingFormat.FLV, + ERecordingFormat.MPEGTS, + ERecordingFormat.HLS, + ]; + for (const format of formats) { + const recording = osn.SimpleRecordingFactory.create(); + + recording.path = path.join(path.normalize(__dirname), "..", "osnData"); + recording.format = format as ERecordingFormat; + recording.quality = ERecordingQuality.HighQuality; + recording.video = obs.defaultVideoContext; + recording.videoEncoder = osn.VideoEncoderFactory.create( + "obs_x264", + `video-encoder-recording-${format}` + ); + recording.lowCPU = false; + recording.audioEncoder = osn.AudioEncoderFactory.create( + "ffmpeg_aac", + `audio-encoder-simple-recording-${format}` + ); + recording.overwrite = false; + recording.noSpace = false; + recording.signalHandler = (signal) => obs.signals.push(signal); + + /* ---------- start ---------- */ + recording.start(); + + let signalInfo = await obs.getNextSignalInfo( + EOBSOutputType.Recording, + EOBSOutputSignal.Start + ); + + if (signalInfo.signal === EOBSOutputSignal.Stop) { + throw Error( + GetErrorMessage( + ETestErrorMsg.RecordOutputDidNotStart, + signalInfo.code.toString(), + signalInfo.error + ) + ); + } + + expect(signalInfo.type).to.equal( + EOBSOutputType.Recording, + GetErrorMessage(ETestErrorMsg.RecordingOutput) + ); + expect(signalInfo.signal).to.equal( + EOBSOutputSignal.Start, + GetErrorMessage(ETestErrorMsg.RecordingOutput) + ); + + await sleep(2500); + + /* ---------- stop ---------- */ + recording.stop(); + + signalInfo = await obs.getNextSignalInfo( + EOBSOutputType.Recording, + EOBSOutputSignal.Stopping + ); + + expect(signalInfo.type).to.equal( + EOBSOutputType.Recording, + GetErrorMessage(ETestErrorMsg.RecordingOutput) + ); + expect(signalInfo.signal).to.equal( + EOBSOutputSignal.Stopping, + GetErrorMessage(ETestErrorMsg.RecordingOutput) + ); + + signalInfo = await obs.getNextSignalInfo( + EOBSOutputType.Recording, + EOBSOutputSignal.Stop + ); + + if (signalInfo.code !== 0) { + throw Error( + GetErrorMessage( + ETestErrorMsg.RecordOutputStoppedWithError, + signalInfo.code.toString(), + signalInfo.error + ) + ); + } + + expect(signalInfo.type).to.equal( + EOBSOutputType.Recording, + GetErrorMessage(ETestErrorMsg.RecordingOutput) + ); + expect(signalInfo.signal).to.equal( + EOBSOutputSignal.Stop, + GetErrorMessage(ETestErrorMsg.RecordingOutput) + ); + + signalInfo = await obs.getNextSignalInfo( + EOBSOutputType.Recording, + EOBSOutputSignal.Wrote + ); + + if (signalInfo.code !== 0) { + throw Error( + GetErrorMessage( + ETestErrorMsg.RecordOutputStoppedWithError, + signalInfo.code.toString(), + signalInfo.error + ) + ); + } + + expect(signalInfo.type).to.equal( + EOBSOutputType.Recording, + GetErrorMessage(ETestErrorMsg.RecordingOutput) + ); + expect(signalInfo.signal).to.equal( + EOBSOutputSignal.Wrote, + GetErrorMessage(ETestErrorMsg.RecordingOutput) + ); + + // cleanup for this format + const videoEncoder = recording.videoEncoder; + const audioEncoder = recording.audioEncoder; + osn.SimpleRecordingFactory.destroy(recording); + videoEncoder.release(); + audioEncoder.release(); + } }); it('Start simple recording - HigherQuality', async function () { @@ -316,9 +464,9 @@ describe(testName, () => { recording.quality = ERecordingQuality.HigherQuality; recording.video = obs.defaultVideoContext; recording.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-recording-3'); recording.lowCPU = false; - recording.audioEncoder = osn.AudioEncoderFactory.create(); + recording.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-recording-1"); recording.overwrite = false; recording.noSpace = false; recording.signalHandler = (signal) => {obs.signals.push(signal)}; @@ -375,7 +523,11 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Wrote, GetErrorMessage(ETestErrorMsg.RecordingOutput)); + const videoEncoder = recording.videoEncoder; + const audioEncoder = recording.audioEncoder; osn.SimpleRecordingFactory.destroy(recording); + videoEncoder.release(); + audioEncoder.release(); }); it('Start simple recording - Lossless', async function () { @@ -471,8 +623,8 @@ describe(testName, () => { recording.format = ERecordingFormat.MP4; recording.quality = ERecordingQuality.HighQuality; recording.video = obs.defaultVideoContext; - recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-browser-rec', ); - recording.audioEncoder = osn.AudioEncoderFactory.create(); + recording.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-browser-rec'); + recording.audioEncoder = osn.AudioEncoderFactory.create('ffmpeg_aac', 'audio-encoder-browser-rec') recording.overwrite = true; recording.noSpace = false; recording.signalHandler = (sig) => obs.signals.push(sig); @@ -519,7 +671,12 @@ describe(testName, () => { } expect(sig.signal).to.equal(EOBSOutputSignal.Wrote,GetErrorMessage(ETestErrorMsg.RecordingOutput),); + const videoEncoder = recording.videoEncoder; + const audioEncoder = recording.audioEncoder; osn.SimpleRecordingFactory.destroy(recording); + videoEncoder.release(); + audioEncoder.release(); + browserInput.release(); sceneItem1.source.release(); sceneItem1.remove(); diff --git a/tests/osn-tests/src/test_osn_simple_replayBuffer.ts b/tests/osn-tests/src/test_osn_simple_replayBuffer.ts index 8cd527e89..58fe6daed 100644 --- a/tests/osn-tests/src/test_osn_simple_replayBuffer.ts +++ b/tests/osn-tests/src/test_osn_simple_replayBuffer.ts @@ -126,8 +126,8 @@ describe(testName, () => { recording.quality = osn.ERecordingQuality.HighQuality; recording.video = obs.defaultVideoContext; recording.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); - recording.audioEncoder = osn.AudioEncoderFactory.create(); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-simple-recording-1'); + recording.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-recording-1"); recording.lowCPU = false; recording.overwrite = false; recording.noSpace = false; @@ -185,8 +185,12 @@ describe(testName, () => { expect(expectedPrefix).to.equal(true, 'Wrong prefix when saving the simple replay buffer'); expect(expectedSuffix).to.equal(true, 'Wrong suffix when saving the simple replay buffer'); + const videoEncoder = recording.videoEncoder; + const audioEncoder = recording.audioEncoder; osn.SimpleReplayBufferFactory.destroy(replayBuffer); osn.SimpleRecordingFactory.destroy(recording); + videoEncoder.release(); + audioEncoder.release(); }); it('Start simple replay buffer - Use Stream through Recording', async function() { @@ -217,9 +221,9 @@ describe(testName, () => { const stream = osn.SimpleStreamingFactory.create(); stream.video = obs.defaultVideoContext; stream.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-simple-streaming-1'); stream.service = osn.ServiceFactory.legacySettings; - stream.audioEncoder = osn.AudioEncoderFactory.create(); + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-1"); stream.signalHandler = (signal) => {obs.signals.push(signal)}; recording.streaming = stream; @@ -377,8 +381,12 @@ describe(testName, () => { expect(expectedPrefix).to.equal(true, 'Wrong prefix when saving the simple replay buffer'); expect(expectedSuffix).to.equal(true, 'Wrong suffix when saving the simple replay buffer'); + const videoEncoder = stream.videoEncoder; + const audioEncoder = stream.audioEncoder; osn.SimpleReplayBufferFactory.destroy(replayBuffer); osn.SimpleRecordingFactory.destroy(recording); osn.SimpleStreamingFactory.destroy(stream); + videoEncoder.release(); + audioEncoder.release(); }); }); diff --git a/tests/osn-tests/src/test_osn_simple_streaming.ts b/tests/osn-tests/src/test_osn_simple_streaming.ts index 97471ddd0..10a8b188f 100644 --- a/tests/osn-tests/src/test_osn_simple_streaming.ts +++ b/tests/osn-tests/src/test_osn_simple_streaming.ts @@ -5,13 +5,16 @@ import { logInfo, logEmptyLine } from '../util/logger'; import { ETestErrorMsg, GetErrorMessage } from '../util/error_messages'; import { OBSHandler } from '../util/obs_handler' import { deleteConfigFiles, sleep } from '../util/general'; -import { EOBSInputTypes, EOBSOutputSignal, EOBSOutputType } from '../util/obs_enums'; +import { EOBSOutputSignal, EOBSOutputType } from '../util/obs_enums'; + +import path = require('path'); const testName = 'osn-simple-streaming'; describe(testName, () => { let obs: OBSHandler; let hasTestFailed: boolean = false; + const mediaPath = path.join(path.normalize(__dirname), '..', 'media'); // Initialize OBS process before(async() => { @@ -81,13 +84,84 @@ describe(testName, () => { osn.SimpleStreamingFactory.destroy(stream); }); + it('Stream with missing video encoder', async function() { + if (obs.isDarwin()) { + this.skip(); + } + const stream = osn.SimpleStreamingFactory.create(); + stream.service = osn.ServiceFactory.legacySettings; + stream.video = obs.defaultVideoContext; + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-1"); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + expect(() => { + stream.start(); + }).throw('Invalid video encoder'); + + + osn.SimpleStreamingFactory.destroy(stream); + }); + + it('Stream with missing audio encoder', async function() { + if (obs.isDarwin()) { + this.skip(); + } + const stream = osn.SimpleStreamingFactory.create(); + stream.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + stream.service = osn.ServiceFactory.legacySettings; + stream.video = obs.defaultVideoContext; + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + expect(() => { + stream.start(); + }).throw('Invalid audio encoder'); + + + osn.SimpleStreamingFactory.destroy(stream); + }); + + it('Stream with missing service', async function() { + if (obs.isDarwin()) { + this.skip(); + } + const stream = osn.SimpleStreamingFactory.create(); + stream.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + stream.video = obs.defaultVideoContext; + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-2"); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + expect(() => { + stream.start(); + }).throw('Invalid service'); + + + osn.SimpleStreamingFactory.destroy(stream); + }); + + it('Stream with missing canvas', async function() { + if (obs.isDarwin()) { + this.skip(); + } + const stream = osn.SimpleStreamingFactory.create(); + stream.videoEncoder = osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + stream.service = osn.ServiceFactory.legacySettings; + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-3"); + stream.signalHandler = (signal) => {obs.signals.push(signal)}; + + expect(() => { + stream.start(); + }).throw('Invalid main canvas'); + + osn.SimpleStreamingFactory.destroy(stream); + }); + it('Start streaming', async function() { if (obs.isDarwin()) { this.skip(); } const stream = osn.SimpleStreamingFactory.create(); stream.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-simple-streaming-1'); stream.service = osn.ServiceFactory.legacySettings; stream.delay = osn.DelayFactory.create(); @@ -96,7 +170,7 @@ describe(testName, () => { stream.network = osn.NetworkFactory.create(); stream.video = obs.defaultVideoContext; - stream.audioEncoder = osn.AudioEncoderFactory.create(); + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-4"); stream.signalHandler = (signal) => {obs.signals.push(signal)}; stream.start(); @@ -164,7 +238,11 @@ describe(testName, () => { expect(signalInfo.signal).to.equal( EOBSOutputSignal.Deactivate, GetErrorMessage(ETestErrorMsg.StreamOutput)); + const streamEncoder = stream.videoEncoder; + const audioEncoder = stream.audioEncoder; osn.SimpleStreamingFactory.destroy(stream); + streamEncoder.release(); + audioEncoder.release(); }); it('Stream with invalid stream key', async function() { @@ -173,7 +251,7 @@ describe(testName, () => { } const stream = osn.SimpleStreamingFactory.create(); stream.videoEncoder = - osn.VideoEncoderFactory.create('obs_x264', 'video-encoder'); + osn.VideoEncoderFactory.create('obs_x264', 'video-encoder-simple-streaming-2'); stream.service = osn.ServiceFactory.legacySettings; stream.service.update({ key: 'invalid' }); stream.delay = @@ -183,7 +261,7 @@ describe(testName, () => { stream.network = osn.NetworkFactory.create(); stream.video = obs.defaultVideoContext; - stream.audioEncoder = osn.AudioEncoderFactory.create(); + stream.audioEncoder = osn.AudioEncoderFactory.create("ffmpeg_aac", "audio-encoder-simple-streaming-5"); stream.signalHandler = (signal) => {obs.signals.push(signal)}; stream.start(); @@ -205,6 +283,10 @@ describe(testName, () => { stream.service.update({ key: obs.userStreamKey }); + const videoEncoder = stream.videoEncoder; + const audioEncoder = stream.audioEncoder; osn.SimpleStreamingFactory.destroy(stream); + videoEncoder.release(); + audioEncoder.release(); }); }); diff --git a/tests/osn-tests/src/test_osn_video_encoder.ts b/tests/osn-tests/src/test_osn_video_encoder.ts index 86976e44b..83aa1d9b8 100644 --- a/tests/osn-tests/src/test_osn_video_encoder.ts +++ b/tests/osn-tests/src/test_osn_video_encoder.ts @@ -47,7 +47,7 @@ describe(testName, () => { }); it('Create a video encoder', () => { - const encoder = osn.VideoEncoderFactory.create('obs_x264', "My x264", {}); + const encoder = osn.VideoEncoderFactory.create('obs_x264', "video-encoder-test-1", {}); expect(encoder).to.not.equal(undefined, 'Invalid x264 video encoder creation'); expect(encoder.active).to.equal(false, "Invalid active value"); expect(encoder.lastError).to.equal('', "Error while creating the video encoder"); @@ -96,10 +96,12 @@ describe(testName, () => { expect(propsArray[10].name).to.equal('repeat_headers', "Invalid repeat_headers name property"); expect(propsArray[10].value).to.equal(false, "Invalid repeat_headers value property"); + + encoder.release(); }); it('Update video encoder properties', () => { - const encoder = osn.VideoEncoderFactory.create('obs_x264', "My x264", {}); + const encoder = osn.VideoEncoderFactory.create('obs_x264', "video-encoder-test-2", {}); encoder.update({ rate_control: 'VBR', bitrate: 5000, @@ -157,5 +159,7 @@ describe(testName, () => { expect(propsArray[10].name).to.equal('repeat_headers', "Invalid repeat_headers name property"); expect(propsArray[10].value).to.equal(true, "Invalid repeat_headers value property"); + + encoder.release(); }); }); diff --git a/tests/osn-tests/util/obs_handler.ts b/tests/osn-tests/util/obs_handler.ts index 4f2a4bb3b..947e074c8 100644 --- a/tests/osn-tests/util/obs_handler.ts +++ b/tests/osn-tests/util/obs_handler.ts @@ -82,6 +82,7 @@ export class OBSHandler { filterTypes: string[]; transitionTypes: string[]; os: string; + ci: boolean; userStreamKey: string; defaultVideoContext: osn.IVideo; @@ -90,6 +91,7 @@ export class OBSHandler { this.os = process.platform; this.osnTestName = testName; this.cacheUploader = new CacheUploader(testName, this.obsPath); + this.ci = process.env.CI === 'true'; this.startup(); if (needDefaultVideoContext) { this.createDefaultVideoContext(); @@ -314,7 +316,7 @@ export class OBSHandler { skipSource(inputType: string) { if (process.platform === 'darwin') { - if (inputType === 'browser_source' || + if (inputType === 'browser_source' || inputType === 'window_capture' || inputType === 'monitor_capture' || inputType === 'display_capture' || @@ -342,4 +344,10 @@ export class OBSHandler { // Wrapped this in a function- just incase we want to add more conditions later or disable only within the build agent. return this.os === 'darwin'; } + + // is the build server environment + isCI() + { + return this.ci; + } }