Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/check-js-generated.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Check Generated JS

on:
push:
paths-ignore: ['**.md']
branches-ignore: [staging]
pull_request:
paths-ignore: ['**.md']
branches-ignore: [staging]

permissions:
contents: read

jobs:
check-js-generated:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'

- name: Install dependencies
run: |
corepack enable
corepack prepare yarn@4.14.1 --activate
yarn install --immutable

- name: Regenerate js/ outputs
run: yarn run build:javascript

- name: Fail if generated files are stale
run: ./ci/check-js-generated.sh
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ scan-build --keep-empty -internal-stats -stats -v -v -v -o check ninja.exe
```
Step with `"fixing"` errors is important as code base and especially third-party code are not ready to be build with clang. And files which failed to compile will not be scanned for errors.

#### Generated JS surface (`js/module.{js,d.ts}`)

`js/module.ts` is the source of truth. `js/module.js` and `js/module.d.ts` (and the `index.*` / `type_check.*` siblings) are tsc outputs of `js/tsconfig.json` — the npm package ships them as-is, so they're committed. Regenerate with `yarn build:javascript`. `yarn local:build` already chains it. CI rejects stale outputs via `.github/workflows/check-js-generated.yml` (`ci/check-js-generated.sh`).

### Tests

The tests for obs studio node are written in Typescript and use Mocha as test framework, with electron-mocha pacakage to make Mocha run in Electron, and Chai as assertion framework.
Expand Down
18 changes: 18 additions & 0 deletions ci/check-js-generated.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
# Verify that the committed tsc outputs under js/ match what regenerating
# from js/module.ts would produce. CI runs `yarn build:javascript` first,
# so any change under js/ at this point is a stale generated file.
dirty=$(git status --porcelain -- js/)

set +x
if [[ $dirty ]]; then
Comment thread
summeroff marked this conversation as resolved.
echo "================================================="
echo "Generated JS files are stale. Run locally:"
echo " yarn build:javascript"
echo "and commit the regenerated files."
echo ""
echo "Stale files:"
echo "$dirty"
echo "================================================="
exit 1
fi
78 changes: 31 additions & 47 deletions js/module.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ export declare const InputFactory: IInputFactory;
export declare const SceneFactory: ISceneFactory;
export declare const FilterFactory: IFilterFactory;
export declare const TransitionFactory: ITransitionFactory;
export declare const DisplayFactory: IDisplayFactory;
export declare const VolmeterFactory: IVolmeterFactory;
export declare const FaderFactory: IFaderFactory;
export declare const Audio: IAudio;
Expand Down Expand Up @@ -351,19 +350,15 @@ export interface IIPC {
disconnect(): void;
}
export interface IGlobal {
startup(locale: string, path?: string): void;
shutdown(): void;
getOutputFlagsFromId(id: string): number;
setOutputSource(channel: number, input: ISource): void;
getOutputSource(channel: number): ISource;
addSceneToBackstage(input: ISource): void;
removeSceneFromBackstage(input: ISource): void;
readonly totalFrames: number;
readonly laggedFrames: number;
readonly initialized: boolean;
locale: string;
multipleRendering: boolean;
readonly version: number;
readonly cpuPercentage: number;
readonly currentFrameRate: number;
readonly averageFrameRenderTime: number;
Expand Down Expand Up @@ -424,7 +419,6 @@ export interface INumberDetails {
readonly step: number;
}
export interface IProperty {
readonly status: number;
readonly name: string;
readonly description: string;
readonly longDescription: string;
Expand All @@ -433,11 +427,14 @@ export interface IProperty {
readonly type: EPropertyType;
readonly value: any;
next(): IProperty;
previous(): IProperty;
is_first(): boolean;
is_last(): boolean;
modified(): boolean;
}
export interface IProperties {
readonly status: number;
first(): IProperty;
last(): IProperty;
count(): number;
get(name: string): IProperty;
}
Expand Down Expand Up @@ -522,8 +519,9 @@ export interface IInput extends ISource {
sendFocus(focus: boolean): void;
sendKeyClick(eventData: IKeyEvent, keyUp: boolean): void;
setFilterOrder(filter: IFilter, movement: EOrderMovement): void;
setFilterOrder(filter: IFilter, movement: EOrderMovement): void;
copyFilters(other: IInput): boolean;
readonly filters: IFilter[];
readonly active: boolean;
readonly width: number;
readonly height: number;
getDuration(): number;
Expand All @@ -532,6 +530,8 @@ export interface IInput extends ISource {
pause(): void;
restart(): void;
stop(): void;
getMediaState(): number;
load(): void;
}
export interface ISceneFactory {
create(name: string): IScene;
Expand All @@ -547,6 +547,13 @@ export interface IScene extends ISource {
findItem(id: string | number): ISceneItem;
getItemAtIdx(idx: number): ISceneItem;
getItems(): ISceneItem[];
getItemsInRange(fromIndex: number, toIndex: number): ISceneItem[];
load(): void;
sendMouseClick(eventData: IMouseEvent, type: EMouseButtonType, mouseUp: boolean, clickCount: number): void;
sendMouseMove(eventData: IMouseEvent, mouseLeave: boolean): void;
sendMouseWheel(eventData: IMouseEvent, x_delta: number, y_delta: number): void;
sendFocus(focus: boolean): void;
sendKeyClick(eventData: IKeyEvent, keyUp: boolean): void;
}
export interface ISceneItem {
readonly source: IInput;
Expand Down Expand Up @@ -588,6 +595,12 @@ export interface ITransition extends ISource {
clear(): void;
set(input: ISource): void;
start(ms: number, input: ISource): void;
load(): void;
sendMouseClick(eventData: IMouseEvent, type: EMouseButtonType, mouseUp: boolean, clickCount: number): void;
sendMouseMove(eventData: IMouseEvent, mouseLeave: boolean): void;
sendMouseWheel(eventData: IMouseEvent, x_delta: number, y_delta: number): void;
sendFocus(focus: boolean): void;
sendKeyClick(eventData: IKeyEvent, keyUp: boolean): void;
}
export interface IConfigurable {
update(settings: ISettings): void;
Expand Down Expand Up @@ -631,26 +644,6 @@ export interface IVolmeter {
}
export interface ICallbackData {
}
export interface IDisplayFactory {
create(source?: IInput): IDisplay;
}
export interface IDisplay {
destroy(): void;
setPosition(x: number, y: number): void;
getPosition(): IVec2;
setSize(x: number, y: number): void;
getSize(): IVec2;
getPreviewOffset(): IVec2;
getPreviewSize(x: number, y: number): void;
shouldDrawUI: boolean;
paddingSize: number;
setPaddingColor(r: number, g: number, b: number, a: number): void;
setBackgroundColor(r: number, g: number, b: number, a: number): void;
setOutlineColor(r: number, g: number, b: number, a: number): void;
setGuidelineColor(r: number, g: number, b: number, a: number): void;
setResizeBoxOuterColor(r: number, g: number, b: number, a: number): void;
setResizeBoxInnerColor(r: number, g: number, b: number, a: number): void;
}
export interface IVideoInfo {
fpsNum: number;
fpsDen: number;
Expand Down Expand Up @@ -691,22 +684,18 @@ export interface IAudioFactory {
disableAudioDucking: boolean;
disableAudioDuckingLegacy: boolean;
}
export interface IModuleFactory extends IFactoryTypes {
export interface IModuleFactory {
open(binPath: string, dataPath: string): IModule;
loadAll(): void;
addPath(path: string, dataPath: string): void;
logLoaded(): void;
modules(): String[];
}
export interface IModule {
initialize(): void;
filename(): string;
name(): string;
author(): string;
description(): string;
binPath(): string;
dataPath(): string;
status(): number;
readonly fileName: string;
readonly name: string;
readonly author: string;
readonly description: string;
readonly binaryPath: string;
readonly dataPath: string;
}
export declare function addItems(scene: IScene, sceneItems: ISceneItemInfo[]): ISceneItem[];
export interface FilterInfo {
Expand Down Expand Up @@ -795,7 +784,6 @@ export interface IVideoEncoderFactory {
create(id: string, name: string, settings?: ISettings): IVideoEncoder;
}
export interface IStreaming {
// Video encoder value is only ignored in the Enhanced Broadcasting mode, otherwise it should be set
videoEncoder?: IVideoEncoder;
service: IService;
enforceServiceBitrate: boolean;
Expand All @@ -806,7 +794,7 @@ export interface IStreaming {
video: IVideo;
signalHandler: (signal: EOutputSignal) => void;
getAvailableEncoders(): IEncoderOption[];
start(): void; // throws
start(): void;
stop(force?: boolean): void;
droppedFrames: number;
totalFrames: number;
Expand Down Expand Up @@ -847,19 +835,15 @@ 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,
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,
additionalVideo?: IVideo;
}
export interface IEnhancedBroadcastingSimpleStreamingFactory {
create(): IEnhancedBroadcastingSimpleStreaming;
Expand Down
36 changes: 18 additions & 18 deletions js/module.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"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.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;
exports.NodeObs = exports.AdvancedReplayBufferFactory = exports.SimpleReplayBufferFactory = exports.AudioEncoderFactory = exports.AdvancedRecordingFactory = exports.SimpleRecordingFactory = exports.AudioTrackFactory = exports.NetworkFactory = exports.ReconnectFactory = exports.DelayFactory = exports.EnhancedBroadcastingSimpleStreamingFactory = exports.EnhancedBroadcastingAdvancedStreamingFactory = exports.AdvancedStreamingFactory = exports.SimpleStreamingFactory = exports.ServiceFactory = exports.VideoEncoderFactory = exports.IPC = exports.ModuleFactory = exports.AudioFactory = exports.Audio = exports.FaderFactory = exports.VolmeterFactory = 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.addItems = addItems;
exports.createSources = createSources;
exports.getSourcesSize = getSourcesSize;
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)
const hasDeveloperApp = process.platform === 'darwin' && fs.existsSync(path.join(__dirname, 'OSN.app'));
const obs = hasDeveloperApp
? require('./OSN.app/distribute/obs-studio-node/obs_studio_client.node')
: require('./obs_studio_client.node');
? require('./OSN.app/distribute/obs-studio-node/obs_studio_client.node')
: require('./obs_studio_client.node');
exports.DefaultD3D11Path = path.resolve(__dirname, `libobs-d3d11.dll`);
exports.DefaultOpenGLPath = path.resolve(__dirname, `libobs-opengl.dll`);
exports.DefaultDrawPluginPath = path.resolve(__dirname, `simple_draw.dll`);
Expand All @@ -23,7 +25,6 @@ exports.InputFactory = obs.Input;
exports.SceneFactory = obs.Scene;
exports.FilterFactory = obs.Filter;
exports.TransitionFactory = obs.Transition;
exports.DisplayFactory = obs.Display;
exports.VolmeterFactory = obs.Volmeter;
exports.FaderFactory = obs.Fader;
exports.Audio = obs.Audio;
Expand Down Expand Up @@ -60,19 +61,18 @@ function addItems(scene, sceneItems) {
}
return items;
}
exports.addItems = addItems;
function createSources(sources) {
const items = [];
if (Array.isArray(sources)) {
sources.forEach(function (source) {
let newSource = null;
try {
newSource = obs.Input.create(source.type, source.name, source.settings);
} catch (error) {
}
catch (error) {
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
return;
}

if (newSource) {
if (newSource.audioMixers) {
newSource.muted = source.muted ?? false;
Expand All @@ -82,34 +82,34 @@ 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) {
let ObsFilter = null;
try {
ObsFilter = obs.Filter.create(filter.type, filter.name, filter.settings);
} catch (filterError) {
}
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);
ObsFilter.release();
}
});
}
} else {
}
else {
console.warn(`[OSN] Input creation failed for source: ${source.name}`);
}
});
} else {
}
else {
console.error(`[OSN] Invalid sources array provided:`, sources);
}
return items;
}
exports.createSources = createSources;
function getSourcesSize(sourcesNames) {
const sourcesSize = [];
if (Array.isArray(sourcesNames)) {
Expand All @@ -122,10 +122,10 @@ function getSourcesSize(sourcesNames) {
}
return sourcesSize;
}
exports.getSourcesSize = getSourcesSize;
;
Comment thread
summeroff marked this conversation as resolved.
const appleBinaryFolder = hasDeveloperApp
? path.join(__dirname, 'OSN.app', 'distribute', 'obs-studio-node', 'bin')
: path.join(__dirname, 'bin');
? path.join(__dirname, 'OSN.app', 'distribute', 'obs-studio-node', 'bin')
: path.join(__dirname, 'bin');
if (fs.existsSync(path.resolve(appleBinaryFolder, 'obs64').replace('app.asar', 'app.asar.unpacked'))) {
obs.IPC.setServerPath(path.resolve(appleBinaryFolder, `obs64`).replace('app.asar', 'app.asar.unpacked'), path.resolve(appleBinaryFolder).replace('app.asar', 'app.asar.unpacked'));
}
Expand Down
Loading
Loading