├── website ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ ├── twitter.png │ │ └── logo.svg ├── vercel.json ├── docs │ ├── guides │ │ ├── _category_.yml │ │ ├── scene.md │ │ ├── sceneitem.md │ │ ├── input.md │ │ └── filter.md │ ├── getting-started │ │ ├── _category_.yml │ │ ├── typescript.md │ │ ├── installation.md │ │ └── basic-walkthrough.md │ └── overview.md ├── babel.config.js ├── src │ ├── pages │ │ ├── markdown-page.md │ │ ├── index.module.css │ │ └── index.js │ ├── components │ │ ├── HomepageFeatures.module.css │ │ └── HomepageFeatures.js │ └── css │ │ └── custom.css ├── tailwind.config.js ├── .gitignore ├── tsconfig.json ├── sidebars.js ├── README.md ├── package.json └── docusaurus.config.js ├── packages ├── animation │ ├── index.ts │ ├── src │ │ ├── utils.ts │ │ ├── performance.ts │ │ ├── types.ts │ │ ├── easing.ts │ │ └── obs-animation.ts │ ├── tests │ │ └── tsconfig.json │ ├── tsconfig.json │ ├── .vscode │ │ └── lauch.json │ └── package.json ├── core-old │ ├── index.ts │ ├── src │ │ ├── mocks │ │ │ ├── index.ts │ │ │ ├── MockInput.ts │ │ │ └── MockFilter.ts │ │ ├── utils.ts │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── SceneItem.ts │ │ ├── Filter.ts │ │ ├── OBS.ts │ │ └── Input.ts │ ├── tests │ │ ├── tsconfig.json │ │ ├── utils.ts │ │ ├── Source.test.ts │ │ ├── SceneItem.test.ts │ │ ├── Filter.test.ts │ │ ├── OBS.test.ts │ │ └── Input.test.ts │ ├── tsconfig.json │ ├── .vscode │ │ └── launch.json │ └── package.json ├── filters │ ├── index.ts │ ├── src │ │ ├── InvertPolarity.ts │ │ ├── Gain.ts │ │ ├── ApplyLUT.ts │ │ ├── Sharpen.ts │ │ ├── RenderDelay.ts │ │ ├── Limiter.ts │ │ ├── AspectRatio.ts │ │ ├── CropPad.ts │ │ ├── LumaKey.ts │ │ ├── Scroll.ts │ │ ├── NoiseGate.ts │ │ ├── Compressor.ts │ │ ├── index.ts │ │ ├── NoiseSuppress.ts │ │ ├── ColorCorrection.ts │ │ ├── ColorKey.ts │ │ ├── Expander.ts │ │ ├── ChromaKey.ts │ │ └── ImageMaskBlend.ts │ ├── tsconfig.json │ └── package.json ├── sources │ ├── index.ts │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── Image.ts │ │ ├── MacAudioInput.ts │ │ ├── VideoCapture.ts │ │ ├── Color.ts │ │ ├── DisplayCapture.ts │ │ ├── FreetypeText.ts │ │ ├── MacOSScreenCapture.ts │ │ ├── DecklinkInput.ts │ │ ├── GDIPlusText.ts │ │ ├── Browser.ts │ │ └── Media.ts │ └── package.json ├── streamfx │ ├── index.ts │ ├── src │ │ ├── filters.ts │ │ └── index.ts │ ├── tsconfig.json │ └── package.json └── core │ ├── src │ ├── index.ts │ ├── obs-types.ts │ ├── global.d.ts │ ├── OBS.ts │ ├── SceneItem.ts │ ├── filters.ts │ ├── inputs.ts │ ├── index.test.ts │ └── definition.ts │ ├── tsconfig.json │ ├── package.json │ └── publish.js ├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── pnpm-workspace.yaml ├── tsconfig.test.json ├── .gitignore ├── jest.config.js ├── .config ├── beemo │ ├── jest.ts │ └── typescript.ts └── beemo.ts ├── .vscode ├── settings.json ├── workspace.code-workspace └── launch.json ├── scripts ├── createVersionTag.js └── setPackageVersions.js ├── tsconfig.docs.json ├── examples ├── basic │ ├── tsconfig.json │ ├── package.json │ └── index.ts ├── extend_scene │ ├── tsconfig.json │ ├── package.json │ └── index.ts └── animation │ ├── tsconfig.json │ ├── package.json │ └── index.ts ├── tsconfig.json ├── tsconfig.options.json ├── LICENSE ├── package.json ├── README.md └── translations └── README_es.md /website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/animation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src" -------------------------------------------------------------------------------- /packages/core-old/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src" -------------------------------------------------------------------------------- /packages/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src" -------------------------------------------------------------------------------- /packages/sources/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src" -------------------------------------------------------------------------------- /packages/streamfx/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Brendonovich 2 | ko_fi: brendonovich 3 | -------------------------------------------------------------------------------- /website/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'examples/*' 4 | - 'website' -------------------------------------------------------------------------------- /website/docs/guides/_category_.yml: -------------------------------------------------------------------------------- 1 | position: 2 2 | label: "Guides" 3 | link: 4 | type: "generated-index" -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brendonovich/sceneify/HEAD/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/static/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brendonovich/sceneify/HEAD/website/static/img/twitter.png -------------------------------------------------------------------------------- /website/docs/getting-started/_category_.yml: -------------------------------------------------------------------------------- 1 | position: 1 2 | label: "Getting Started" 3 | link: 4 | type: "generated-index" -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/core-old/src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MockOBSWebSocket" 2 | export * from "./MockInput" 3 | export * from "./MockFilter" -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": false, 5 | "sourceMap": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /website/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | #MacOs 5 | .idea/ 6 | 7 | tsconfig.tsbuildinfo 8 | 9 | lib 10 | dist 11 | coverage 12 | esm 13 | cjs 14 | dts 15 | .pnpm-debug.log 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "preset": "ts-jest", 3 | "extensionsToTreatAsEsm": [], 4 | "testEnvironment": "node", 5 | "projects": [ 6 | "packages/*" 7 | ] 8 | }; -------------------------------------------------------------------------------- /.config/beemo/jest.ts: -------------------------------------------------------------------------------- 1 | import { JestConfig } from "@beemo/driver-jest"; 2 | 3 | export default { 4 | preset: "ts-jest", 5 | projects: [ 6 | "packages/*" 7 | ] 8 | } as JestConfig; 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": [ 3 | "core", 4 | "sources", 5 | "animation" 6 | ], 7 | "jest.jestCommandLine": "pnpm jest" 8 | } -------------------------------------------------------------------------------- /.config/beemo/typescript.ts: -------------------------------------------------------------------------------- 1 | import { TypeScriptConfig } from "@beemo/driver-typescript"; 2 | 3 | export default { 4 | compilerOptions: { 5 | stripInternal: true, 6 | }, 7 | } as TypeScriptConfig; 8 | -------------------------------------------------------------------------------- /scripts/createVersionTag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a git tag from the root package.json version. 3 | */ 4 | 5 | const exec = require("child_process").exec; 6 | 7 | exec(`git tag ${process.env.npm_package_version}`); 8 | -------------------------------------------------------------------------------- /packages/animation/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function getDeep(obj: any, path: string[]): any | undefined { 2 | for (let i = 0, len = path.length; i < len; i++) { 3 | obj = obj?.[path[i]]; 4 | } 5 | return obj; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as definition from "./definition.js"; 2 | export * as runtime from "./runtime.js"; 3 | export * from "./inputs.js"; 4 | export * from "./filters.js"; 5 | export * from "./obs.js"; 6 | -------------------------------------------------------------------------------- /packages/animation/src/performance.ts: -------------------------------------------------------------------------------- 1 | export const performance: import("perf_hooks").Performance = 2 | (global as any).window !== undefined 3 | ? (global as any).performance 4 | : require("perf_hooks").performance; 5 | -------------------------------------------------------------------------------- /tsconfig.docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.options.json", 3 | "compilerOptions": { 4 | "declarationDir": "dts", 5 | "outDir": "dts" 6 | }, 7 | "include": ["packages/*/src/**/*", "packages/*/types/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /packages/streamfx/src/filters.ts: -------------------------------------------------------------------------------- 1 | export const streamfxBlurFilter = defineFilterType( 2 | "streamfx-filter-blur" 3 | ).settings<{ 4 | "Filter.Blur.Type": string; 5 | "Filter.Blur.Subtype": "area"; 6 | "Filter.Blur.Size": number; 7 | }>(); 8 | -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class', 4 | mode: "jit", 5 | prefix: 'tw-', 6 | theme: { 7 | extend: {}, 8 | }, 9 | variants: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | important: true 14 | }; 15 | -------------------------------------------------------------------------------- /.config/beemo.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | module: "@beemo/dev", 3 | drivers: [ 4 | ["jest", {}], 5 | [ 6 | "typescript", 7 | { 8 | buildFolder: "dts", 9 | declarationOnly: true, 10 | }, 11 | ], 12 | ], 13 | settings: { 14 | node: true, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "target": "ES6", 8 | "sourceMap": true 9 | }, 10 | "exclude": ["dts", "tests"], 11 | "include": ["./index.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /examples/extend_scene/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "target": "ES6", 8 | "sourceMap": true 9 | }, 10 | "exclude": ["dts", "tests"], 11 | "include": ["./index.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Preserve", 4 | "moduleResolution": "Bundler", 5 | "target": "ES6", 6 | "declaration": true, 7 | "outDir": "dist", 8 | "skipLibCheck": true, 9 | "lib": ["es2020", "dom"], 10 | "types": ["@types/deno"], 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declarationDir": "dts", 4 | "outDir": "dts", 5 | "rootDir": "src", 6 | "emitDeclarationOnly": true 7 | }, 8 | "exclude": ["dts", "tests"], 9 | "extends": "../tsconfig.options.json", 10 | "include": ["src/**/*", "types/**/*", "../types/**/*"], 11 | "references": [] 12 | } 13 | -------------------------------------------------------------------------------- /packages/filters/src/InvertPolarity.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export class InvertPolarityFilter extends Filter< 4 | {}, 5 | TSource 6 | > { 7 | constructor(args: CustomFilterArgs<{}>) { 8 | super({ 9 | ...args, 10 | kind: "gain_filter", 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/animation/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": false, 4 | "emitDeclarationOnly": false, 5 | "noEmit": true, 6 | "rootDir": ".." 7 | }, 8 | "extends": "../../../tsconfig.test.json", 9 | "include": ["test/**/*", "src/**/*"], 10 | "references": [ 11 | { 12 | "path": ".." 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/core-old/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": false, 4 | "emitDeclarationOnly": false, 5 | "noEmit": true, 6 | "rootDir": ".." 7 | }, 8 | "extends": "../../../tsconfig.test.json", 9 | "include": ["test/**/*", "src/**/*"], 10 | "references": [ 11 | { 12 | "path": ".." 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/animation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "target": "ES6" 8 | // "esModuleInterop": false, 9 | // "allowSyntheticDefaultImports": true 10 | }, 11 | "exclude": ["dts", "tests"], 12 | "include": ["./index.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/core-old/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { mocks, OBS } from "../src"; 2 | 3 | export let obs = new OBS(); 4 | 5 | beforeEach(async () => { 6 | obs.socket = new mocks.MockOBSWebSocket() as any; 7 | await obs.connect("", ""); 8 | }); 9 | 10 | export function resetSceneify() { 11 | let socket = obs.socket; 12 | obs = new OBS(); 13 | obs.socket = socket; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core-old/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declarationDir": "dts", 4 | "outDir": "dts", 5 | "rootDir": "src", 6 | "declarationMap": false, 7 | "emitDeclarationOnly": true 8 | }, 9 | "exclude": ["dts", "tests"], 10 | "extends": "../../tsconfig.options.json", 11 | "include": ["src/**/*", "types/**/*", "../../types/**/*"], 12 | "references": [] 13 | } 14 | -------------------------------------------------------------------------------- /packages/filters/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declarationDir": "dts", 4 | "outDir": "dts", 5 | "rootDir": "src", 6 | "declarationMap": false, 7 | "emitDeclarationOnly": true 8 | }, 9 | "exclude": ["dts", "tests"], 10 | "extends": "../../tsconfig.options.json", 11 | "include": ["src/**/*", "types/**/*", "../../types/**/*"], 12 | "references": [] 13 | } 14 | -------------------------------------------------------------------------------- /packages/sources/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declarationDir": "dts", 4 | "outDir": "dts", 5 | "rootDir": "src", 6 | "declarationMap": false, 7 | "emitDeclarationOnly": true 8 | }, 9 | "exclude": ["dts", "tests"], 10 | "extends": "../../tsconfig.options.json", 11 | "include": ["src/**/*", "types/**/*", "../../types/**/*"], 12 | "references": [] 13 | } 14 | -------------------------------------------------------------------------------- /packages/streamfx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declarationDir": "dts", 4 | "outDir": "dts", 5 | "rootDir": "src", 6 | "declarationMap": false, 7 | "emitDeclarationOnly": true 8 | }, 9 | "exclude": ["dts", "tests"], 10 | "extends": "../../tsconfig.options.json", 11 | "include": ["src/**/*", "types/**/*", "../../types/**/*"], 12 | "references": [] 13 | } 14 | -------------------------------------------------------------------------------- /packages/sources/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Image"; 2 | export * from "./Browser"; 3 | export * from "./Color"; 4 | export * from "./FreetypeText"; 5 | export * from "./GDIPlusText"; 6 | export * from "./Media"; 7 | export * from "./DisplayCapture"; 8 | export * from "./VideoCapture"; 9 | export * from "./DecklinkInput"; 10 | export * from "./MacAudioInput"; 11 | export * from "./MacOSScreenCapture"; 12 | -------------------------------------------------------------------------------- /packages/animation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declarationDir": "dts", 4 | "outDir": "dts", 5 | "rootDir": "src", 6 | "declarationMap": false, 7 | "emitDeclarationOnly": true 8 | }, 9 | "exclude": ["dts", "tests"], 10 | "extends": "../../tsconfig.options.json", 11 | "include": ["src/**/*"], 12 | "references": [ 13 | { 14 | "path": "../core" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/filters/src/Gain.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type GainFilterSettings = { db: number }; 4 | 5 | export class GainFilter extends Filter< 6 | GainFilterSettings, 7 | TSource 8 | > { 9 | constructor(args: CustomFilterArgs) { 10 | super({ 11 | ...args, 12 | kind: "gain_filter", 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/filters/src/ApplyLUT.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type ApplyLUTFilterSettings = { 4 | image_path: string; 5 | }; 6 | 7 | export class ApplyLUTFilter extends Filter< 8 | ApplyLUTFilterSettings, 9 | TSource 10 | > { 11 | constructor(args: CustomFilterArgs) { 12 | super({ 13 | ...args, 14 | kind: "clut_filter", 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/filters/src/Sharpen.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type SharpenFilterSettings = { 4 | sharpness: number; 5 | }; 6 | 7 | export class SharpenFilter extends Filter< 8 | SharpenFilterSettings, 9 | TSource 10 | > { 11 | constructor(args: CustomFilterArgs) { 12 | super({ 13 | ...args, 14 | kind: "sharpness_filter_v2", 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core-old/src/mocks/MockInput.ts: -------------------------------------------------------------------------------- 1 | import { CustomInputArgs, Input } from "../Input"; 2 | import { SourceFilters } from "../Source"; 3 | 4 | export interface MockInputSettings { 5 | a: number; 6 | b: string; 7 | } 8 | 9 | export class MockInput< 10 | F extends SourceFilters 11 | > extends Input { 12 | constructor(args: CustomInputArgs) { 13 | super({ 14 | ...args, 15 | kind: "mock", 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/filters/src/RenderDelay.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type RenderDelayFilterSettings = { 4 | delay_ms: number; 5 | }; 6 | 7 | export class RenderDelayFilter extends Filter< 8 | RenderDelayFilterSettings, 9 | TSource 10 | > { 11 | constructor(args: CustomFilterArgs) { 12 | super({ 13 | ...args, 14 | kind: "gpu_delay", 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core-old/src/mocks/MockFilter.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter } from "../Filter"; 2 | import { Source } from "../Source"; 3 | 4 | export interface MockFilterSettings { 5 | a: number; 6 | b: string; 7 | } 8 | 9 | export class MockFilter extends Filter< 10 | MockFilterSettings, 11 | TSource 12 | > { 13 | constructor(args: CustomFilterArgs) { 14 | super({ 15 | ...args, 16 | kind: "mock", 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/core-old/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function removeUndefinedValues(obj: Record) { 2 | return Object.entries(obj).reduce( 3 | (acc, [k, v]) => (v === undefined ? acc : { ...acc, [k]: v }), 4 | {} 5 | ); 6 | } 7 | 8 | export function wait(ms: number) { 9 | return new Promise((res) => setTimeout(res, ms)); 10 | } 11 | 12 | export function rgba(red: number, green: number, blue: number, alpha = 255) { 13 | return red + (green << 8) + (blue << 16) + (alpha << 24); 14 | } 15 | -------------------------------------------------------------------------------- /packages/filters/src/Limiter.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type LimiterFilterSettings = { 4 | threshold: number; 5 | release_time: number; 6 | }; 7 | 8 | export class LimiterFilter extends Filter< 9 | LimiterFilterSettings, 10 | TSource 11 | > { 12 | constructor(args: CustomFilterArgs) { 13 | super({ 14 | ...args, 15 | kind: "limiter_filter", 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/sources/src/Image.ts: -------------------------------------------------------------------------------- 1 | import { CustomInputArgs, Input, SourceFilters } from "@sceneify/core"; 2 | 3 | export type ImageSourceSettings = { 4 | file: string; 5 | unload: boolean; 6 | linear_alpha: boolean; 7 | }; 8 | 9 | export class ImageSource extends Input< 10 | ImageSourceSettings, 11 | Filters 12 | > { 13 | constructor(args: CustomInputArgs) { 14 | super({ ...args, kind: "image_source" }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/filters/src/AspectRatio.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type AspectRatioFilterSettings = { 4 | resolution: string; 5 | sampling: string; 6 | }; 7 | 8 | export class AspectRatioFilter extends Filter< 9 | AspectRatioFilterSettings, 10 | TSource 11 | > { 12 | constructor(args: CustomFilterArgs) { 13 | super({ 14 | ...args, 15 | kind: "scale_filter", 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/streamfx/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type BlurFilterSettings = { 4 | "Filter.Blur.Type": string, 5 | "Filter.Blur.Subtype": string, 6 | "Filter.Blur.Size": number 7 | } 8 | 9 | export class BlurFilter extends Filter< 10 | BlurFilterSettings, 11 | TSource 12 | > { 13 | constructor(args: CustomFilterArgs) { 14 | super({ 15 | ...args, 16 | kind: "streamfx-filter-blur", 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 966px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | .hero__title { 26 | font-size: 4rem; 27 | } -------------------------------------------------------------------------------- /packages/filters/src/CropPad.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type CropPadFilterSettings = { 4 | bottom: number; 5 | left: number; 6 | relative: boolean; 7 | right: number; 8 | rop: number; 9 | }; 10 | 11 | export class CropPadFilter extends Filter< 12 | CropPadFilterSettings, 13 | TSource 14 | > { 15 | constructor(args: CustomFilterArgs) { 16 | super({ 17 | ...args, 18 | kind: "crop_filter", 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/filters/src/LumaKey.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | type LumaKeyFilterSettings = { 4 | luma_max: number; 5 | luma_max_smooth: number; 6 | luma_min: number; 7 | luma_min_smooth: number; 8 | }; 9 | 10 | export class LumaKeyFilter extends Filter< 11 | LumaKeyFilterSettings, 12 | TSource 13 | > { 14 | constructor(args: CustomFilterArgs) { 15 | super({ 16 | ...args, 17 | kind: "luma_key_filter", 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/filters/src/Scroll.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type ScrollFilterSettings = { 4 | limit_cx: boolean; 5 | limit_cy: boolean; 6 | loop: boolean; 7 | speed_x: number; 8 | speed_y: number; 9 | }; 10 | 11 | export class ScrollFilter extends Filter< 12 | ScrollFilterSettings, 13 | TSource 14 | > { 15 | constructor(args: CustomFilterArgs) { 16 | super({ 17 | ...args, 18 | kind: "scroll_filter", 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "start": "ts-node index.ts", 8 | "dev": "cd .. && yarn build && cd example && yarn start" 9 | }, 10 | "author": "Brendan Allan", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/node": "^16.11.16", 14 | "ts-node": "^10.2.1" 15 | }, 16 | "dependencies": { 17 | "@sceneify/core": "*", 18 | "@sceneify/filters": "*", 19 | "@sceneify/sources": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/extend_scene/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "start": "ts-node index.ts", 8 | "dev": "cd .. && yarn build && cd example && yarn start" 9 | }, 10 | "author": "Brendan Allan", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/node": "^16.11.16", 14 | "ts-node": "^10.2.1" 15 | }, 16 | "dependencies": { 17 | "@sceneify/core": "*", 18 | "@sceneify/filters": "*", 19 | "@sceneify/sources": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core-old/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Source"; 2 | export * from "./Scene"; 3 | export * from "./Input"; 4 | export * from "./SceneItem"; 5 | export * from "./Filter"; 6 | 7 | export * from "./OBS"; 8 | export * from "./constants"; 9 | 10 | export type { 11 | OBSRequestTypes, 12 | OBSResponseTypes, 13 | OBSEventTypes, 14 | DeepPartial, 15 | OBSEventTypesOverrides, 16 | OBSRequestTypesOverrides, 17 | OBSResponseTypesOverrides, 18 | Font, 19 | } from "./types"; 20 | 21 | export * as mocks from "./mocks"; 22 | export { rgba, wait } from "./utils"; 23 | -------------------------------------------------------------------------------- /examples/animation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "start": "ts-node index.ts", 8 | "dev": "cd .. && yarn build && cd example && yarn start" 9 | }, 10 | "author": "Brendan Allan", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "ts-node": "^10.2.1", 14 | "@types/node": "^16.9.3" 15 | }, 16 | "dependencies": { 17 | "@sceneify/animation": "*", 18 | "@sceneify/core": "*", 19 | "@sceneify/sources": "*", 20 | "@sceneify/filters": "*" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/filters/src/NoiseGate.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type NoiseGateFilterSettings = { 4 | open_threshold: number; 5 | close_threshold: number; 6 | attack_time: number; 7 | hold_time: number; 8 | release_time: number; 9 | }; 10 | 11 | export class NoiseGateFilter extends Filter< 12 | NoiseGateFilterSettings, 13 | TSource 14 | > { 15 | constructor(args: CustomFilterArgs) { 16 | super({ 17 | ...args, 18 | kind: "noise_gate_filter", 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sceneify/core", 3 | "main": "./src/index.ts", 4 | "scripts": { 5 | "build": "tsc", 6 | "deno": "deno --unstable-sloppy-imports" 7 | }, 8 | "publishConfig": { 9 | "access": "public", 10 | "main": "./dist/index.js", 11 | "types": "./dist/index.d.ts" 12 | }, 13 | "dependencies": { 14 | "obs-websocket-js": "5.0.1", 15 | "type-fest": "^4.28.0" 16 | }, 17 | "devDependencies": { 18 | "@types/deno": "^2.0.0", 19 | "typescript": "^5.7.2" 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "version": "0.0.0-main-fcf95365" 25 | } -------------------------------------------------------------------------------- /packages/filters/src/Compressor.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type CompressorFilterSettings = { 4 | ratio: number; 5 | threshold: number; 6 | attack_time: number; 7 | release_time: number; 8 | output_gain: number; 9 | sidechain_source: string; 10 | }; 11 | 12 | export class CompressorFilter extends Filter< 13 | CompressorFilterSettings, 14 | TSource 15 | > { 16 | constructor(args: CustomFilterArgs) { 17 | super({ 18 | ...args, 19 | kind: "compressor_filter", 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/filters/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ApplyLUT"; 2 | export * from "./AspectRatio"; 3 | export * from "./ChromaKey"; 4 | export * from "./ColorCorrection"; 5 | export * from "./ColorKey"; 6 | export * from "./Compressor"; 7 | export * from "./CropPad"; 8 | export * from "./Expander"; 9 | export * from "./Gain"; 10 | export * from "./ImageMaskBlend"; 11 | export * from "./InvertPolarity"; 12 | export * from "./Limiter"; 13 | export * from "./LumaKey"; 14 | export * from "./NoiseGate"; 15 | export * from "./NoiseSuppress"; 16 | export * from "./RenderDelay"; 17 | export * from "./Scroll"; 18 | export * from "./Sharpen"; 19 | -------------------------------------------------------------------------------- /website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/animation/.vscode/lauch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "name": "vscode-jest-tests", 6 | "request": "launch", 7 | "runtimeArgs": ["test", "--"], 8 | "runtimeExecutable": "pnpm", 9 | "cwd": "${workspaceFolder}", 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen", 12 | "disableOptimisticBPs": true, 13 | "skipFiles": [ 14 | "${workspaceFolder}/node_modules/**/*.js", 15 | "${workspaceFolder}/../../node_modules/**/*.js", 16 | "/**" 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/core-old/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "name": "vscode-jest-tests", 6 | "request": "launch", 7 | "runtimeArgs": ["test", "--"], 8 | "runtimeExecutable": "pnpm", 9 | "cwd": "${workspaceFolder}", 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen", 12 | "disableOptimisticBPs": true, 13 | "skipFiles": [ 14 | "${workspaceFolder}/node_modules/**/*.js", 15 | "${workspaceFolder}/../../node_modules/**/*.js", 16 | "/**" 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/filters/src/NoiseSuppress.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export enum NoiseSuppressMethod { 4 | Speex = "speex", 5 | RNNoise = "rnnoise", 6 | NVAFX = "nvafx", 7 | } 8 | 9 | export type NoiseSuppressFilterSettings = { 10 | method: NoiseSuppressMethod; 11 | }; 12 | 13 | export class NoiseSuppressFilter extends Filter< 14 | NoiseSuppressFilterSettings, 15 | TSource 16 | > { 17 | constructor(args: CustomFilterArgs) { 18 | super({ 19 | ...args, 20 | kind: "noise_suppress_filter", 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Beemo", 4 | "extends": "./tsconfig.options.json", 5 | "files": [], 6 | "references": [ 7 | { 8 | "path": "website" 9 | }, 10 | { 11 | "path": "packages/animation" 12 | }, 13 | { 14 | "path": "packages/animation/tests" 15 | }, 16 | { 17 | "path": "packages/core" 18 | }, 19 | { 20 | "path": "packages/core/tests" 21 | }, 22 | { 23 | "path": "packages/filters" 24 | }, 25 | { 26 | "path": "packages/sources" 27 | }, 28 | { 29 | "path": "packages/streamfx" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /packages/filters/src/ColorCorrection.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export type ColorCorrectionFilterSettings = { 4 | brightness: number; 5 | color_add: number; 6 | color_multiply: number; 7 | contrast: number; 8 | gamma: number; 9 | hue_shift: number; 10 | opacity: number; 11 | saturation: number; 12 | }; 13 | 14 | export class ColorCorrectionFilter extends Filter< 15 | ColorCorrectionFilterSettings, 16 | TSource 17 | > { 18 | constructor(args: CustomFilterArgs) { 19 | super({ 20 | ...args, 21 | kind: "color_filter_v2", 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/publish.js: -------------------------------------------------------------------------------- 1 | const decoder = new TextDecoder("utf-8"); 2 | const data = await Deno.readFile("package.json"); 3 | const pkgJson = JSON.parse(decoder.decode(data)); 4 | 5 | pkgJson.version = `0.0.0-main-${(await getCommitId()).slice(0, 8)}`; 6 | 7 | await Deno.writeTextFile("package.json", JSON.stringify(pkgJson, null, 2)); 8 | 9 | async function getCommitId() { 10 | const process = Deno.run({ 11 | cmd: ["git", "rev-parse", "HEAD"], 12 | stdout: "piped", 13 | stderr: "piped", 14 | }); 15 | 16 | const output = await process.output(); 17 | const commitId = new TextDecoder().decode(output).trim(); 18 | 19 | process.close(); 20 | 21 | return commitId; 22 | } 23 | -------------------------------------------------------------------------------- /packages/sources/src/MacAudioInput.ts: -------------------------------------------------------------------------------- 1 | import { Input, SourceFilters, CustomInputArgs } from "@sceneify/core"; 2 | 3 | export type MacAudioInputCaptureSettings = { 4 | device_id: string; 5 | }; 6 | 7 | export type MacAudioInputCapturePropertyLists = Pick< 8 | MacAudioInputCaptureSettings, 9 | "device_id" 10 | >; 11 | 12 | export class MacAudioInputCapture< 13 | Filters extends SourceFilters = {} 14 | > extends Input< 15 | MacAudioInputCaptureSettings, 16 | Filters, 17 | MacAudioInputCapturePropertyLists 18 | > { 19 | constructor(args: CustomInputArgs) { 20 | super({ 21 | ...args, 22 | kind: "coreaudio_input_capture", 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | module.exports = { 13 | // By default, Docusaurus generates a sidebar from the docs folder structure 14 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 15 | 16 | // But you can create a sidebar manually 17 | /* 18 | tutorialSidebar: [ 19 | { 20 | type: 'category', 21 | label: 'Tutorial', 22 | items: ['hello'], 23 | }, 24 | ], 25 | */ 26 | }; 27 | -------------------------------------------------------------------------------- /packages/sources/src/VideoCapture.ts: -------------------------------------------------------------------------------- 1 | import { Input, SourceFilters, CustomInputArgs } from "@sceneify/core"; 2 | 3 | export type VideoCaptureSourceSettings = { 4 | device: string; 5 | device_name: string; 6 | use_preset: boolean; 7 | buffering: boolean; 8 | enable_audio: boolean; 9 | }; 10 | 11 | export type VideoCaptureSourcePropertyLists = Pick< 12 | VideoCaptureSourceSettings, 13 | "device" 14 | >; 15 | 16 | export class VideoCaptureSource< 17 | Filters extends SourceFilters = {} 18 | > extends Input< 19 | VideoCaptureSourceSettings, 20 | Filters, 21 | VideoCaptureSourcePropertyLists 22 | > { 23 | constructor(args: CustomInputArgs) { 24 | super({ 25 | ...args, 26 | kind: "av_capture_input_v2", 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/sources/src/Color.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeepPartial, 3 | Input, 4 | SourceFilters, 5 | CustomInputArgs, 6 | } from "@sceneify/core"; 7 | 8 | export type ColorSourceSettings = { 9 | color: number; 10 | width: number; 11 | height: number; 12 | }; 13 | 14 | export class ColorSource extends Input< 15 | ColorSourceSettings, 16 | Filters 17 | > { 18 | constructor(args: CustomInputArgs) { 19 | super({ ...args, kind: "color_source_v3" }); 20 | } 21 | 22 | override async setSettings(settings: DeepPartial) { 23 | await super.setSettings(settings); 24 | 25 | this.itemInstances.forEach((item) => 26 | item.updateSizeFromSource(this.settings.width, this.settings.height) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/sources/src/DisplayCapture.ts: -------------------------------------------------------------------------------- 1 | import { Input, SourceFilters, CustomInputArgs } from "@sceneify/core"; 2 | 3 | export type DisplayCaptureSourceSettings = { 4 | monitor: number; 5 | compatibility: boolean; 6 | capture_cursor: boolean; 7 | }; 8 | 9 | export type DisplayCaptureSourcePropertyLists = Pick< 10 | DisplayCaptureSourceSettings, 11 | "monitor" 12 | >; 13 | 14 | /** 15 | * Only available on Windows 16 | */ 17 | export class DisplayCaptureSource< 18 | Filters extends SourceFilters = {} 19 | > extends Input< 20 | DisplayCaptureSourceSettings, 21 | Filters, 22 | DisplayCaptureSourcePropertyLists 23 | > { 24 | constructor(args: CustomInputArgs) { 25 | super({ 26 | ...args, 27 | kind: "monitor_capture", 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/sources/src/FreetypeText.ts: -------------------------------------------------------------------------------- 1 | import { Input, SourceFilters, CustomInputArgs, Font } from "@sceneify/core"; 2 | 3 | export type FreetypeTextSourceSettings = { 4 | font: Font; 5 | text: string; 6 | from_file: boolean; 7 | antialiasing: boolean; 8 | log_mode: boolean; 9 | log_lines: number; 10 | text_file: string; 11 | color1: number; 12 | color2: number; 13 | outline: boolean; 14 | drop_shadow: boolean; 15 | custom_width: number; 16 | word_wrap: boolean; 17 | }; 18 | 19 | export class FreetypeTextSource< 20 | Filters extends SourceFilters = {} 21 | > extends Input { 22 | constructor(args: CustomInputArgs) { 23 | super({ 24 | ...args, 25 | kind: "text_ft2_source_v2", 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/filters/src/ColorKey.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export enum ColorKeyColorType { 4 | Green = "green", 5 | Blue = "blue", 6 | Red = "red", 7 | Magenta = "magenta", 8 | Custom = "custom", 9 | } 10 | 11 | export type ColorKeyFilterSettings = { 12 | brightness: number; 13 | contrast: number; 14 | gamma: number; 15 | key_color: number; 16 | key_color_type: ColorKeyColorType; 17 | opacity: number; 18 | similarity: number; 19 | smoothness: number; 20 | }; 21 | 22 | export class ColorKeyFilter extends Filter< 23 | ColorKeyFilterSettings, 24 | TSource 25 | > { 26 | constructor(args: CustomFilterArgs) { 27 | super({ 28 | ...args, 29 | kind: "color_key_filter_v2", 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/filters/src/Expander.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export enum ExpanderDetectorType { 4 | RMS = "RMS", 5 | Peak = "peak", 6 | } 7 | 8 | export enum ExpanderPreset { 9 | Expander = "expander", 10 | Gate = "gate", 11 | } 12 | 13 | export type ExpanderFilterSettings = { 14 | ratio: number; 15 | threshold: number; 16 | attack_time: number; 17 | release_time: number; 18 | output_gain: number; 19 | detector: ExpanderDetectorType; 20 | presets: ExpanderPreset; 21 | }; 22 | 23 | export class ExpanderFilter extends Filter< 24 | ExpanderFilterSettings, 25 | TSource 26 | > { 27 | constructor(args: CustomFilterArgs) { 28 | super({ 29 | ...args, 30 | kind: "expander_filter", 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/filters/src/ChromaKey.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export enum ChromaKeyColorType { 4 | Green = "green", 5 | Blue = "blue", 6 | Magenta = "magenta", 7 | Custom = "custom", 8 | } 9 | 10 | export type ChromaKeyFilterSettings = { 11 | brightness: number; 12 | contrast: number; 13 | gamma: number; 14 | key_color: number; 15 | key_color_type: ChromaKeyColorType; 16 | opacity: number; 17 | similarity: number; 18 | smoothness: number; 19 | spill: number; 20 | }; 21 | 22 | export class ChromaKeyFilter extends Filter< 23 | ChromaKeyFilterSettings, 24 | TSource 25 | > { 26 | constructor(args: CustomFilterArgs) { 27 | super({ 28 | ...args, 29 | kind: "chroma_key_filter_v2", 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/filters/src/ImageMaskBlend.ts: -------------------------------------------------------------------------------- 1 | import { CustomFilterArgs, Filter, Source } from "@sceneify/core"; 2 | 3 | export enum MaskBlendType { 4 | AlphaMaskAlphaChannel = "mask_alpha_filter.effect", 5 | AlphaMaskColourChannel = "mask_colour_filter.effect", 6 | BlendMultiply = "blend_mul_filter.effect", 7 | BlendAddition = "blend_add_filter.effect", 8 | BlendSubtraction = "blend_sub_filter.effect", 9 | } 10 | 11 | export type ImageMaskBlendFilterSettings = { 12 | image_path: string; 13 | type: MaskBlendType; 14 | }; 15 | 16 | export class ImageMaskBlendFilter extends Filter< 17 | ImageMaskBlendFilterSettings, 18 | TSource 19 | > { 20 | constructor(args: CustomFilterArgs) { 21 | super({ 22 | ...args, 23 | kind: "mask_filter_v2", 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/animation/src/types.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = T extends Function 2 | ? T 3 | : T extends object 4 | ? { [P in keyof T]?: DeepPartial } 5 | : T; 6 | 7 | export type FilterType = Pick< 8 | Base, 9 | { 10 | [Key in keyof Base]: Base[Key] extends Condition ? Key : never; 11 | }[keyof Base] 12 | >; 13 | 14 | export type DeepReplace = T extends object 15 | ? { 16 | [K in keyof T]: DeepReplace; 17 | } 18 | : Replace; 19 | 20 | export type DeepPartialReplace = T extends object 21 | ? { 22 | [K in keyof T]?: DeepReplace; 23 | } 24 | : Replace; 25 | 26 | export type DeepSearch = T extends object 27 | ? { 28 | [K in keyof T]: DeepSearch; 29 | } 30 | : T extends Search 31 | ? T 32 | : never; 33 | -------------------------------------------------------------------------------- /tsconfig.options.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "isolatedModules": true, 10 | "lib": [ 11 | "esnext" 12 | ], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noEmitOnError": false, 16 | "noImplicitOverride": true, 17 | "noImplicitReturns": true, 18 | "pretty": true, 19 | "removeComments": false, 20 | "resolveJsonModule": false, 21 | "skipLibCheck": true, 22 | "sourceMap": false, 23 | "strict": true, 24 | "target": "es2020", 25 | "composite": true, 26 | "declarationMap": true, 27 | "emitDeclarationOnly": true, 28 | "stripInternal": true 29 | } 30 | } -------------------------------------------------------------------------------- /packages/animation/src/easing.ts: -------------------------------------------------------------------------------- 1 | export enum Easing { 2 | In, 3 | Out, 4 | InOut, 5 | Linear, 6 | } 7 | 8 | type EasingFunction = (from: number, to: number, factor: number) => number; 9 | 10 | export const easingFuncs: Record = { 11 | [Easing.Out]: (from, to, factor) => 12 | from + (to - from) * (1 - Math.pow(1 - factor, 3)), 13 | [Easing.In]: (from, to, factor) => from + (to - from) * Math.pow(factor, 3), 14 | [Easing.InOut]: (from, to, factor) => 15 | from + 16 | (to - from) * 17 | (factor < 0.5 18 | ? 4 * Math.pow(factor, 3) 19 | : 1 - Math.pow(-2 * factor + 2, 3) / 2), 20 | [Easing.Linear]: (from, to, factor) => from + (to - from) * factor, 21 | }; 22 | 23 | export let DEFAULT_EASING = Easing.Linear; 24 | export const setDefaultEasing = (newDefault: Easing) => 25 | (DEFAULT_EASING = newDefault); 26 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /.vscode/workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "core", 5 | "path": "../packages/core" 6 | }, 7 | { 8 | "name": "animation", 9 | "path": "../packages/animation" 10 | }, 11 | { 12 | "name": "website", 13 | "path": "../website" 14 | }, 15 | { 16 | "name": "root", 17 | "path": ".." 18 | } 19 | ], 20 | "settings": { 21 | "conventionalCommits.scopes": [ 22 | "README", 23 | "Version", 24 | "Example", 25 | "core", 26 | "animation", 27 | "filters", 28 | "web", 29 | "sources" 30 | ], 31 | "jest.disabledWorkspaceFolders": ["root", "website"], 32 | "jest.jestCommandLine": "pnpm test --", 33 | "search.exclude": { 34 | "**/node_modules": true, 35 | "**/bower_components": true, 36 | "**/*.code-search": true, 37 | "**/dist": true, 38 | "**/lib": true 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/sources/src/MacOSScreenCapture.ts: -------------------------------------------------------------------------------- 1 | import { Input, SourceFilters, CustomInputArgs } from "@sceneify/core"; 2 | 3 | export enum MacOSCaptureType { 4 | Display = 0, 5 | Window = 1, 6 | Application = 2, 7 | } 8 | 9 | export type MacOSScreenCaptureSettings = { 10 | type: MacOSCaptureType; 11 | display: number; 12 | application: string; 13 | window: number; 14 | show_empty_names: boolean; 15 | show_cursor: boolean; 16 | }; 17 | 18 | export type MacOSScreenCapturePropertyLists = Pick< 19 | MacOSScreenCaptureSettings, 20 | "display" | "application" | "window" 21 | >; 22 | 23 | export class MacOSScreenCapture< 24 | Filters extends SourceFilters = {} 25 | > extends Input< 26 | MacOSScreenCaptureSettings, 27 | Filters, 28 | MacOSScreenCapturePropertyLists 29 | > { 30 | constructor(args: CustomInputArgs) { 31 | super({ 32 | ...args, 33 | kind: "screen_capture", 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Brendonovich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sceneify-root", 3 | "version": "1.0.0-beta.5", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Brendonovich/sceneify" 8 | }, 9 | "workspaces": { 10 | "packages": [ 11 | "packages/*", 12 | "website" 13 | ] 14 | }, 15 | "scripts": { 16 | "test": "beemo jest", 17 | "coverage": "pnpm test -- --coverage", 18 | "build": "packemon build --addEngines", 19 | "type": "beemo typescript --build", 20 | "build-all": "pnpm build && pnpm type", 21 | "clean": "packemon clean", 22 | "set-package-versions": "node scripts/setPackageVersions.js", 23 | "create-version-tag": "node scripts/createVersionTag.js" 24 | }, 25 | "devDependencies": { 26 | "@beemo/cli": "^2.0.5", 27 | "@beemo/core": "^2.1.3", 28 | "@beemo/dev": "^1.7.7", 29 | "typescript": "^5.7.2" 30 | }, 31 | "dependencies": { 32 | "@babel/core": "^7.17.5", 33 | "@babel/preset-env": "^7.16.11", 34 | "@beemo/driver-babel": "^2.0.5", 35 | "@beemo/driver-jest": "^2.0.4", 36 | "jest": "^27.5.1", 37 | "packemon": "^1.14.0", 38 | "ts-jest": "^27.1.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/animation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sceneify/animation", 3 | "version": "1.0.0-beta.0", 4 | "description": "", 5 | "main": "./lib/node/index.js", 6 | "types": "./dts/index.d.ts", 7 | "module": "./esm/index.js", 8 | "browser": "./lib/browser/index.js", 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@datastructures-js/queue": "^4.1.3", 13 | "@sceneify/core": "*" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^18.7.16", 17 | "jest": "^27.4.5" 18 | }, 19 | "scripts": { 20 | "test": "jest -i" 21 | }, 22 | "files": [ 23 | "dist", 24 | "dts", 25 | "esm/**/*.{js,map}", 26 | "lib", 27 | "lib/**/*.{js,map}", 28 | "src/**/*.{ts,tsx,json}" 29 | ], 30 | "packemon": { 31 | "format": [ 32 | "lib", 33 | "esm" 34 | ], 35 | "platform": [ 36 | "browser", 37 | "node" 38 | ] 39 | }, 40 | "engines": { 41 | "node": ">=12.17.0", 42 | "npm": ">=6.13.0" 43 | }, 44 | "jest": { 45 | "preset": "ts-jest", 46 | "globals": { 47 | "ts-jest": { 48 | "tsconfig": "/tests/tsconfig.json" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/filters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sceneify/filters", 3 | "version": "1.0.0-beta.0", 4 | "description": "", 5 | "main": "./cjs/index.cjs", 6 | "browser": "./lib/index.js", 7 | "module": "./esm/index.js", 8 | "types": "./dts/index.d.ts", 9 | "author": "", 10 | "license": "ISC", 11 | "scripts": { 12 | "test": "jest -i" 13 | }, 14 | "dependencies": { 15 | "@sceneify/core": "*" 16 | }, 17 | "devDependencies": { 18 | "jest": "^27.4.5" 19 | }, 20 | "files": [ 21 | "cjs/**/*.{cjs,map}", 22 | "dist", 23 | "dts", 24 | "esm/**/*.{js,map}", 25 | "lib", 26 | "lib/**/*.{js,map}", 27 | "src/**/*.{ts,tsx,json}" 28 | ], 29 | "packemon": [ 30 | { 31 | "format": "cjs", 32 | "platform": "node" 33 | }, 34 | { 35 | "format": [ 36 | "lib", 37 | "esm" 38 | ], 39 | "platform": "browser" 40 | } 41 | ], 42 | "engines": { 43 | "node": ">=12.17.0", 44 | "npm": ">=6.13.0" 45 | }, 46 | "jest": { 47 | "preset": "ts-jest", 48 | "globals": { 49 | "ts-jest": { 50 | "tsconfig": "/tests/tsconfig.json" 51 | } 52 | } 53 | }, 54 | "type": "commonjs" 55 | } 56 | -------------------------------------------------------------------------------- /packages/streamfx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sceneify/streamfx", 3 | "version": "1.0.0-beta.0", 4 | "description": "", 5 | "main": "./cjs/index.cjs", 6 | "browser": "./lib/index.js", 7 | "module": "./esm/index.js", 8 | "types": "./dts/index.d.ts", 9 | "author": "", 10 | "license": "ISC", 11 | "scripts": { 12 | "test": "jest -i" 13 | }, 14 | "dependencies": { 15 | "@sceneify/core": "*" 16 | }, 17 | "devDependencies": { 18 | "jest": "^27.4.5" 19 | }, 20 | "files": [ 21 | "cjs/**/*.{cjs,map}", 22 | "dist", 23 | "dts", 24 | "esm/**/*.{js,map}", 25 | "lib", 26 | "lib/**/*.{js,map}", 27 | "src/**/*.{ts,tsx,json}" 28 | ], 29 | "packemon": [ 30 | { 31 | "format": "cjs", 32 | "platform": "node" 33 | }, 34 | { 35 | "format": [ 36 | "lib", 37 | "esm" 38 | ], 39 | "platform": "browser" 40 | } 41 | ], 42 | "engines": { 43 | "node": ">=12.17.0", 44 | "npm": ">=6.13.0" 45 | }, 46 | "jest": { 47 | "preset": "ts-jest", 48 | "globals": { 49 | "ts-jest": { 50 | "tsconfig": "/tests/tsconfig.json" 51 | } 52 | } 53 | }, 54 | "type": "commonjs" 55 | } 56 | -------------------------------------------------------------------------------- /packages/sources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sceneify/sources", 3 | "version": "1.0.0-beta.0", 4 | "description": "", 5 | "main": "./cjs/index.cjs", 6 | "browser": "./lib/index.js", 7 | "module": "./esm/index.js", 8 | "types": "./dts/index.d.ts", 9 | "author": "", 10 | "license": "ISC", 11 | "scripts": { 12 | "test": "jest -i" 13 | }, 14 | "dependencies": { 15 | "@sceneify/core": "*", 16 | "eventemitter3": "^4.0.7" 17 | }, 18 | "devDependencies": { 19 | "jest": "^27.4.5" 20 | }, 21 | "files": [ 22 | "cjs/**/*.{cjs,map}", 23 | "dist", 24 | "dts", 25 | "esm/**/*.{js,map}", 26 | "lib", 27 | "lib/**/*.{js,map}", 28 | "src/**/*.{ts,tsx,json}" 29 | ], 30 | "packemon": [ 31 | { 32 | "format": "cjs", 33 | "platform": "node" 34 | }, 35 | { 36 | "format": [ 37 | "lib", 38 | "esm" 39 | ], 40 | "platform": "browser" 41 | } 42 | ], 43 | "engines": { 44 | "node": ">=12.17.0", 45 | "npm": ">=6.13.0" 46 | }, 47 | "jest": { 48 | "preset": "ts-jest", 49 | "globals": { 50 | "ts-jest": { 51 | "tsconfig": "/tests/tsconfig.json" 52 | } 53 | } 54 | }, 55 | "type": "commonjs" 56 | } 57 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "example (basic)", 9 | "type": "node", 10 | "request": "launch", 11 | "runtimeArgs": ["index.ts"], 12 | "runtimeExecutable": "ts-node", 13 | "cwd": "${workspaceRoot}/examples/basic", 14 | "protocol": "inspector", 15 | "internalConsoleOptions": "openOnSessionStart" 16 | }, 17 | { 18 | "type": "node", 19 | "name": "vscode-jest-tests", 20 | "request": "launch", 21 | "console": "integratedTerminal", 22 | "internalConsoleOptions": "neverOpen", 23 | "disableOptimisticBPs": true, 24 | "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", 25 | "cwd": "${workspaceFolder}", 26 | "args": ["jest", "--runInBand", "--watchAll=false"] 27 | }, 28 | { 29 | "name": "Launch selected example", 30 | "type": "node", 31 | "request": "launch", 32 | "cwd": "${fileDirname}", 33 | "runtimeExecutable": "pnpm", 34 | "runtimeArgs": ["start"] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/obs-types.ts: -------------------------------------------------------------------------------- 1 | export interface OBSSceneItemTransform { 2 | sourceWidth: number; 3 | sourceHeight: number; 4 | 5 | positionX: number; 6 | positionY: number; 7 | 8 | rotation: number; 9 | 10 | scaleX: number; 11 | scaleY: number; 12 | 13 | width: number; 14 | height: number; 15 | 16 | alignment: OBSAlignment; 17 | 18 | boundsType: OBSBoundsType; 19 | boundsAlignment: OBSAlignment; 20 | boundsWidth: number; 21 | boundsHeight: number; 22 | 23 | cropLeft: number; 24 | cropTop: number; 25 | cropRight: number; 26 | cropBottom: number; 27 | } 28 | 29 | export type OBSAlignment = 0 | 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10; 30 | export type OBSBoundsType = 31 | | "OBS_BOUNDS_NONE" 32 | | "OBS_BOUNDS_STRETCH" 33 | | "OBS_BOUNDS_SCALE_INNER" 34 | | "OBS_BOUNDS_SCALE_OUTER" 35 | | "OBS_BOUNDS_SCALE_TO_WIDTH" 36 | | "OBS_BOUNDS_SCALE_TO_HEIGHT" 37 | | "OBS_BOUNDS_MAX_ONLY"; 38 | export type OBSMonitoringType = 39 | | "OBS_MONITORING_TYPE_NONE" 40 | | "OBS_MONITORING_TYPE_MONITOR_ONLY" 41 | | "OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT"; 42 | 43 | export interface OBSFont { 44 | face: string; 45 | flags: number; 46 | size: number; 47 | style: string; 48 | } 49 | 50 | export type OBSVideoRange = 0 | 1 | 2; 51 | 52 | export type OBSVolumeInput = { db: number } | { mul: number }; 53 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Credit to NextAuth.js for this css */ 2 | :root { 3 | --ifm-color-link: #289ef9; 4 | --ifm-color-primary: rgb(133, 95, 239); 5 | --ifm-color-primary-dark: #03a7fa; 6 | --ifm-color-primary-darker: #039eed; 7 | --ifm-color-primary-darkest: #0382c3; 8 | --ifm-color-primary-light: #3abbfc; 9 | --ifm-color-primary-lighter: #48bffd; 10 | --ifm-color-primary-lightest: #03a7fa; 11 | --ifm-code-font-size: 95%; 12 | --ifm-color-info: #1eb1fc; 13 | --ifm-color-success: #1eb1fc; 14 | --ifm-color-warning: #c94b4b; 15 | --ifm-font-family-base: -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 16 | Noto Sans, sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, 17 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 18 | --ifm-background-color: #fff; 19 | --ifm-footer-background-color: #f9f9f9; 20 | --ifm-hero-background-color: #f5f5f5; 21 | --ifm-navbar-background-color: rgba(255, 255, 255, 0.95); 22 | --ifm-h1-font-size: 3rem; 23 | --ifm-h1-font-size: 2rem; 24 | } 25 | 26 | html[data-theme="dark"]:root { 27 | --ifm-color-primary: #1eb1fc; 28 | --ifm-footer-background-color: #000; 29 | --ifm-html-background-color: #242526; 30 | --ifm-background-color: #090909; 31 | --ifm-hero-background-color: #111111; 32 | --ifm-navbar-background-color: rgba(0, 0, 0, 0.95); 33 | } 34 | -------------------------------------------------------------------------------- /packages/core-old/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sceneify/core-old", 3 | "version": "1.0.0-beta.0", 4 | "description": "", 5 | "main": "./cjs/index.cjs", 6 | "browser": "./lib/index.js", 7 | "module": "./esm/index.js", 8 | "types": "./dts/index.d.ts", 9 | "author": "", 10 | "license": "ISC", 11 | "scripts": { 12 | "test": "jest -i" 13 | }, 14 | "dependencies": { 15 | "next-tick": "^1.1.0", 16 | "obs-websocket-js": "5.0.1" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^27.0.3", 20 | "@types/next-tick": "^1.0.0", 21 | "@types/node": "^16.9.3", 22 | "@types/ws": "^7.4.6", 23 | "jest": "^27.4.5" 24 | }, 25 | "files": [ 26 | "cjs/**/*.{cjs,map}", 27 | "dist", 28 | "dts", 29 | "esm/**/*.{js,map}", 30 | "lib", 31 | "lib/**/*.{js,map}", 32 | "src/**/*.{ts,tsx,json}" 33 | ], 34 | "packemon": [ 35 | { 36 | "format": "cjs", 37 | "platform": "node" 38 | }, 39 | { 40 | "format": [ 41 | "lib", 42 | "esm" 43 | ], 44 | "platform": "browser" 45 | } 46 | ], 47 | "engines": { 48 | "node": ">=12.17.0", 49 | "npm": ">=6.13.0" 50 | }, 51 | "jest": { 52 | "preset": "ts-jest", 53 | "globals": { 54 | "ts-jest": { 55 | "tsconfig": "/tests/tsconfig.json" 56 | } 57 | } 58 | }, 59 | "type": "commonjs" 60 | } 61 | -------------------------------------------------------------------------------- /packages/core/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import "obs-websocket-js"; 2 | 3 | declare module "obs-websocket-js" { 4 | type SceneifyPrivateSettings = { 5 | init: "created" | "linked"; 6 | }; 7 | 8 | interface OBSRequestTypes { 9 | // undocumented requests that the devs left in for us :) 10 | SetSourcePrivateSettings: { 11 | sourceName: string; 12 | sourceSettings: { 13 | SCENEIFY?: SceneifyPrivateSettings & { 14 | filters?: Array<{ name: string }>; 15 | }; 16 | }; 17 | }; 18 | GetSourcePrivateSettings: { 19 | sourceName: string; 20 | }; 21 | SetSceneItemPrivateSettings: { 22 | sceneName: string; 23 | sceneItemId: number; 24 | sceneItemSettings: { 25 | SCENEIFY?: SceneifyPrivateSettings; 26 | }; 27 | }; 28 | GetSceneItemPrivateSettings: { 29 | sceneName: string; 30 | sceneItemId: number; 31 | }; 32 | } 33 | 34 | interface OBSResponseTypes { 35 | SetSourcePrivateSettings: void; 36 | GetSourcePrivateSettings: { 37 | sourceSettings: { 38 | SCENEIFY?: SceneifyPrivateSettings & { 39 | filters?: Array<{ name: string }>; 40 | }; 41 | }; 42 | }; 43 | SetSceneItemPrivateSettings: {}; 44 | GetSceneItemPrivateSettings: { 45 | sceneItemSettings: { 46 | SCENEIFY?: SceneifyPrivateSettings; 47 | }; 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "^2.0.0-beta.14", 18 | "@docusaurus/preset-classic": "^2.0.0-beta.9", 19 | "@mdx-js/react": "^1.6.21", 20 | "@svgr/webpack": "^5.5.0", 21 | "clsx": "^1.1.1", 22 | "docusaurus-plugin-typedoc-api": "^1.7.0", 23 | "file-loader": "^6.2.0", 24 | "prism-react-renderer": "^1.2.1", 25 | "react": "^17.0.1", 26 | "react-dom": "^17.0.1", 27 | "url-loader": "^4.1.1" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.5%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@types/react": "^17.0.38", 43 | "autoprefixer": "^10.4.0", 44 | "postcss": "^8.4.5", 45 | "postcss-import": "^14.0.2", 46 | "postcss-preset-env": "^6.7.0", 47 | "tailwindcss": "^2.2.19", 48 | "typescript": "^5.7.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/core-old/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { SceneItemTransform } from "./SceneItem"; 2 | 3 | export enum Alignment { 4 | CenterLeft = 1, 5 | Center = 0, 6 | CenterRight = 2, 7 | TopLeft = 5, 8 | TopCenter = 4, 9 | TopRight = 6, 10 | BottomLeft = 9, 11 | BottomCenter = 8, 12 | BottomRight = 10, 13 | } 14 | 15 | export enum SourceType { 16 | OBS_SOURCE_TYPE_INPUT, 17 | OBS_SOURCE_TYPE_FILTER, 18 | OBS_SOURCE_TYPE_TRANSITION, 19 | OBS_SOURCE_TYPE_SCENE, 20 | } 21 | export enum BoundsType { 22 | None = "OBS_BOUNDS_NONE", 23 | Stretch = "OBS_BOUNDS_STRETCH", 24 | ScaleInner = "OBS_BOUNDS_SCALE_INNER", 25 | ScaleOuter = "OBS_BOUNDS_SCALE_OUTER", 26 | ScaleToWidth = "OBS_BOUNDS_SCALE_TO_WIDTH", 27 | ScaleToHeight = "OBS_BOUNDS_SCALE_TO_HEIGHT", 28 | MaxOnly = "OBS_BOUNDS_MAX_ONLY", 29 | } 30 | 31 | export enum MonitoringType { 32 | None = "OBS_MONITORING_TYPE_NONE", 33 | MonitorOnly = "OBS_MONITORING_TYPE_MONITOR_ONLY", 34 | MonitorAndOutput = "OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT", 35 | } 36 | 37 | export const DEFAULT_SCENE_ITEM_TRANSFORM: SceneItemTransform = { 38 | positionX: 0, 39 | positionY: 0, 40 | rotation: 0, 41 | scaleX: 1, 42 | scaleY: 1, 43 | cropTop: 0, 44 | cropBottom: 0, 45 | cropLeft: 0, 46 | cropRight: 0, 47 | alignment: Alignment.Center, 48 | boundsAlignment: Alignment.Center, 49 | sourceWidth: 0, 50 | sourceHeight: 0, 51 | width: 0, 52 | height: 0, 53 | boundsWidth: 0, 54 | boundsHeight: 0, 55 | boundsType: BoundsType.None, 56 | }; 57 | 58 | export enum VideoRange { 59 | Default = 0, 60 | Partial = 1, 61 | Full = 2, 62 | } 63 | -------------------------------------------------------------------------------- /packages/sources/src/DecklinkInput.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | SourceFilters, 4 | CustomInputArgs, 5 | VideoRange, 6 | } from "@sceneify/core"; 7 | 8 | export const DecklinkPixelFormat = { 9 | YUV8Bit: 0x32767579, 10 | YUV10Bit: 0x76323130, 11 | BGRA8Bit: 0x42475241, 12 | }; 13 | 14 | export const DecklinkColorSpace = { 15 | Default: 0, 16 | BT601: 1, 17 | BT709: 2, 18 | }; 19 | 20 | export const DecklinkChannelFormat = { 21 | None: 0, 22 | Stereo: 2, 23 | TwoPointOne: 3, 24 | FourPointZero: 4, 25 | FourPointOne: 5, 26 | FivePointOne: 6, 27 | SevenPointOne: 8, 28 | }; 29 | 30 | export type DecklinkInputSettings = { 31 | device_name: string; 32 | device_hash: string; 33 | video_connection: number; 34 | audio_connection: number; 35 | mode_id: number; 36 | pixel_format: number; 37 | color_space: number; 38 | color_range: VideoRange; 39 | channel_format: number; 40 | swap: boolean; 41 | buffering: boolean; 42 | deactivate_when_not_showing: boolean; 43 | allow_10_bit: boolean; 44 | }; 45 | 46 | export type DecklinkInputPropertyLists = Pick< 47 | DecklinkInputSettings, 48 | | "device_hash" 49 | | "video_connection" 50 | | "audio_connection" 51 | | "mode_id" 52 | | "pixel_format" 53 | | "color_space" 54 | | "channel_format" 55 | >; 56 | 57 | export class DecklinkInput extends Input< 58 | DecklinkInputSettings, 59 | Filters, 60 | DecklinkInputPropertyLists 61 | > { 62 | constructor(args: CustomInputArgs) { 63 | super({ 64 | ...args, 65 | kind: "decklink-input", 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /website/docs/getting-started/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | slug: /typescript 4 | --- 5 | 6 | # TypeScript Support 7 | 8 | While Sceneify can be used directly from JavaScript, it is written in TypeScript and provides first-class TypeScript support. Each class provided by Sceneify is capable of remembering extra information about how it was created when TypeScript is used: 9 | 10 | - [Scene](/api/core/class/Scene): Items, filters and settings types 11 | - [Source](/api/core/class/Source): Filters and settings types 12 | - [SceneItem](/api/core/class/SceneItem): Base source and containing scene 13 | 14 | All of this information can assist in using Sceneify as your programming environment can be aware of most - if not all - of your OBS layout as you code, providing suggestions and type safety. 15 | 16 | ## Code Editor Integration 17 | 18 | If you are using a code editor such as [Visual Studio Code](https://code.visualstudio.com/), you likely already have TypeScript checking your JavaScript code automatically and providing autocompletion. If not, install the TypeScript extension for your code editor. 19 | 20 | ## Using the TypeScript Compiler 21 | 22 | The TypeScript compiler `tsc` can perform the same type checking as your code editor, the only difference being that it isn't performed as you type. 23 | 24 | Alternatively, you can skip compilation and run your TypeScript code with a utility like [ts-node](https://github.com/TypeStrong/ts-node), which will execute your TypeScript files in the same way that `node` executes JavaScript files. This will type check your files before running them, providing the same level of safety as `tsc`. -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './HomepageFeatures.module.css'; 4 | 5 | const FeatureList = [ 6 | { 7 | title: 'Code-First Scene Collections', 8 | description: ( 9 | <> 10 | Sceneify allows you to declare all of your scenes, sources, scene items, 11 | and filters in code. 12 | 13 | ), 14 | }, 15 | { 16 | title: 'Performant on Restarts', 17 | description: ( 18 | <> 19 | Sceneify keeps track of everything in OBS, allowing it to know what to 20 | skip creating each time you run your code 21 | 22 | ), 23 | }, 24 | { 25 | title: 'Incremental Adoption', 26 | description: ( 27 | <> 28 | Sceneify allows you to link existing OBS items to objects in code, 29 | and will not modify anything you don't give it access to. 30 | 31 | ), 32 | }, 33 | ]; 34 | 35 | function Feature({Svg, title, description}) { 36 | return ( 37 |
38 |
39 |

{title}

40 |

{description}

41 |
42 |
43 | ); 44 | } 45 | 46 | export default function HomepageFeatures() { 47 | return ( 48 |
49 |
50 |
51 | {FeatureList.map((props, idx) => ( 52 | 53 | ))} 54 |
55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /packages/sources/src/GDIPlusText.ts: -------------------------------------------------------------------------------- 1 | import { Font, Input, SourceFilters, CustomInputArgs } from "@sceneify/core"; 2 | 3 | export const GDITransform = { 4 | None: 0, 5 | Uppercase: 1, 6 | Lowercase: 2, 7 | Startcase: 3, 8 | }; 9 | 10 | export const GDIAlign = { 11 | Left: "left", 12 | Center: "center", 13 | Right: "right", 14 | }; 15 | 16 | export const GDIVAlign = { 17 | Top: "top", 18 | Center: "center", 19 | Bottom: "bottom", 20 | }; 21 | 22 | export type GDIPlusTextSourceSettings = { 23 | font: Font; 24 | use_file: boolean; 25 | text: string; 26 | file: string; 27 | antialiasing: boolean; 28 | transform: number; 29 | vertical: boolean; 30 | color: number; 31 | /** 0-100 */ 32 | opacity: number; 33 | gradient: boolean; 34 | gradient_color: number; 35 | gradient_opacity: number; 36 | /** 0-360 */ 37 | gradient_dir: number; 38 | bk_color: number; 39 | /** 0-100 */ 40 | bk_opacity: number; 41 | align: string; 42 | valign: string; 43 | outline: boolean; 44 | outline_size: number; 45 | outline_color: number; 46 | /** 0-100 */ 47 | outline_opacity: number; 48 | chatlog_mode: boolean; 49 | chatlog_lines: number; 50 | extents: boolean; 51 | extents_cx: number; 52 | extents_cy: number; 53 | extends_wrap: boolean; 54 | }; 55 | 56 | export type GDIPlusTextSourcePropertyLists = Pick< 57 | GDIPlusTextSourceSettings, 58 | "transform" | "align" | "valign" 59 | >; 60 | 61 | export class GDIPlusTextSource< 62 | Filters extends SourceFilters = {} 63 | > extends Input { 64 | constructor(args: CustomInputArgs) { 65 | super({ ...args, kind: "text_gdiplus_v2" }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Layout from "@theme/Layout"; 3 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 4 | 5 | import styles from "./index.module.css"; 6 | import HomepageFeatures from "../components/HomepageFeatures"; 7 | 8 | function HomepageHeader() { 9 | const { siteConfig } = useDocusaurusContext(); 10 | return ( 11 |
12 |
13 |
20 | logo 25 |

34 | {siteConfig.title} 35 |

36 |
37 |

44 | {siteConfig.tagline} 45 |

46 |
47 |
48 | ); 49 | } 50 | 51 | export default function Home() { 52 | return ( 53 | 56 | 57 |
58 | 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /packages/sources/src/Browser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeepPartial, 3 | Input, 4 | SourceFilters, 5 | SceneItem, 6 | Scene, 7 | CustomInputArgs, 8 | } from "@sceneify/core"; 9 | 10 | export type BrowserSourceSettings = { 11 | url: string; 12 | width: number; 13 | height: number; 14 | reroute_audio: boolean; 15 | }; 16 | 17 | export class BrowserSourceItem< 18 | Input extends BrowserSource 19 | > extends SceneItem { 20 | constructor(source: Input, scene: Scene, id: number, ref: string) { 21 | super(source, scene, id, ref); 22 | 23 | this.updateSizeFromSource(source.settings.width, source.settings.height); 24 | } 25 | } 26 | 27 | /** 28 | * **Warning**: BrowserSource items will not have correct properties when they are 29 | * initialized, as browser sources are always created with a width and height of 0. 30 | * If width and height are not provided in the source's intial settings, it's intial 31 | * item will have a width and height of 0 until item.getProperties is called, 32 | * or the source's width and height are updated. 33 | */ 34 | export class BrowserSource extends Input< 35 | BrowserSourceSettings, 36 | Filters 37 | > { 38 | constructor(args: CustomInputArgs) { 39 | super({ ...args, kind: "browser_source" }); 40 | } 41 | 42 | override createSceneItemObject( 43 | scene: Scene, 44 | id: number, 45 | ref: string 46 | ): BrowserSourceItem { 47 | return new BrowserSourceItem(this, scene, id, ref); 48 | } 49 | 50 | override async setSettings(settings: DeepPartial) { 51 | await super.setSettings(settings); 52 | 53 | this.itemInstances.forEach((item) => 54 | item.updateSizeFromSource(this.settings.width, this.settings.height) 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/OBS.ts: -------------------------------------------------------------------------------- 1 | import OBSWebsocket from "obs-websocket-js"; 2 | import { Scene, Input } from "./runtime.js"; 3 | 4 | export type LogLevel = "none" | "info" | "error"; 5 | 6 | export class OBS { 7 | ws: OBSWebsocket; 8 | logging: LogLevel; 9 | 10 | /** @internal */ 11 | syncedInputs = new Map>(); 12 | 13 | constructor() { 14 | this.ws = new OBSWebsocket(); 15 | this.logging = "info"; 16 | } 17 | 18 | async connect(url?: string, password?: string) { 19 | await this.ws.connect(url, password); 20 | } 21 | 22 | /* 23 | * @internal 24 | */ 25 | log(level: Omit, msg: string) { 26 | if (this.logging === "none") return; 27 | if (this.logging === "error" || level === "info") console.log(msg); 28 | } 29 | 30 | async getCurrentScene() { 31 | return await this.ws 32 | .call("GetCurrentProgramScene") 33 | .then((c) => c.currentProgramSceneName); 34 | } 35 | 36 | async setCurrentScene(scene: string | Scene) { 37 | await this.ws.call("SetCurrentProgramScene", { 38 | sceneName: typeof scene === "string" ? scene : scene.name, 39 | }); 40 | } 41 | 42 | async getPreviewScene() { 43 | return await this.ws 44 | .call("GetCurrentPreviewScene") 45 | .then((c) => c.currentPreviewSceneName); 46 | } 47 | 48 | async setPreviewScene(scene: string | Scene) { 49 | await this.ws.call("SetCurrentPreviewScene", { 50 | sceneName: typeof scene === "string" ? scene : scene.name, 51 | }); 52 | } 53 | 54 | async startStream() { 55 | await this.ws.call("StartStream"); 56 | } 57 | 58 | async stopStream() { 59 | await this.ws.call("StopStream"); 60 | } 61 | 62 | async toggleStream() { 63 | return await this.ws.call("ToggleStream").then((r) => r.outputActive); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Sceneify logo 3 |

4 | 5 |

Sceneify

6 |

The easiest way to control OBS from JavaScript

7 | 8 |

9 | 10 | Downloads 11 | 12 | 13 | Core Version 14 | 15 | 16 | Build Size 17 | 18 |

19 | 20 | Using `obs-websocket` can be difficult. Small manipulations of scenes and scene items are manageable, but keeping track of scenes, sources, settings, filters and more can quickly become a daunting task. 21 | 22 | Sceneify aims to fix this. By working with `Scene`, `Source`, and `SceneItem` objects, you can have unparalleled control over your OBS layouts. 23 | 24 | # Beta Warning 25 | 26 | This library is not well tested and is still under heavy development. Feel free to use it, but make sure you make a backup of your scene collections before doing anything with Sceneify. 27 | 28 | ## Features 29 | 30 | - Persistence across code reloads, so scenes and items aren't deleted and recreated each time you run your code 31 | - `Scene`, `Source` and `SceneItem` are designed to be overridden, allowing for complex layouts to be abstracted into subclasses 32 | - Easy integration into existing layouts with `Scene.link()`, allowing for incremental migration to Sceneify without handing over your entire layout to your code. -------------------------------------------------------------------------------- /website/docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: /installation 4 | --- 5 | 6 | # Installation 7 | 8 | First of all, you will need to install [OBS Websocket v5](https://github.com/obsproject/obs-websocket/releases/latest) if you do not have it installed. 9 | 10 | ## Core 11 | 12 | Then, install [@sceneify/core](/api/core) with your package manager of choice: 13 | 14 | import Tabs from '@theme/Tabs'; 15 | import TabItem from '@theme/TabItem'; 16 | 17 | 18 | 19 | 20 | ``` 21 | npm i @sceneify/core@beta 22 | ``` 23 | 24 | 25 | 26 | 27 | ``` 28 | yarn add @sceneify/core@beta 29 | ``` 30 | 31 | 32 | 33 | 34 | ``` 35 | pnpm i @sceneify/core@beta 36 | ``` 37 | 38 | 39 | 40 | 41 | ## Extra Packages 42 | 43 | Technically, [@sceneify/core](/api/core) is all you need to use Sceneify, but there are more packages that help to improve the experience: 44 | 45 | - [@sceneify/sources](/api/sources) : Types and implementations for all of OBS's default sources, including special implementations for [BrowserSource](/api/sources/class/BrowserSource) 46 | - [@sceneify/filters](/api/filters): Types and implementations for all of OBS's default filters 47 | 48 | These can be installed in the same way as before: 49 | 50 | 51 | 52 | 53 | ``` 54 | npm i @sceneify/sources@beta @sceneify/filters@beta 55 | ``` 56 | 57 | 58 | 59 | 60 | ``` 61 | yarn add @sceneify/sources@beta @sceneify/filters@beta 62 | ``` 63 | 64 | 65 | 66 | 67 | ``` 68 | pnpm i @sceneify/sources@beta @sceneify/filters@beta 69 | ``` 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /website/docs/guides/scene.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | slug: /scene 4 | --- 5 | 6 | # Scene 7 | 8 | To create an empty scene, first declare it with and give it a name: 9 | 10 | ```ts 11 | const someScene = new Scene({ 12 | name: "Scene Name", // Name of the scene in OBS 13 | // Must be unique among all sources and scenes 14 | }); 15 | ``` 16 | 17 | Then, call [create()](/api/core/class/Scene#create) and provide an instance of [OBS](/api/core/class/OBS) the scene should be created in: 18 | 19 | ```ts 20 | await someScene.create(obs); 21 | ``` 22 | 23 | ## Items 24 | 25 | Scenes accept an `items` property, which is a map that describes how each of the scene's items should be created. 26 | 27 | The keys of this map are what Sceneify calls `refs`, and are used to uniquely identify each item of a scene. 28 | 29 | The values of this map are [scene item schemas](/api/core#SceneItemSchema), which specify the properties that the items should be created with, 30 | as well as the source they are instances of. 31 | The source does not have to exist in OBS, nor does it even have to be initialized. This will all be done by the scene when it is created. 32 | 33 | ```ts 34 | new Scene({ 35 | items: { 36 | // someItem will be the ref of this item 37 | someItem: { 38 | source: someSource, // The item's source must be provided. 39 | // Specify transform fields, enabled etc... 40 | scaleX: 1, 41 | enabled: true, 42 | }, 43 | }, 44 | name: "Scene Name", 45 | }); 46 | ``` 47 | 48 | After the scene is created, its [items](/api/core/class/Scene#items) array will be populated with all of its items, and each item can be accessed by ref using [item()](/api/core/class/Scene#item). 49 | 50 | ## Properties 51 | 52 | As scenes are also sources, they share many similar properties as [Inputs](/api/core/class/Input): 53 | 54 | - [name](/api/core/class/Scene#name): The name of the scene 55 | - [kind](/api/core/class/Scene#kind): The kind of the scene (will always be `scene`) -------------------------------------------------------------------------------- /website/docs/guides/sceneitem.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | slug: /sceneitem 4 | --- 5 | 6 | # SceneItem 7 | 8 | After creating a scene, you can access specific items using [item()](/api/core/class/Scene#item). 9 | Simply provide an item's `ref` and it will return the [item](/api/core/class/SceneItem) or `undefined` if there is no item with the provided `ref`. 10 | 11 | ```ts 12 | const someItem = someScene.item("someItem"); 13 | ``` 14 | 15 | ## Creating Items Dynamically 16 | 17 | Scene items don't have to be created at the same time as a scene. 18 | You may use [createItem()](/api/core/class/Scene#createItem) to create an item from a [schema](/api/core#SceneItemSchema) at any time after a scene has been created. 19 | 20 | ```ts 21 | await scene.createItem("anotherRef", { 22 | source: anotherSource, 23 | positionX: 0, 24 | enabled: false, 25 | }); 26 | ``` 27 | 28 | This function is used internally by [create()](/api/core/class/Scene#create), so you can be certain that creation of the item will perform the same way as if it was created at the same time as the scene. 29 | 30 | ## Removing 31 | 32 | Scene items can be removed from their containing scene with [remove()](/api/core/class/SceneItem#remove) 33 | 34 | ```ts 35 | await someItem.remove(); 36 | ``` 37 | 38 | ## Properties 39 | 40 | - [scene](/api/core/class/SceneItem#scene) - The scene that the item exists in 41 | - [source](/api/core/class/SceneItem#source) - The source that the item is an instance of 42 | - [id](/api/core/class/SceneItem#id) - The id of the item in OBS 43 | - [ref](/api/core/class/SceneItem#ref) - The ref of the item used by Sceneify 44 | 45 | These properties can be set with their associated functions: 46 | 47 | - [transform](/api/core/class/SceneItem#transform) - [setTransform](/api/core/class/SceneItem#setTransform) 48 | - [enabled](/api/core/class/SceneItem#enabled) - [setEnabled](/api/core/class/SceneItem#setEnabled) 49 | - [locked](/api/core/class/SceneItem#locked) - [setLocked](/api/core/class/SceneItem#setLocked) 50 | -------------------------------------------------------------------------------- /scripts/setPackageVersions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Updates the package.json files with a version to release to npm under 3 | * the main tag. 4 | * 5 | * Based on https://github.com/facebook/relay/blob/main/gulpfile.js 6 | */ 7 | 8 | const fs = require("fs/promises"); 9 | const path = require("path"); 10 | 11 | const RELEASE_COMMIT_SHA = process.env.RELEASE_COMMIT_SHA; 12 | 13 | if (RELEASE_COMMIT_SHA && RELEASE_COMMIT_SHA.length !== 40) { 14 | throw new Error( 15 | "If the RELEASE_COMMIT_SHA env variable is set, it should be set to the " + 16 | "40 character git commit hash." 17 | ); 18 | } 19 | 20 | const VERSION = RELEASE_COMMIT_SHA 21 | ? `0.0.0-main-${RELEASE_COMMIT_SHA.substring(0, 8)}` 22 | : process.env.npm_package_version; 23 | 24 | console.log(RELEASE_COMMIT_SHA) 25 | 26 | async function main() { 27 | const packages = await fs.readdir(path.join(__dirname, "../packages")); 28 | const pkgJsons = {}; 29 | const pkgJsonPaths = {}; 30 | 31 | for (pkg of packages) { 32 | const pkgJsonPath = path.join( 33 | __dirname, 34 | "../packages", 35 | pkg, 36 | "package.json" 37 | ); 38 | pkgJsonPaths[pkg] = pkgJsonPath; 39 | const packageJson = JSON.parse(await fs.readFile(pkgJsonPath, "utf8")); 40 | 41 | pkgJsons[pkg] = packageJson; 42 | } 43 | 44 | const packageNames = Object.values(pkgJsons).map((pkg) => pkg.name); 45 | 46 | for (const pkg of packages) { 47 | let packageJson = pkgJsons[pkg]; 48 | 49 | packageJson.version = VERSION; 50 | for (const depKind of [ 51 | "dependencies", 52 | "devDependencies", 53 | "peerDependencies", 54 | ]) { 55 | const deps = packageJson[depKind]; 56 | for (const dep in deps) { 57 | if (packageNames.includes(dep)) { 58 | deps[dep] = VERSION; 59 | } 60 | } 61 | } 62 | await fs.writeFile( 63 | pkgJsonPaths[pkg], 64 | JSON.stringify(packageJson, null, 2) + "\n", 65 | "utf8" 66 | ); 67 | } 68 | } 69 | 70 | main(); 71 | -------------------------------------------------------------------------------- /examples/extend_scene/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | Alignment, 4 | SceneItem, 5 | SourceFilters, 6 | OBS, 7 | } from "@sceneify/core"; 8 | import { ImageSource } from "@sceneify/sources"; 9 | 10 | export class WindowItem extends SceneItem { 11 | constructor(source: W, scene: Scene, id: number, ref: string) { 12 | super(source, scene, id, ref); 13 | } 14 | } 15 | 16 | interface Args { 17 | name: string; 18 | contentScene: TContentScene; 19 | boundsItem: TContentScene extends Scene ? keyof I : never; 20 | icon: ImageSource; 21 | filters?: Filters; 22 | } 23 | 24 | class Window< 25 | TContentScene extends Scene = any, 26 | Filters extends SourceFilters = SourceFilters 27 | > extends Scene<{}, Filters> { 28 | constructor({ name, filters }: Args) { 29 | super({ 30 | name: `${name} Window`, 31 | items: {}, 32 | filters, 33 | }); 34 | } 35 | 36 | createSceneItemObject( 37 | scene: Scene, 38 | id: number, 39 | ref: string 40 | ): WindowItem { 41 | return new WindowItem(this, scene, id, ref); 42 | } 43 | } 44 | 45 | async function main() { 46 | const obs = new OBS(); 47 | 48 | // Connect to OBS before creating or linking any scenes 49 | await obs.connect("ws:localhost:4455"); 50 | 51 | let a = new Window({ 52 | name: "A", 53 | contentScene: new Scene({ 54 | name: "A Content", 55 | items: { 56 | test: { 57 | source: new ImageSource({ 58 | name: "Test", 59 | }), 60 | }, 61 | }, 62 | }), 63 | icon: new ImageSource({ 64 | name: "A Icon", 65 | }), 66 | boundsItem: "test", 67 | }); 68 | 69 | let scene = new Scene({ 70 | name: "test", 71 | items: { 72 | window: { 73 | source: a, 74 | }, 75 | }, 76 | }); 77 | 78 | let item = await scene.createItem("test", { 79 | source: a, 80 | }); 81 | } 82 | 83 | main(); 84 | -------------------------------------------------------------------------------- /website/docs/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Overview 6 | 7 | Sceneify is a collection of libraries for NodeJS and the browser for creating, and manipulating items in OBS. With a consistent API and easy extensibility, it makes interacting with OBS through OBS Websocket much simpler and more approachable. 8 | 9 | :::caution 10 | 11 | This documentation is for the upcoming 1.0.0 release of Sceneify that uses OBS Websocket v5. 12 | 13 | ::: 14 | 15 | :::note 16 | 17 | This documentation assumes that you are familar with OBS, including terminology, unique name requirements of sources and filters, and how items, sources and filters are created and removed. 18 | 19 | ::: 20 | 21 | ## Motivation 22 | 23 | OBS is a very powerful piece of software, being capable of compositing, mixing, encoding and streaming live and recorded video. OBS Websocket pushes this to the extreme, allowing for full control of almost every aspect of an OBS instance. That is, as long as you are capable and willing to write the code required to tell the websocket what to do. This can be simple if you're just trying to automate scene switching, mute an audio source or some other small task, but can become quite a daunting task with more complex software and layouts. 24 | 25 | Sceneify was created out of a desire to simplify the process of using OBS Websocket. It is primarily designed to make OBS scripting more focused on what OBS should do, and less about how it's done. In this sense, it could be considered a declarative abstraction over OBS Websocket. 26 | 27 | Much of the development of Sceneify was done in private by Brendonovich for Twitch streamer JDudeTV, who before using Sceneify had a massive JavaScript file that meticulously controlled everything on his stream through OBS Websocket. It didn't take long for the script to become almost unmaintainable, and a new solution was needed. 28 | 29 | Now, JDudeTV uses Sceneify for creating his entire OBS layout, animating it and making every scene item part of a physics world. This project alone is proof that Sceneify works and has the ability to drastically simplify the process of controlling OBS with code. 30 | -------------------------------------------------------------------------------- /packages/sources/src/Media.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | SourceFilters, 4 | CustomInputArgs, 5 | OBSEventTypes, 6 | OBS, 7 | VideoRange, 8 | } from "@sceneify/core"; 9 | import { EventEmitter } from "eventemitter3"; 10 | 11 | export type MediaSourceSettings = { 12 | is_local_file: boolean; 13 | local_file: string; 14 | looping: boolean; 15 | restart_on_activate: boolean; 16 | buffering_mb: number; 17 | input: string; 18 | input_format: string; 19 | reconnect_delay_sec: number; 20 | hw_decode: boolean; 21 | clear_on_media_end: boolean; 22 | close_when_inactive: boolean; 23 | speed_percent: number; 24 | color_range: VideoRange; 25 | linear_alpha: boolean; 26 | seekable: boolean; 27 | }; 28 | 29 | export interface MediaSourceEvents { 30 | PlaybackStarted: void; 31 | PlaybackEnded: void; 32 | } 33 | 34 | export class MediaSource extends Input< 35 | MediaSourceSettings, 36 | Filters 37 | > { 38 | private emitter = new EventEmitter(); 39 | 40 | constructor(args: CustomInputArgs) { 41 | super({ ...args, kind: "ffmpeg_source" }); 42 | } 43 | 44 | private emit(event: keyof MediaSourceEvents) { 45 | this.emitter.emit(event); 46 | } 47 | 48 | startedListener = (args: OBSEventTypes["MediaInputPlaybackStarted"]) => { 49 | if (this.name === args.inputName) { 50 | this.emit("PlaybackStarted"); 51 | } 52 | }; 53 | 54 | endedListener = (args: OBSEventTypes["MediaInputPlaybackEnded"]) => { 55 | if (this.name === args.inputName) { 56 | this.emit("PlaybackEnded"); 57 | } 58 | }; 59 | 60 | override async initialize(obs: OBS) { 61 | await super.initialize(obs); 62 | obs.on("MediaInputPlaybackStarted", this.startedListener); 63 | obs.on("MediaInputPlaybackEnded", this.endedListener); 64 | } 65 | 66 | override async remove() { 67 | await super.remove(); 68 | 69 | this.obs.off("MediaInputPlaybackStarted", this.startedListener); 70 | this.obs.off("MediaInputPlaybackEnded", this.endedListener); 71 | } 72 | 73 | on(event: keyof MediaSourceEvents, fn: () => void) { 74 | return this.emitter.on(event, fn); 75 | } 76 | 77 | off(event: keyof MediaSourceEvents, fn: () => void) { 78 | return this.emitter.off(event, fn); 79 | } 80 | 81 | once(event: keyof MediaSourceEvents, fn: () => void) { 82 | return this.emitter.once(event, fn); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /website/docs/guides/input.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: /input 4 | --- 5 | 6 | # Input 7 | 8 | [Inputs](/api/core/class/Input) can be declared by calling `new Input` with the appropriate arguments. 9 | 10 | ```ts 11 | const imageSource = new Input({ 12 | kind: "image_source", // The kind of the input 13 | name: "Image Input", // Name of the input in OBS 14 | // Must be unique among all sources 15 | }); 16 | ``` 17 | 18 | The [sources pacakge](/api/sources) contains helper classes that have type definitions and input kinds built into them. 19 | 20 | import Tabs from '@theme/Tabs'; 21 | import TabItem from '@theme/TabItem'; 22 | 23 | 24 | 25 | 26 | ```js 27 | const { ImageSource } = require("@sceneify/sources"); 28 | 29 | const imageSource = new ImageSource({ 30 | name: "Image Source", 31 | // kind is no longer necessary, since it is provided by ImageSource 32 | }); 33 | ``` 34 | 35 | 36 | 37 | 38 | ```ts 39 | import { ImageSource } from "@sceneify/sources"; 40 | 41 | const imageSource = new ImageSource({ 42 | name: "Image Source", 43 | // Kind is no longer necessary, since it is provided by ImageSource 44 | }); 45 | ``` 46 | 47 | 48 | 49 | 50 | ## Inputs Don't Create Themselves 51 | 52 | Declaring a source with `new Input` does not create the source in OBS. In fact, inputs never create themselves. Instead, it is the responsibility of scenes to detect if an input already exists, and create it if necessary. 53 | 54 | Inputs have two properties that you can use to check if they have been processed by a scene: 55 | 56 | - [exists](/api/core/class/Input#exists): Whether the input exists in OBS 57 | - [initialized](/api/core/class/Input#initialzed): Whether a scene has checked if the input exists in OBS 58 | 59 | ## Properties 60 | 61 | In addition to the two properties above, inputs have a number of other properties: 62 | 63 | - [name](/api/core/class/Input#name): The name of the input 64 | - [kind](/api/core/class/Input#kind): The kind of the input 65 | 66 | These properties can be set with their associated functions: 67 | 68 | - [settings](/api/core/class/Input#settings) - [setSettings](/api/core/class/Input#setSettings) 69 | - [volume](/api/core/class/Input#volume) - [setVolume](/api/core/class/Input#volume) 70 | - [audioMonitorType](/api/core/class/Input#audioMonitorType) - [setAudioMonitorType](/api/core/class/Input#setAudioMonitorType) 71 | - [audioSyncOffset](/api/core/class/Input#audioSyncOffset) - [setAudioSyncOffset](/api/core/class/Input#setAudioSyncOffset) 72 | - [muted](/api/core/class/Input#muted) - [setMuted](/api/core/class/Input#setMuted) 73 | -------------------------------------------------------------------------------- /packages/core/src/SceneItem.ts: -------------------------------------------------------------------------------- 1 | export interface SceneItemTransform { 2 | sourceWidth: number; 3 | sourceHeight: number; 4 | 5 | positionX: number; 6 | positionY: number; 7 | 8 | rotation: number; 9 | 10 | scaleX: number; 11 | scaleY: number; 12 | 13 | width: number; 14 | height: number; 15 | 16 | alignment: Alignment; 17 | 18 | boundsType: BoundsType; 19 | boundsAlignment: Alignment; 20 | boundsWidth: number; 21 | boundsHeight: number; 22 | 23 | cropLeft: number; 24 | cropTop: number; 25 | cropRight: number; 26 | cropBottom: number; 27 | } 28 | 29 | export type SceneItemTransformInput = Partial< 30 | Omit< 31 | SceneItemTransform, 32 | | "width" 33 | | "height" 34 | | "sourceWidth" 35 | | "sourceHeight" 36 | | "boundsWidth" 37 | | "boundsHeight" 38 | > 39 | >; 40 | 41 | export function sceneItemTransformToOBS( 42 | transform: Partial 43 | ): Partial { 44 | return { 45 | ...transform, 46 | alignment: transform.alignment 47 | ? alignmentToOBS(transform.alignment) 48 | : undefined, 49 | boundsAlignment: transform.boundsAlignment 50 | ? alignmentToOBS(transform.boundsAlignment) 51 | : undefined, 52 | boundsType: transform.boundsType 53 | ? boundsTypeToOBS(transform.boundsType) 54 | : undefined, 55 | }; 56 | } 57 | 58 | type Alignment = 59 | | "centerLeft" 60 | | "center" 61 | | "centerRight" 62 | | "topLeft" 63 | | "top" 64 | | "topRight" 65 | | "bottomLeft" 66 | | "bottom" 67 | | "bottomRight"; 68 | 69 | import { 70 | OBSAlignment, 71 | OBSBoundsType, 72 | OBSSceneItemTransform, 73 | } from "./obs-types.js"; 74 | 75 | const Alignment: Record = { 76 | centerLeft: 1, 77 | center: 0, 78 | centerRight: 2, 79 | topLeft: 5, 80 | top: 4, 81 | topRight: 6, 82 | bottomLeft: 9, 83 | bottom: 8, 84 | bottomRight: 10, 85 | }; 86 | 87 | export function alignmentToOBS(alignment: Alignment): OBSAlignment { 88 | return Alignment[alignment]; 89 | } 90 | 91 | type BoundsType = 92 | | "none" 93 | | "stretch" 94 | | "scaleInner" 95 | | "scaleOuter" 96 | | "scaleToWidth" 97 | | "scaleToHeight" 98 | | "maxOnly"; 99 | 100 | const BoundsType: Record = { 101 | none: "OBS_BOUNDS_NONE", 102 | stretch: "OBS_BOUNDS_STRETCH", 103 | scaleInner: "OBS_BOUNDS_SCALE_INNER", 104 | scaleOuter: "OBS_BOUNDS_SCALE_OUTER", 105 | scaleToWidth: "OBS_BOUNDS_SCALE_TO_WIDTH", 106 | scaleToHeight: "OBS_BOUNDS_SCALE_TO_HEIGHT", 107 | maxOnly: "OBS_BOUNDS_MAX_ONLY", 108 | }; 109 | 110 | export function boundsTypeToOBS(boundsType: BoundsType): OBSBoundsType { 111 | return BoundsType[boundsType]; 112 | } 113 | -------------------------------------------------------------------------------- /translations/README_es.md: -------------------------------------------------------------------------------- 1 | # `sceneify` 2 | 3 | ### _La forma más sencilla de controlar OBS desde JS 🎥_ 4 | 5 | [![Downloads](https://img.shields.io/npm/dt/sceneify.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/sceneify) 6 | [![Downloads](https://img.shields.io/npm/v/sceneify.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/sceneify) 7 | [![Build Size](https://img.shields.io/bundlephobia/min/sceneify?label=bundle%20size&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/result?p=sceneify) 8 | 9 | Usar `obs-websocket` puede ser complicado. Pequeñas manipulaciones de escenas y elementos de escena son manejables , pero hacer un seguimiento de las escenas, las fuentes, la configuración, los filtros y más puede convertirse rápidamente en una tarea abrumadora. 10 | 11 | `sceneify` tiene como objetivo solucionar este problema. Al trabajar con los objetos `Scene`,` Source` y `SceneItem`, puedes tener un control incomparable sobre sus diseños OBS. 12 | 13 | Si está familiarizado con las bases de datos, ¡es como un ORM para OBS! 14 | 15 | # Advertencia Beta 16 | 17 | Esta biblioteca no está bien probada y todavía se encuentra en intenso desarrollo. Siéntase libre de usarlo, pero asegúrese de hacer una copia de seguridad de sus colecciones de escenas antes de hacer cualquier cosa con `sceneify`. 18 | 19 | ## Características 20 | 21 | - Persistencia en las recargas de código, por lo que las escenas y los elementos no se eliminan ni se vuelven a crear cada vez que ejecuta su código 22 | - Solicitud automática por lotes 23 | - `Scene`,` Source` y `SceneItem` están diseñados para ser anulados, lo que permite abstraer diseños complejos en subclases 24 | - Fácil integración en diseños existentes con `Scene.link ()`, lo que permite una migración incremental a `sceneify` sin entregar todo el diseño a su código 25 | 26 | ## Instalación 27 | 28 | 1. Instale el [fork de `obs-websocket`](https://github.com/MemedowsTeam/obs-websocket/releases) 29 | 30 | `sceneify` expone alguna funcionalidad (por ejemplo,` obs.clean () `,` Scene.remove () `) que requiere la instalación del fork personalizado de` obs-websocket`. Este fork simplemente agrega soporte para eliminar 31 | escenas, conservando todas las demás funciones anteriores. `sceneify` admitirá ` obs-websocket` v5 cuando se lance, que tiene soporte nativo para eliminar escenas, y también v4 para compatibilidad con versiones anteriores. 32 | 33 | 2. Instale `sceneify` 34 | 35 | ``` 36 | yarn add sceneify 37 | ``` 38 | 39 | o npm 40 | 41 | ``` 42 | npm install sceneify 43 | ``` 44 | 45 | Si estás usando typescript, asegúrese de estar usando al menos `typescript@4.4.0`, ya que `sceneify` usa algunas características para proporcionar tipos más precisos para solicitudes y eventos.. 46 | 47 | 3. Conéctese a OBS. Consulte la carpeta de ejemplo para obtener más información. 48 | 49 | ## Agradecimientos 50 | 51 | - [JDudeTV](https://twitch.tv/jdudetv) por ser el catalizador de este proyecto, ayudar con el desarrollo y utilizarlo en la producción de su flujo. 52 | - [HannahGBS](https://twitter.com/hannah_gbs) por agregar soporte RemoveScene al fork `obs-websocket` y ayudar con el desarrollo y la documentación de fuentes y tipos de filtros. 53 | - [lclc98](https://github.com/lclc98) por ayudar a los tipos de filtro y fuente de documentos 54 | -------------------------------------------------------------------------------- /packages/animation/src/obs-animation.ts: -------------------------------------------------------------------------------- 1 | import { Filter, Input, OBS, SceneItem, Source } from "@sceneify/core"; 2 | import { AnimationSubject } from "."; 3 | 4 | type SerializedTarget = 5 | | { type: SubjectType.SceneItem; sceneItemId: number; sceneName: string } 6 | | { type: SubjectType.Source; sourceName: string } 7 | | { type: SubjectType.Filter; sourceName: string; filterName: string }; 8 | 9 | interface SetKeyframesRequest { 10 | targets: ({ 11 | animations: { 12 | property: string; 13 | keyframes: { timestamp: number; value: number }[]; 14 | }[]; 15 | } & SerializedTarget)[]; 16 | } 17 | 18 | function getSubjectOBS(subject: AnimationSubject) { 19 | if (subject instanceof Source) return subject.obs; 20 | else return subject.source.obs; 21 | } 22 | 23 | enum SubjectType { 24 | SceneItem = 0, 25 | Source = 1, 26 | Filter = 2, 27 | } 28 | 29 | function serializeSubject(subject: AnimationSubject): SerializedTarget { 30 | if (subject instanceof Source) 31 | return { 32 | type: SubjectType.Source, 33 | sourceName: subject.name, 34 | }; 35 | else if (subject instanceof SceneItem) 36 | return { 37 | type: SubjectType.SceneItem, 38 | sceneItemId: subject.id, 39 | sceneName: subject.scene.name, 40 | }; 41 | else 42 | return { 43 | type: SubjectType.Filter, 44 | sourceName: subject.source.name, 45 | filterName: subject.name, 46 | }; 47 | } 48 | 49 | export const keyframes = ( 50 | source: S, 51 | animations: AnimationTarget["animations"] 52 | ): AnimationTarget => ({ source, animations }); 53 | 54 | type AnimatableProperties = S extends Input | Filter 55 | ? S["settings"] 56 | : S extends SceneItem 57 | ? S["transform"] 58 | : never; 59 | 60 | type AnimationTarget = { 61 | source: S; 62 | animations: { 63 | [P in keyof AnimatableProperties]?: { 64 | [timestamp: number]: number; 65 | }; 66 | }; 67 | }; 68 | 69 | export async function pluginAnimate< 70 | Targets extends AnimationTarget[] 71 | >(targets: Targets) { 72 | const subjectsMap = new Map[]>(); 73 | 74 | for (const target of targets) { 75 | const obs = getSubjectOBS(target.source); 76 | const subjects = subjectsMap.get(obs) || []; 77 | subjects.push(target); 78 | subjectsMap.set(obs, subjects); 79 | } 80 | 81 | for (const [obs, targets] of subjectsMap) { 82 | const data: SetKeyframesRequest = { 83 | targets: targets.map((target) => ({ 84 | ...serializeSubject(target.source), 85 | animations: Object.entries(target.animations).map( 86 | ([property, keyframes]) => ({ 87 | property, 88 | keyframes: Object.entries(keyframes as Record).map( 89 | ([timeStr, value]) => ({ 90 | timestamp: parseInt(timeStr), 91 | value, 92 | }) 93 | ), 94 | }) 95 | ), 96 | })), 97 | }; 98 | 99 | await obs.call("CallVendorRequest", { 100 | requestType: "SetAnimation", 101 | vendorName: "obs-animation", 102 | requestData: data as any, 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | const lightCodeTheme = require("prism-react-renderer/themes/github"); 2 | const darkCodeTheme = require("prism-react-renderer/themes/dracula"); 3 | const path = require("path"); 4 | 5 | // With JSDoc @type annotations, IDEs can provide config autocompletion 6 | /** @type {import('@docusaurus/types').DocusaurusConfig} */ 7 | module.exports = { 8 | title: "Sceneify", 9 | tagline: "The easiest way to control OBS from JavaScript", 10 | url: "https://sceneify.brendonovich.dev", 11 | baseUrl: "/", 12 | onBrokenLinks: "throw", 13 | onBrokenMarkdownLinks: "warn", 14 | favicon: "img/favicon.ico", 15 | organizationName: "Brendonovich", 16 | projectName: "sceneify", 17 | presets: [ 18 | [ 19 | "@docusaurus/preset-classic", 20 | /** @type {import('@docusaurus/preset-classic').Options} */ 21 | ({ 22 | docs: { 23 | sidebarPath: require.resolve("./sidebars.js"), 24 | editUrl: 25 | "https://github.com/Brendonovich/sceneify/edit/master/website/", 26 | }, 27 | theme: { 28 | customCss: require.resolve("./src/css/custom.css"), 29 | }, 30 | }), 31 | ], 32 | ], 33 | plugins: [ 34 | [ 35 | "docusaurus-plugin-typedoc-api", 36 | { 37 | projectRoot: path.join(__dirname, "../"), 38 | packages: ["core", "sources", "filters", "animation"].map( 39 | (pkg) => `packages/${pkg}` 40 | ), 41 | tsconfigName: "tsconfig.docs.json", 42 | }, 43 | ], 44 | ], 45 | 46 | themeConfig: 47 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 48 | ({ 49 | image: "img/twitter.png", 50 | metadata: [{ name: "twitter:card", content: "summary" }], 51 | navbar: { 52 | title: "Sceneify", 53 | items: [ 54 | { 55 | type: "doc", 56 | docId: "overview", 57 | position: "left", 58 | label: "Docs", 59 | }, 60 | { 61 | to: "api", 62 | label: "API", 63 | position: "left", 64 | }, 65 | { 66 | href: "https://github.com/Brendonovich/sceneify", 67 | label: "GitHub", 68 | position: "right", 69 | }, 70 | ], 71 | }, 72 | footer: { 73 | links: [ 74 | { 75 | title: "Docs", 76 | items: [ 77 | { 78 | label: "Overview", 79 | to: "/docs/overview", 80 | }, 81 | ], 82 | }, 83 | { 84 | title: "Links", 85 | items: [ 86 | { 87 | label: "GitHub", 88 | href: "https://github.com/Brendonovich/sceneify", 89 | }, 90 | { 91 | label: "Twitter", 92 | href: "https://twitter.com/brendonovich1", 93 | }, 94 | ], 95 | }, 96 | ], 97 | copyright: ` 98 | Copyright © ${new Date().getFullYear()} Brendonovich
99 | Sceneify is in no way affiliated with OBS Studio or the OBS Project 100 | `, 101 | }, 102 | prism: { 103 | theme: lightCodeTheme, 104 | darkTheme: darkCodeTheme, 105 | }, 106 | }), 107 | }; 108 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'packages/**' 8 | tags: 9 | - '[0-9]+.[0-9]+.[0-9]+*' 10 | pull_request: 11 | branches: [main] 12 | paths: 13 | - 'packages/**' 14 | 15 | jobs: 16 | build: 17 | name: Build 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: pnpm/action-setup@v2.0.1 22 | with: 23 | version: 6.24.2 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: '16' 27 | cache: 'pnpm' 28 | - name: Install dependencies 29 | run: pnpm i --frozen-lockfile 30 | 31 | - name: Build 32 | run: pnpm build-all 33 | 34 | - uses: actions/upload-artifact@v2 35 | with: 36 | name: build-artifacts 37 | path: | 38 | packages/*/cjs 39 | packages/*/esm 40 | packages/*/lib 41 | packages/*/dts 42 | 43 | test: 44 | name: Test 45 | runs-on: ubuntu-latest 46 | 47 | # strategy: 48 | # fail-fast: false 49 | # matrix: 50 | # # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 51 | # node-version: [12.x, 14.x, 16.x, 17.x] 52 | 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: pnpm/action-setup@v2.0.1 56 | with: 57 | version: 6.24.2 58 | - uses: actions/setup-node@v2 59 | with: 60 | node-version: '16' 61 | cache: 'pnpm' 62 | - name: Install dependencies 63 | run: pnpm i --frozen-lockfile 64 | - run: pnpm test -- --verbose 65 | 66 | publish: 67 | name: 'Publish to NPM' 68 | runs-on: ubuntu-latest 69 | if: github.event_name == 'push' && github.repository == 'brendonovich/sceneify' 70 | needs: [build, test] 71 | steps: 72 | - uses: actions/checkout@v2 73 | 74 | - uses: pnpm/action-setup@v2.0.1 75 | with: 76 | version: 6.24.2 77 | 78 | - uses: actions/setup-node@v2 79 | with: 80 | node-version: '16' 81 | cache: 'pnpm' 82 | 83 | - name: Download build 84 | uses: actions/download-artifact@v2 85 | with: 86 | name: build-artifacts 87 | path: packages 88 | 89 | - name: Configure main version 90 | if: github.ref == 'refs/heads/main' 91 | run: pnpm set-package-versions 92 | env: 93 | RELEASE_COMMIT_SHA: ${{ github.sha }} 94 | 95 | - name: Publish main to npm 96 | if: github.ref == 'refs/heads/main' 97 | run: pnpm publish -r ${TAG} --no-git-checks --filter "@sceneify/*" --access public 98 | env: 99 | TAG: ${{ (github.ref == 'refs/heads/main' && '--tag=main') || '' }} 100 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 101 | 102 | - name: Configure release version 103 | if: github.ref_type == 'tag' 104 | run: pnpm set-package-versions 105 | 106 | - name: Publish release to npm 107 | if: github.ref_type == 'tag' 108 | run: pnpm publish -r ${TAG} --no-git-checks --filter "@sceneify/*" --access public 109 | env: 110 | TAG: ${{ (contains(github.ref_name, '-beta.') && '--tag=beta') || ''}} 111 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 112 | -------------------------------------------------------------------------------- /website/docs/guides/filter.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | slug: /filter 4 | --- 5 | 6 | # Filter 7 | 8 | [Filters](/api/core/class/Filter) can be declared in a similar way to sources, by calling `new Filter` with the appropriate arguments. 9 | 10 | ```ts 11 | const colorCorrectionFilter = new Filter({ 12 | kind: "color_filter_v2", // The kind of the filter 13 | name: "Filter", // Name of the filter in OBS 14 | // Must be unique within the filters of 15 | // the source it will be created for 16 | }); 17 | ``` 18 | 19 | The [filters package](/api/filters) contains helper classes that have type definitions and filter kinds built into them. 20 | 21 | import Tabs from '@theme/Tabs'; 22 | import TabItem from '@theme/TabItem'; 23 | 24 | 25 | 26 | 27 | ```js 28 | const { ColorCorrectionFilter } = require("@simple-obs/filters"); 29 | 30 | const colorCorrectionFilter = new ColorCorrectionFilter({ 31 | name: "Filter", 32 | // kind is no longer necessary, since it is 33 | // provided by ColorCorrectionFilter 34 | }); 35 | ``` 36 | 37 | 38 | 39 | 40 | ```ts 41 | import { ColorCorrectionFilter } from "@simple-obs/filters"; 42 | 43 | const colorCorrectionFilter = new ColorCorrectionFilter({ 44 | name: "Filter", 45 | // Kind is no longer necessary, since it is provided by ColorCorrectionFilter 46 | }); 47 | ``` 48 | 49 | 50 | 51 | 52 | ## Create Source with Filter 53 | 54 | [Sources](/api/core/class/Source) (and [Scenes](/api/core/class/Source), since they are also sources) take a map of filters as an argument when being created to specify default filters that the source should be created with. 55 | 56 | ```ts 57 | const imageSource = new ImageSource({ 58 | name: "Some Source", 59 | filters: { 60 | colorCorrection: new ColorCorrectionFilter({ 61 | name: "Filter", 62 | }), 63 | }, 64 | }); 65 | 66 | const scene = new Scene({ 67 | name: "Some Scene", 68 | items: { 69 | image: { 70 | source: imageSource, 71 | }, 72 | }, 73 | filters: { 74 | colorCorrection: new ColorCorrectionFilter({ 75 | name: "Filter", 76 | }), 77 | }, 78 | }); 79 | ``` 80 | 81 | The keys of this map are for acceessing the filters through the source's [filter()](/api/core/class/Source#filter) method, and though it does not have the same function as a `ref`, it must uniquely identify the filter within the source it is assigned to. 82 | 83 | ```ts 84 | const filter = source.filter("colorCorrection"); 85 | ``` 86 | 87 | ## Add a Filter to a Source 88 | 89 | A filter can be added to a source dynamically by calling [addFilter](/api/core/class/Source#addFilter) on the source. 90 | 91 | ```ts 92 | const filter = await source.addFilter( 93 | "colorCorrection", 94 | new ColorCorrectionFilter({ 95 | name: "Filter", 96 | }) 97 | ); 98 | ``` 99 | 100 | ## Properties 101 | 102 | - [name](/api/core/class/Filter#name): The name the filter was created with 103 | - [kind](/api/core/class/Filter#kind): The kind of the filter 104 | - [source](/api/core/class/Filter#source): The source the filter is assigned to 105 | 106 | These properties can be set with their associated functions: 107 | 108 | - [settings](/api/core/class/Source#settings) - [setSettings](/api/core/class/Filter#setSettings) 109 | - [enabled](/api/core/class/Filter#enabled) - [setEnabled](/api/core/class/Filter#setEnabled) -------------------------------------------------------------------------------- /packages/core-old/tests/Source.test.ts: -------------------------------------------------------------------------------- 1 | import { Scene, mocks } from "../src"; 2 | import { obs, resetSceneify } from "./utils"; 3 | 4 | describe("createSceneItem", () => { 5 | it("fails if source is not initialized", async () => { 6 | const input = new mocks.MockInput({ 7 | name: "Input", 8 | }); 9 | 10 | const scene = new Scene({ 11 | name: "Test", 12 | items: {}, 13 | }); 14 | 15 | await expect(input.createSceneItem("test", scene)).rejects.toThrow( 16 | "not initialized" 17 | ); 18 | }); 19 | 20 | it("finds existing item with refs", async () => { 21 | // make items in obs 22 | const existingInput = new mocks.MockInput({ 23 | name: "Input", 24 | }); 25 | 26 | const existingScene = new Scene({ 27 | name: "Test", 28 | items: { 29 | test: { 30 | source: existingInput, 31 | }, 32 | }, 33 | }); 34 | 35 | await existingScene.create(obs); 36 | 37 | resetSceneify(); 38 | 39 | const input = new mocks.MockInput({ 40 | name: existingInput.name, 41 | }); 42 | 43 | const scene = new Scene({ 44 | name: existingScene.name, 45 | items: {}, 46 | }); 47 | 48 | await input.initialize(obs); 49 | 50 | let item = await input.createSceneItem("test", scene); 51 | 52 | expect(item.id).toBe(existingScene.item("test").id); 53 | expect(existingInput.refs).toEqual(input.refs); 54 | }); 55 | 56 | it("creates a new item if ref is broken", async () => { 57 | // make items in obs 58 | const existingInput = new mocks.MockInput({ 59 | name: "Input", 60 | }); 61 | 62 | const existingScene = new Scene({ 63 | name: "Test", 64 | items: { 65 | test: { 66 | source: existingInput, 67 | }, 68 | test2: { 69 | source: existingInput, 70 | }, 71 | }, 72 | }); 73 | 74 | await existingScene.create(obs); 75 | 76 | expect(existingInput.refs).toEqual({ 77 | [existingScene.name]: { 78 | [existingScene.item("test").ref]: existingScene.item("test").id, 79 | [existingScene.item("test2").ref]: existingScene.item("test2").id, 80 | }, 81 | }); 82 | 83 | await obs.call("RemoveSceneItem", { 84 | sceneName: existingScene.name, 85 | sceneItemId: existingScene.item("test").id, 86 | }); 87 | 88 | resetSceneify(); 89 | 90 | const input = new mocks.MockInput({ 91 | name: existingInput.name, 92 | }); 93 | 94 | const scene = new Scene({ 95 | name: existingScene.name, 96 | items: {}, 97 | }); 98 | 99 | await input.initialize(obs); 100 | 101 | let item = await input.createSceneItem("test", scene); 102 | 103 | expect(item.id).not.toBe(existingScene.item("test").id); 104 | 105 | expect(input.refs).toEqual({ 106 | [scene.name]: { 107 | [item.ref]: item.id, 108 | [existingScene.item("test2").ref]: existingScene.item("test2").id, 109 | }, 110 | }); 111 | }); 112 | 113 | it("adds filters to the source", async () => { 114 | const scene = await new Scene({ 115 | name: "Test", 116 | items: {}, 117 | }).create(obs); 118 | 119 | const input = new mocks.MockInput({ 120 | name: "Input", 121 | filters: { 122 | test: new mocks.MockFilter({ 123 | name: "Filter", 124 | }), 125 | }, 126 | }); 127 | 128 | await input.initialize(obs); 129 | await input.createSceneItem("test", scene); 130 | 131 | expect(input.filters.length).toBe(1); 132 | 133 | const { filters } = await obs.call("GetSourceFilterList", { 134 | sourceName: input.name, 135 | }); 136 | expect(filters.length).toBe(1); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/core/src/filters.ts: -------------------------------------------------------------------------------- 1 | import { defineFilterType } from "./definition.js"; 2 | 3 | export const applyLUTFilter = defineFilterType("clut_filter").settings<{ 4 | image_path: string; 5 | }>(); 6 | 7 | export const aspectRatioFilter = defineFilterType("scale_filter").settings<{ 8 | resolution: string; 9 | sampling: string; 10 | }>(); 11 | 12 | export const chromaKeyFilter = defineFilterType( 13 | "chroma_key_filter_v2" 14 | ).settings<{ 15 | brightness: number; 16 | contrast: number; 17 | gamma: number; 18 | key_color: number; 19 | key_color_type: "green" | "blue" | "magenta" | "custom"; 20 | opacity: number; 21 | similarity: number; 22 | smoothness: number; 23 | spill: number; 24 | }>(); 25 | 26 | export const colorCorrectionFilter = defineFilterType( 27 | "color_filter_v2" 28 | ).settings<{ 29 | brightness: number; 30 | contrast: number; 31 | gamma: number; 32 | key_color: number; 33 | key_color_type: "green" | "blue" | "magenta" | "custom"; 34 | opacity: number; 35 | similarity: number; 36 | smoothness: number; 37 | spill: number; 38 | }>(); 39 | 40 | export const colorKeyFilter = defineFilterType("color_key_filter_v2").settings<{ 41 | brightness: number; 42 | contrast: number; 43 | gamma: number; 44 | key_color: number; 45 | key_color_type: "green" | "blue" | "red" | "magenta" | "custom"; 46 | opacity: number; 47 | similarity: number; 48 | smoothness: number; 49 | }>(); 50 | 51 | export const compressorFilter = defineFilterType("compressor_filter").settings<{ 52 | ratio: number; 53 | threshold: number; 54 | attack_time: number; 55 | release_time: number; 56 | output_gain: number; 57 | sidechain_source: string; 58 | }>(); 59 | 60 | export const cropPadFilter = defineFilterType("crop_filter").settings<{ 61 | bottom: number; 62 | left: number; 63 | relative: boolean; 64 | right: number; 65 | rop: number; 66 | }>(); 67 | 68 | export const expanderFilter = defineFilterType("expander_filter").settings<{ 69 | ratio: number; 70 | threshold: number; 71 | attack_time: number; 72 | release_time: number; 73 | output_gain: number; 74 | detector: "RMS" | "peak"; 75 | presets: "expander" | "gate"; 76 | }>(); 77 | 78 | export const gainFilter = defineFilterType("gain_filter").settings<{ 79 | db: number; 80 | }>(); 81 | 82 | export const imageMaskBlendFilter = defineFilterType( 83 | "mask_filter_v2" 84 | ).settings<{ 85 | image_path: string; 86 | type: 87 | | "mask_alpha_filter.effect" 88 | | "mask_colour_filter.effect" 89 | | "blend_mul_filter.effect" 90 | | "blend_add_filter.effect" 91 | | "blend_sub_filter.effect"; 92 | }>(); 93 | 94 | export const invertPolarityFilter = defineFilterType("invert_polarity_filter"); 95 | 96 | export const limiterFilter = defineFilterType("limiter_filter").settings<{ 97 | threshold: number; 98 | release_time: number; 99 | }>(); 100 | 101 | export const lumaKeyFilter = defineFilterType("luma_key_filter").settings<{ 102 | luma_max: number; 103 | luma_max_smooth: number; 104 | luma_min: number; 105 | luma_min_smooth: number; 106 | }>(); 107 | 108 | export const noiseGateFilter = defineFilterType("noise_gate_filter").settings<{ 109 | open_threshold: number; 110 | close_threshold: number; 111 | attack_time: number; 112 | hold_time: number; 113 | release_time: number; 114 | }>(); 115 | 116 | export const noiseSuppressFilter = defineFilterType( 117 | "noise_suppress_filter_v2" 118 | ).settings<{ method: "speex" | "rnnoise" | "nvafx" }>(); 119 | 120 | export const renderDelayFilter = defineFilterType("gpu_delay").settings<{ 121 | delay_ms: number; 122 | }>(); 123 | 124 | export const scrollFilter = defineFilterType("scroll_filter").settings<{ 125 | limit_cx: boolean; 126 | limit_cy: boolean; 127 | loop: boolean; 128 | speed_x: number; 129 | speed_y: number; 130 | }>(); 131 | 132 | export const sharpenFilter = defineFilterType("sharpness_filter_v2").settings<{ 133 | sharpness: number; 134 | }>(); 135 | -------------------------------------------------------------------------------- /packages/core/src/inputs.ts: -------------------------------------------------------------------------------- 1 | import { defineInputType } from "./definition.js"; 2 | import { OBSFont, OBSVideoRange } from "./obs-types.js"; 3 | 4 | export const browserSource = defineInputType("browser_source").settings<{ 5 | url: string; 6 | width: number; 7 | height: number; 8 | reroute_audio: boolean; 9 | css: string; 10 | }>(); 11 | 12 | export const colorSource = defineInputType("color_source_v3").settings<{ 13 | color: number; 14 | width: number; 15 | height: number; 16 | }>(); 17 | 18 | export const imageSource = defineInputType("image_source").settings<{ 19 | file: string; 20 | unload: boolean; 21 | linear_alpha: boolean; 22 | }>(); 23 | 24 | export const freetypeTextSource = defineInputType( 25 | "text_ft2_source_v2" 26 | ).settings<{ 27 | font: OBSFont; 28 | text: string; 29 | from_file: boolean; 30 | antialiasing: boolean; 31 | log_mode: boolean; 32 | log_lines: number; 33 | text_file: string; 34 | color1: number; 35 | color2: number; 36 | outline: boolean; 37 | drop_shadow: boolean; 38 | custom_width: number; 39 | word_wrap: boolean; 40 | }>(); 41 | 42 | export const gdiPlusTextSource = defineInputType("text_gdiplus_v2").settings<{ 43 | font: OBSFont; 44 | use_file: boolean; 45 | text: string; 46 | file: string; 47 | antialiasing: boolean; 48 | transform: 0 | 1 | 2 | 3; 49 | vertical: boolean; 50 | color: number; 51 | /** 0-100 */ 52 | opacity: number; 53 | gradient: boolean; 54 | gradient_color: number; 55 | gradient_opacity: number; 56 | /** 0-360 */ 57 | gradient_dir: number; 58 | bk_color: number; 59 | /** 0-100 */ 60 | bk_opacity: number; 61 | align: "left" | "center" | "right"; 62 | valign: "top" | "center" | "bottom"; 63 | outline: boolean; 64 | outline_size: number; 65 | outline_color: number; 66 | /** 0-100 */ 67 | outline_opacity: number; 68 | chatlog_mode: boolean; 69 | chatlog_lines: number; 70 | extents: boolean; 71 | extents_cx: number; 72 | extents_cy: number; 73 | extends_wrap: boolean; 74 | }>(); 75 | 76 | export const mediaSource = defineInputType("ffmpeg_source").settings<{ 77 | is_local_file: boolean; 78 | local_file: string; 79 | looping: boolean; 80 | restart_on_activate: boolean; 81 | buffering_mb: number; 82 | input: string; 83 | input_format: string; 84 | reconnect_delay_sec: number; 85 | hw_decode: boolean; 86 | clear_on_media_end: boolean; 87 | close_when_inactive: boolean; 88 | speed_percent: number; 89 | color_range: OBSVideoRange; 90 | linear_alpha: boolean; 91 | seekable: boolean; 92 | }>(); 93 | 94 | export const displayCaptureSource = defineInputType( 95 | "monitor_capture" 96 | ).settings<{ 97 | monitor: number; 98 | compatibility: boolean; 99 | capture_cursor: boolean; 100 | }>(); 101 | 102 | export const videoCaptureSource = defineInputType( 103 | "av_capture_input_v2" 104 | ).settings<{ device: string; device_name: string }>(); 105 | 106 | export const decklinkInput = defineInputType("decklink-input").settings<{ 107 | device_name: string; 108 | device_hash: string; 109 | video_connection: number; 110 | audio_connection: number; 111 | mode_id: number; 112 | pixel_format: 0x32767579 | 0x76323130 | 0x42475241; 113 | color_space: 0 | 1 | 2; 114 | color_range: OBSVideoRange; 115 | channel_format: 0 | 2 | 3 | 4 | 5 | 6 | 8; 116 | swap: boolean; 117 | buffering: boolean; 118 | deactivate_when_not_showing: boolean; 119 | allow_10_bit: boolean; 120 | }>(); 121 | 122 | export const coreAudioInputCapture = defineInputType( 123 | "coreaudio_input_capture" 124 | ).settings<{ 125 | device_id: string; 126 | enable_downmix: boolean; 127 | }>(); 128 | 129 | export const macOSScreenCapture = defineInputType("screen_capture").settings<{ 130 | application: string; 131 | display_uuid: string; 132 | hide_obs: boolean; 133 | show_cursor: boolean; 134 | show_empty_names: boolean; 135 | show_hidden_windows: boolean; 136 | type: number; 137 | window: number; 138 | }>(); 139 | -------------------------------------------------------------------------------- /packages/core-old/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "../Source"; 2 | import { 3 | OBSRequestTypes as BaseRequestTypes, 4 | OBSResponseTypes as BaseResponseTypes, 5 | OBSEventTypes as BaseEventTypes, 6 | } from "obs-websocket-js"; 7 | import { BoundsType, MonitoringType } from "../constants"; 8 | 9 | /** 10 | * Makes every field and nested field optional in the provided object. 11 | */ 12 | export type DeepPartial = T extends object 13 | ? { [P in keyof T]?: DeepPartial } 14 | : T; 15 | 16 | export type FilterType = Pick< 17 | Base, 18 | { 19 | [Key in keyof Base]: Base[Key] extends Condition ? Key : never; 20 | }[keyof Base] 21 | >; 22 | 23 | export type SourceItemType = ReturnType< 24 | S["createSceneItemObject"] 25 | >; 26 | 27 | export type Settings = Record; 28 | 29 | export interface Filter { 30 | filterName: string; 31 | filterEnabled: boolean; 32 | filterIndex: number; 33 | filterKind: string; 34 | filterSettings: Settings; 35 | } 36 | 37 | export interface Font { 38 | face: string; 39 | flags: number; 40 | size: number; 41 | style: string; 42 | } 43 | 44 | export interface SceneItemTransform { 45 | sourceWidth: number; 46 | sourceHeight: number; 47 | 48 | positionX: number; 49 | positionY: number; 50 | 51 | rotation: number; 52 | 53 | scaleX: number; 54 | scaleY: number; 55 | 56 | width: number; 57 | height: number; 58 | 59 | alignment: number; 60 | 61 | boundsType: BoundsType; 62 | boundsAlignment: number; 63 | boundsWidth: number; 64 | boundsHeight: number; 65 | 66 | cropLeft: number; 67 | cropTop: number; 68 | cropRight: number; 69 | cropBottom: number; 70 | } 71 | 72 | export interface PropertyItem { 73 | itemEnabled: boolean; 74 | itemName: string; 75 | itemValue: string | number; 76 | } 77 | 78 | export interface OBSRequestTypesOverrides { 79 | SetSceneItemTransform: { 80 | sceneName: string; 81 | sceneItemId: number; 82 | sceneItemTransform: Partial; 83 | }; 84 | 85 | SetSourcePrivateSettings: { 86 | sourceName: string; 87 | sourceSettings: Settings; 88 | }; 89 | 90 | GetSourcePrivateSettings: { 91 | sourceName: string; 92 | }; 93 | 94 | SetInputAudioMonitorType: { 95 | inputName: string; 96 | monitorType: MonitoringType; 97 | }; 98 | } 99 | 100 | export interface OBSResponseTypesOverrides { 101 | GetSceneItemList: { 102 | sceneItems: { 103 | sceneItemId: number; 104 | sceneItemIndex: number; 105 | sourceName: string; 106 | sourceType: string; 107 | inputKind?: string; 108 | isGroup?: boolean; 109 | }[]; 110 | }; 111 | 112 | GetSceneItemTransform: { sceneItemTransform: SceneItemTransform }; 113 | 114 | GetSceneList: { 115 | scenes: { 116 | sceneName: string; 117 | sceneIndex: number; 118 | }[]; 119 | currentProgramSceneName: string; 120 | currentPreviewSceneName: string; 121 | }; 122 | 123 | GetInputList: { 124 | inputs: { 125 | inputName: string; 126 | inputKind: string; 127 | unversionedInputKind: string; 128 | }[]; 129 | }; 130 | 131 | GetInputSettings: { 132 | inputSettings: Settings; 133 | inputName: string; 134 | inputKind: string; 135 | }; 136 | 137 | GetInputAudioMonitorType: { 138 | monitorType: MonitoringType; 139 | }; 140 | 141 | SetSourcePrivateSettings: undefined; 142 | 143 | GetSourcePrivateSettings: { 144 | sourceSettings: Settings; 145 | }; 146 | 147 | GetInputPropertiesListPropertyItems: { 148 | propertyItems: PropertyItem[]; 149 | }; 150 | } 151 | 152 | export interface OBSEventTypesOverrides {} 153 | 154 | export type OBSRequestTypes = 155 | | Omit & 156 | OBSRequestTypesOverrides; 157 | 158 | export type OBSResponseTypes = 159 | | Omit & 160 | OBSResponseTypesOverrides; 161 | 162 | export type OBSEventTypes = 163 | | Omit & OBSEventTypesOverrides; 164 | -------------------------------------------------------------------------------- /website/docs/getting-started/basic-walkthrough.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | slug: /basic-walkthrough 4 | --- 5 | 6 | # Basic Walkthrough 7 | 8 | This page contains a walkthrough of how to create two scenes, one nested inside another, to demonstrate some of Sceneify's functionality. 9 | 10 | ## Step 0: Create a new Scene Collection 11 | 12 | While you could use your existing scene collection in OBS, it is **highly** recommended to create a new one. Sceneify won't alter what you don't tell it to interact with, but for safety's sake it is best to create a new scene collection. 13 | 14 | ## Step 1: Setup 15 | 16 | Firstly, import the necessary classes from Sceneify. 17 | 18 | import Tabs from '@theme/Tabs'; 19 | import TabItem from '@theme/TabItem'; 20 | 21 | 22 | 23 | 24 | ```js 25 | const { OBS, Scene, Alignment } = require("@sceneify/core"); 26 | const { ColorSource } = require("@sceneify/sources"); 27 | ``` 28 | 29 | 30 | 31 | 32 | ```ts 33 | import { OBS, Scene, Alignment } from "@sceneify/core"; 34 | import { ColorSource } from "@sceneify/sources"; 35 | ``` 36 | 37 | 38 | 39 | 40 | Next, create an `async` function that the code can run inside, and execute it. 41 | While not strictly necessary, this allows us to use the `await` keyword, which makes for a nicer development experience. 42 | If you are unfamiliar with JavaScript's `async/await` syntax, it's recommended that you do some research and understand it. 43 | Using it wrong can lead to problems with things being out of sync and not happening in the order you want them to. 44 | 45 | ```ts 46 | async function main() { 47 | // Code will go in here 48 | } 49 | 50 | main(); 51 | ``` 52 | 53 | Lastly, create an instance of [OBS](/api/core/class/OBS) in code and connect it to OBS. 54 | You may need to change the port number from `4444` if your OBS Websocket server runs on a different port. 55 | 56 | ```ts 57 | // Creates an OBS object that will connect to OBS 58 | const obs = new OBS(); 59 | 60 | // Connects the OBS object to OBS 61 | await obs.connect("localhost:4444"); 62 | ``` 63 | 64 | ## Step 2: Declare Nested Scene 65 | 66 | Next, we create an instance of [Scene](/api/core/class/Scene) that represents the nested scene in OBS. 67 | It contains one item with default properties, and its source is a [ColourSource](/api/sources/class/ColorSource) created with default settings. 68 | 69 | ```ts 70 | const nestedScene = new Scene({ 71 | name: "Nested", 72 | items: { 73 | color: { 74 | source: new ColorSource({ 75 | name: "Nested Color Source", 76 | settings: {}, 77 | }), 78 | }, 79 | }, 80 | }); 81 | ``` 82 | 83 | ## Step 3: Declare Parent Scene 84 | 85 | Like before, create a `new Scene` that represents the main scene. Give it one item, and make the item's source the nested scene you created earlier. 86 | Assign this item some properties, such as 2x scale and center alignment. 87 | 88 | ```ts 89 | const mainScene = new Scene({ 90 | name: "Main", 91 | items: { 92 | nested: { 93 | source: nestedScene, 94 | scaleX: 2, 95 | scaleY: 2, 96 | alignment: Alignment.Center, 97 | }, 98 | }, 99 | }); 100 | ``` 101 | 102 | ## Step 4: Create the Scenes in OBS 103 | 104 | Finally, tell the main scene to create itself and all of its children (in this case, the nested scene) in the instance of [OBS](/api/core/class/OBS) you created in Step 1: 105 | 106 | ```ts 107 | await mainScene.create(obs); 108 | ``` 109 | 110 | ## Step 5: Make the Main Scene the Current Scene 111 | 112 | Just for fun, make the main scene the current scene in OBS so that you can view it right after it's created. 113 | 114 | ```ts 115 | await mainScene.makeCurrentScene(); 116 | ``` 117 | 118 | ## Step 6: Run the Script 119 | 120 | Making sure you have OBS running, run the script you just wrote. You should see two scenes be created, with "Main Scene" containing "Nested Scene" as a scene item at 2x scale. 121 | 122 | ## Going Forward 123 | 124 | There are so many features that weren't touched on in this walkthrough, and you are likely confused about some things. 125 | 126 | - The [Guides](/docs/category/guides) explain most parts of Sceneify and how to use them 127 | - The [API Reference](/api) contains detailed information on the types and uses of every aspect of Sceneify. 128 | -------------------------------------------------------------------------------- /packages/core-old/src/SceneItem.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_SCENE_ITEM_TRANSFORM } from "./constants"; 2 | import { Scene } from "./Scene"; 3 | import { Source } from "./Source"; 4 | import { SceneItemTransform as RawSceneItemTransform } from "./types"; 5 | import { Alignment, BoundsType } from "."; 6 | import { removeUndefinedValues } from "./utils"; 7 | 8 | export interface SceneItemTransform { 9 | sourceWidth: number; 10 | sourceHeight: number; 11 | 12 | positionX: number; 13 | positionY: number; 14 | 15 | rotation: number; 16 | 17 | scaleX: number; 18 | scaleY: number; 19 | 20 | width: number; 21 | height: number; 22 | 23 | alignment: Alignment; 24 | 25 | boundsType: BoundsType; 26 | boundsAlignment: Alignment; 27 | boundsWidth: number; 28 | boundsHeight: number; 29 | 30 | cropLeft: number; 31 | cropTop: number; 32 | cropRight: number; 33 | cropBottom: number; 34 | } 35 | 36 | /** 37 | * Represents an item of a source in OBS. 38 | * Creation of a SceneItem assumes that the item already exists in OBS. 39 | * It's the responsibility of the caller (probably a Source) to ensure that 40 | * the item has been created. SceneItems are for accessing already existing items. 41 | */ 42 | export class SceneItem< 43 | TSource extends Source = Source, 44 | TScene extends Scene = Scene 45 | > { 46 | constructor( 47 | public source: TSource, 48 | public scene: TScene, 49 | public id: number, 50 | public ref: string 51 | ) {} 52 | 53 | transform: SceneItemTransform = { ...DEFAULT_SCENE_ITEM_TRANSFORM }; 54 | enabled = true; 55 | locked = false; 56 | 57 | /** 58 | * 59 | * PROPERTIES 60 | * 61 | */ 62 | 63 | /** 64 | * Fetches the item's transform, enabled, and locked properties and assigns them to the item. 65 | */ 66 | async fetchProperties() { 67 | const args = { 68 | sceneName: this.scene.name, 69 | sceneItemId: this.id, 70 | }; 71 | const [{ sceneItemTransform }, { sceneItemEnabled }, { sceneItemLocked }] = 72 | await Promise.all([ 73 | this.source.obs.call("GetSceneItemTransform", args), 74 | this.source.obs.call("GetSceneItemEnabled", args), 75 | this.source.obs.call("GetSceneItemLocked", args), 76 | ]); 77 | 78 | this.transform = sceneItemTransform as SceneItemTransform; 79 | this.enabled = sceneItemEnabled; 80 | this.locked = sceneItemLocked; 81 | } 82 | 83 | async setTransform(transform: Partial) { 84 | await this.source.obs.call("SetSceneItemTransform", { 85 | sceneName: this.scene.name, 86 | sceneItemId: this.id, 87 | sceneItemTransform: transform as RawSceneItemTransform, 88 | }); 89 | 90 | this.transform = { 91 | ...this.transform, 92 | ...removeUndefinedValues(transform), 93 | }; 94 | 95 | this.updateSizeFromSource(); 96 | } 97 | 98 | async setEnabled(enabled: boolean) { 99 | await this.source.obs.call("SetSceneItemEnabled", { 100 | sceneName: this.scene.name, 101 | sceneItemId: this.id, 102 | sceneItemEnabled: enabled, 103 | }); 104 | 105 | this.enabled = enabled; 106 | } 107 | 108 | async setLocked(locked: boolean) { 109 | await this.source.obs.call("SetSceneItemLocked", { 110 | sceneName: this.scene.name, 111 | sceneItemId: this.id, 112 | sceneItemLocked: locked, 113 | }); 114 | 115 | this.locked = locked; 116 | } 117 | 118 | async remove() { 119 | await this.source.obs.call("RemoveSceneItem", { 120 | sceneName: this.scene.name, 121 | sceneItemId: this.id, 122 | }); 123 | 124 | this.source.removeItemInstance(this); 125 | this.scene.items.splice(this.scene.items.indexOf(this), 1); 126 | 127 | if (this.source.exists) 128 | await this.source.removeRef(this.scene.name, this.ref); 129 | } 130 | 131 | /** 132 | * Some sources have custom settings for width and height. Thus, sourceWidth and 133 | * sourceHeight for their scene items can change. This method reassigns these values and 134 | * calculates properties.width and properties.height as a product of the source dimensions 135 | * and item scale. 136 | */ 137 | updateSizeFromSource(sourceWidth?: number, sourceHeight?: number) { 138 | this.transform.sourceWidth = sourceWidth ?? this.transform.sourceWidth; 139 | this.transform.sourceHeight = sourceHeight ?? this.transform.sourceHeight; 140 | 141 | this.transform.width = this.transform.scaleX * this.transform.sourceWidth; 142 | this.transform.height = this.transform.scaleY * this.transform.sourceHeight; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/core-old/src/Filter.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "./Source"; 2 | import { DeepPartial, Settings } from "./types"; 3 | 4 | export interface FilterArgs { 5 | name: string; 6 | kind: string; 7 | settings?: DeepPartial; 8 | enabled?: boolean; 9 | } 10 | 11 | export type CustomFilterArgs = Omit< 12 | FilterArgs, 13 | "kind" 14 | >; 15 | 16 | const filterDefaultSettings = new Map(); 17 | 18 | export class Filter< 19 | TSettings extends Settings = Settings, 20 | TSource extends Source = Source 21 | > { 22 | constructor(args: FilterArgs) { 23 | this.initialSettings = args.settings || ({} as any); 24 | this.name = args.name; 25 | this.kind = args.kind; 26 | this.enabled = args.enabled ?? true; 27 | } 28 | 29 | name: string; 30 | kind: string; 31 | enabled: boolean; 32 | settings: DeepPartial = {} as any; 33 | 34 | source!: TSource; 35 | ref!: string; 36 | 37 | private initialSettings: DeepPartial = {} as any; 38 | 39 | async setSettings(settings: DeepPartial, overlay = true) { 40 | this.checkSource(); 41 | 42 | await this.source!.obs.call("SetSourceFilterSettings", { 43 | sourceName: this.source!.name, 44 | filterName: this.name, 45 | filterSettings: settings as any, 46 | overlay, 47 | }); 48 | 49 | for (let setting in settings) { 50 | this.settings[setting] = settings[setting]; 51 | } 52 | } 53 | 54 | async setEnabled(enabled: boolean) { 55 | this.checkSource(); 56 | 57 | await this.source!.obs.call("SetSourceFilterEnabled", { 58 | sourceName: this.source!.name, 59 | filterEnabled: enabled, 60 | filterName: this.name, 61 | }); 62 | 63 | this.enabled = enabled; 64 | } 65 | 66 | async remove() { 67 | this.checkSource(); 68 | 69 | await this.source!.obs.call("RemoveSourceFilter", { 70 | sourceName: this.source!.name, 71 | filterName: this.name, 72 | }); 73 | 74 | this.source!.filters.splice(this.source!.filters.indexOf(this), 1); 75 | this.source = undefined as any; 76 | } 77 | 78 | /** @internal */ 79 | async create(ref: string, source: TSource) { 80 | if (this.source) 81 | if (this.source === source && this.ref === ref) 82 | // Return early since create() has already been called 83 | return; 84 | else 85 | throw new Error( 86 | `Failed to add filter ${this.name} to source ${this.name}: Filter has already been added to source ${this.source.name} under ref ${this.ref}` 87 | ); 88 | 89 | this.ref = ref; 90 | this.source = source; 91 | 92 | const { exists } = await source.obs 93 | .call("GetSourceFilter", { 94 | sourceName: this.source.name, 95 | filterName: this.name, 96 | }) 97 | .then((f) => { 98 | if (this.kind !== f.filterKind) 99 | throw { 100 | error: `Failed to add filter ${this.name} to source ${this.source.name}: Filter exists in OBS but has different kind, expected ${this.kind} but found ${f.filterKind}`, 101 | }; 102 | 103 | return { exists: true }; 104 | }) 105 | .catch((data: { error: string; exists: boolean }) => { 106 | if (data.error) throw new Error(data.error); 107 | 108 | return { exists: data.exists ?? false }; 109 | }); 110 | 111 | if (!exists) 112 | await source.obs.call("CreateSourceFilter", { 113 | filterName: this.name, 114 | filterKind: this.kind, 115 | filterSettings: this.initialSettings, 116 | sourceName: this.source.name, 117 | }); 118 | else await this.setSettings(this.initialSettings); 119 | 120 | const defaultSettings = await this.getDefaultSettings(); 121 | 122 | this.settings = { 123 | ...defaultSettings, 124 | ...this.initialSettings, 125 | }; 126 | 127 | await this.setEnabled(this.enabled); 128 | } 129 | 130 | async getDefaultSettings() { 131 | const cached = filterDefaultSettings.get(this.kind); 132 | 133 | if (cached) return cached; 134 | 135 | const { defaultFilterSettings } = await this.source.obs.call( 136 | "GetSourceFilterDefaultSettings", 137 | { 138 | filterKind: this.kind, 139 | } 140 | ); 141 | 142 | filterDefaultSettings.set(this.kind, defaultFilterSettings); 143 | 144 | return { ...defaultFilterSettings }; 145 | } 146 | 147 | /** @internal */ 148 | checkSource() { 149 | if (!this.source) 150 | throw new Error(`Filter ${this.name} does not have a source.`); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /packages/core-old/tests/SceneItem.test.ts: -------------------------------------------------------------------------------- 1 | import { Scene, mocks } from "../src"; 2 | import { obs } from "./utils"; 3 | 4 | describe("setTransform", () => { 5 | it("updates obs transform", async () => { 6 | const scene = new Scene({ 7 | name: "Test", 8 | items: { 9 | item: { 10 | source: new mocks.MockInput({ 11 | name: "Source", 12 | }), 13 | }, 14 | }, 15 | }); 16 | 17 | await scene.create(obs); 18 | 19 | const item = scene.item("item"); 20 | 21 | item.source.setSettings({}); 22 | 23 | expect(item.transform.rotation).toBe(0); 24 | expect(item.transform.positionX).toBe(0); 25 | 26 | await item.setTransform({ 27 | rotation: 10, 28 | positionX: 10, 29 | }); 30 | 31 | const { sceneItemTransform } = await obs.call("GetSceneItemTransform", { 32 | sceneName: scene.name, 33 | sceneItemId: item.id, 34 | }); 35 | 36 | expect(item.transform.rotation).toBe(10); 37 | expect(item.transform.positionX).toBe(10); 38 | 39 | expect(sceneItemTransform.rotation).toBe(10); 40 | expect(sceneItemTransform.positionX).toBe(10); 41 | }); 42 | 43 | it("ignores undefined values", async () => { 44 | const scene = new Scene({ 45 | name: "Test", 46 | items: { 47 | item: { 48 | source: new mocks.MockInput({ 49 | name: "Source", 50 | }), 51 | }, 52 | }, 53 | }); 54 | 55 | await scene.create(obs); 56 | 57 | const item = scene.item("item"); 58 | 59 | expect(item.transform.rotation).toBe(0); 60 | expect(item.transform.positionX).toBe(0); 61 | expect(item.transform.positionY).toBe(0); 62 | 63 | await item.setTransform({ 64 | rotation: undefined, 65 | positionX: 10, 66 | positionY: undefined, 67 | }); 68 | 69 | const { sceneItemTransform } = await obs.call("GetSceneItemTransform", { 70 | sceneName: scene.name, 71 | sceneItemId: item.id, 72 | }); 73 | 74 | expect(item.transform.rotation).toBe(0); 75 | expect(item.transform.positionX).toBe(10); 76 | expect(item.transform.positionY).toBe(0); 77 | 78 | expect(sceneItemTransform.rotation).toBe(0); 79 | expect(sceneItemTransform.positionX).toBe(10); 80 | expect(sceneItemTransform.positionY).toBe(0); 81 | }); 82 | }); 83 | 84 | describe("setEnabled", () => { 85 | it("sets item's enabled state", async () => { 86 | const scene = new Scene({ 87 | name: "Test", 88 | items: { 89 | item: { 90 | source: new mocks.MockInput({ 91 | name: "Source", 92 | }), 93 | }, 94 | }, 95 | }); 96 | 97 | await scene.create(obs); 98 | 99 | const item = scene.item("item"); 100 | 101 | expect(item.enabled).toBe(true); 102 | 103 | await item.setEnabled(false); 104 | 105 | expect(item.enabled).toBe(false); 106 | }); 107 | }); 108 | 109 | describe("setLocked", () => { 110 | it("sets item's locked state", async () => { 111 | const scene = new Scene({ 112 | name: "Test", 113 | items: { 114 | item: { 115 | source: new mocks.MockInput({ 116 | name: "Source", 117 | }), 118 | }, 119 | }, 120 | }); 121 | 122 | await scene.create(obs); 123 | 124 | const item = scene.item("item"); 125 | 126 | expect(item.locked).toBe(false); 127 | 128 | await item.setLocked(true); 129 | 130 | expect(item.locked).toBe(true); 131 | }); 132 | }); 133 | 134 | describe("remove", () => { 135 | it("removes item from scene", async () => { 136 | const input = new mocks.MockInput({ 137 | name: "Source", 138 | }); 139 | 140 | const scene = new Scene({ 141 | name: "Test", 142 | items: { 143 | item: { 144 | source: input, 145 | }, 146 | }, 147 | }); 148 | 149 | await scene.create(obs); 150 | await scene.item("item").remove(); 151 | 152 | expect(scene.item("item")).toBeUndefined(); 153 | }); 154 | 155 | it("removes item ref", async () => { 156 | let input = new mocks.MockInput({ 157 | name: "Source", 158 | }); 159 | 160 | const scene = new Scene({ 161 | name: "Test", 162 | items: { 163 | item1: { 164 | source: input, 165 | }, 166 | item2: { 167 | source: input, 168 | }, 169 | }, 170 | }); 171 | 172 | await scene.create(obs); 173 | 174 | expect(input.refs).toEqual({ 175 | [scene.name]: { 176 | [scene.item("item1").ref]: scene.item("item1").id, 177 | [scene.item("item2").ref]: scene.item("item2").id, 178 | }, 179 | }); 180 | 181 | await scene.item("item1").remove(); 182 | 183 | expect(input.refs).toEqual({ 184 | [scene.name]: { 185 | [scene.item("item2").ref]: scene.item("item2").id, 186 | }, 187 | }); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /examples/animation/index.ts: -------------------------------------------------------------------------------- 1 | import { OBS, Scene, SceneItem } from "@sceneify/core"; 2 | import { 3 | animate, 4 | filterAnimate as pluginAnimate, 5 | keyframes, 6 | } from "@sceneify/animation"; 7 | import { ColorSource } from "@sceneify/sources"; 8 | import { ColorCorrectionFilter } from "@sceneify/filters"; 9 | 10 | async function main() { 11 | const obs = new OBS(); 12 | 13 | await obs.connect("ws:localhost:4455"); 14 | 15 | const mainScene = new Scene({ 16 | name: "Main", 17 | items: {}, 18 | }); 19 | 20 | await mainScene.create(obs); 21 | 22 | const items: SceneItem[] = []; 23 | 24 | const ITEM_COUNT = 100; 25 | 26 | const SPEED = 1000; 27 | const WIDTH = 1900; 28 | const HEIGHT = 1060; 29 | 30 | const LAP_TIME = (1000 * (2 * (WIDTH + HEIGHT))) / SPEED; 31 | 32 | for (const i of [...Array(ITEM_COUNT).keys()]) 33 | items.push( 34 | await mainScene.createItem(`${i}`, { 35 | source: new ColorSource({ 36 | name: `Blue Color Source ${i}`, 37 | settings: { 38 | color: 0xffff0000, 39 | width: 20, 40 | height: 20, 41 | }, 42 | filters: { 43 | colorCorrection: new ColorCorrectionFilter({ 44 | name: `Color Correction`, 45 | settings: { 46 | hue_shift: 0, 47 | }, 48 | }), 49 | }, 50 | }), 51 | positionY: 0, 52 | positionX: 0, 53 | }) 54 | ); 55 | 56 | await mainScene.makeCurrentScene(); 57 | await obs.clean(); 58 | const kfs = items.reduce((acc, item, index) => { 59 | const OFFSET = index * 100; 60 | return [ 61 | ...acc, 62 | keyframes(item, { 63 | positionX: { 64 | [OFFSET]: 0, 65 | [OFFSET + (1000 * WIDTH) / SPEED]: WIDTH, 66 | [OFFSET + (1000 * (WIDTH + HEIGHT)) / SPEED]: WIDTH, 67 | [OFFSET + (1000 * (2 * WIDTH + HEIGHT)) / SPEED]: 0, 68 | }, 69 | positionY: { 70 | [OFFSET]: 0, 71 | [OFFSET + (1000 * WIDTH) / SPEED]: 0, 72 | [OFFSET + (1000 * (WIDTH + HEIGHT)) / SPEED]: 1080 - 20, 73 | [OFFSET + (1000 * (2 * WIDTH + HEIGHT)) / SPEED]: 1080 - 20, 74 | [OFFSET + (1000 * (2 * WIDTH + HEIGHT)) / SPEED]: 0, 75 | }, 76 | }), 77 | keyframes(item.source.filter("colorCorrection")!, { 78 | hue_shift: { 79 | [OFFSET]: 0, 80 | [OFFSET + LAP_TIME]: 360, 81 | }, 82 | }), 83 | ]; 84 | }, [] as any[]); 85 | 86 | pluginAnimate(kfs); 87 | 88 | // items.map((item, index) => { 89 | // setTimeout( 90 | // () => 91 | // setInterval( 92 | // () => 93 | // animate({ 94 | // subjects: { 95 | // item, 96 | // filter: item.source.filter("colorCorrection")!, 97 | // }, 98 | // keyframes: { 99 | // item: { 100 | // positionX: { 101 | // 0: 0, 102 | // 1000: 1920 - 10, 103 | // 2000: 1920 - 10, 104 | // 3000: 0, 105 | // }, 106 | // positionY: { 107 | // 0: 0, 108 | // 1000: 0, 109 | // 2000: 1080 - 10, 110 | // 3000: 1080 - 10, 111 | // 4000: 0, 112 | // }, 113 | // }, 114 | // filter: { 115 | // hue_shift: { 116 | // 0: 0, 117 | // 4000: 360, 118 | // }, 119 | // }, 120 | // }, 121 | // }), 122 | // 4050 123 | // ), 124 | // 4050 * (index / ITEM_COUNT) 125 | // ); 126 | // }); 127 | 128 | // await mainScene.create(obs); 129 | // await mainScene.makeCurrentScene(); 130 | 131 | // await animate({ 132 | // subjects: { 133 | // blueItem: mainScene.item("blue"), 134 | // // redColorFilter: mainScene.items.red.source.filters.color, 135 | // redSource: mainScene.item("red").source, 136 | // }, 137 | // keyframes: { 138 | // blueItem: { 139 | // positionX: { 140 | // // Keyframe values can be passed as simple values and use default easing 141 | // 0: 0, 142 | // // Or can be passed with custom easing values (+ more data in the future) 143 | // 1000: keyframe(1920, Easing.InOut), 144 | // 2000: keyframe(0, Easing.InOut), 145 | // }, 146 | // }, 147 | // // redColorFilter: { 148 | // // hue_shift: { 149 | // // 0: 0, 150 | // // 1000: 180, 151 | // // 2000: 0, 152 | // // }, 153 | // // }, 154 | // redSource: { 155 | // width: { 156 | // 0: keyframe(200, Easing.InOut), 157 | // 1000: keyframe(600, Easing.InOut), 158 | // 2000: keyframe(200, Easing.InOut), 159 | // }, 160 | // }, 161 | // }, 162 | // }); 163 | } 164 | 165 | main(); 166 | 167 | // utility for setTimeout that is nice to use with async/await syntax 168 | const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); 169 | -------------------------------------------------------------------------------- /examples/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { Alignment, OBS, Scene } from "@sceneify/core"; 2 | import { ColorCorrectionFilter } from "@sceneify/filters"; 3 | // import { ColorCorrectionFilter } from "@sceneify/filters"; 4 | import { ColorSource } from "@sceneify/sources"; 5 | 6 | // README 7 | // Running this code requies that you create a scene named "Linked Scene" with a single 8 | // color source inside it names "Linked Color Source", otherwise the linking example 9 | // will cause the script to throw an error 10 | 11 | async function main() { 12 | const obs = new OBS(); 13 | 14 | // Connect to OBS before creating or linking any scenes 15 | await obs.connect("ws:localhost:4455"); 16 | 17 | // verifies that the required linked scene exists 18 | await verifyLinkedSceneExists(obs); 19 | 20 | // Requests can be made directly through the websocket using OBS.call 21 | const { baseWidth: OBS_WIDTH, baseHeight: OBS_HEIGHT } = await obs.call( 22 | "GetVideoSettings" 23 | ); 24 | 25 | // Sources are just classes, and can be declared anywhere 26 | const color = new ColorSource({ 27 | name: "Color Source", 28 | settings: { 29 | color: 0xffff0000, 30 | width: 400, 31 | height: 400, 32 | }, 33 | filters: { 34 | color: new ColorCorrectionFilter({ 35 | name: "Color Corrector", 36 | settings: { 37 | brightness: 0, 38 | hue_shift: 0, 39 | }, 40 | }), 41 | }, 42 | }); 43 | 44 | // Scenes are just classes, only functioning as a schema at first 45 | const mainScene = new Scene({ 46 | name: "Main", 47 | items: { 48 | red: { 49 | source: color, 50 | alignment: Alignment.Center, 51 | positionX: OBS_WIDTH / 2, 52 | positionY: OBS_HEIGHT / 2, 53 | }, 54 | }, 55 | }); 56 | 57 | // Once Scene.create() is called, mainScene and its items can be used however you like. 58 | await mainScene.create(obs); 59 | await mainScene.makeCurrentScene(); 60 | 61 | await wait(1000); 62 | 63 | // Color should change from red to green 64 | await mainScene.item("red").source.setSettings({ 65 | color: 0xff00ff00, 66 | }); 67 | 68 | await wait(1000); 69 | 70 | // Color should change from green to pink 71 | // await mainScene.item("red").source.filter("color").setSettings({ 72 | // hue_shift: 180, 73 | // }); 74 | 75 | await wait(3000); 76 | 77 | const linkedScene = new Scene({ 78 | name: "Linked Scene", 79 | items: { 80 | color: { 81 | source: new ColorSource({ 82 | name: "Linked Color Source", 83 | }), 84 | alignment: Alignment.TopLeft, 85 | positionX: 0, 86 | positionY: 0, 87 | }, 88 | }, 89 | }); 90 | 91 | // Alternatively to Scene.create(), you can call Scene.link(), which will attempt to match the schema 92 | // of the scene you call it on to an existing scene and its items in OBS. 93 | // When linking, you can choose whether to force the linked items to have their properties and/or 94 | // settings set to the values you have defined in your schema. You'll probably want these to be true 95 | // if your schema contains custom property or setting values. 96 | await linkedScene.link(obs, { 97 | // setTransform: true, 98 | }); 99 | 100 | // After linking, you can interact with the items you declared in the schema in the same way 101 | // you would after calling Scene.create() 102 | await linkedScene.makeCurrentScene(); 103 | 104 | await wait(1000); 105 | 106 | // Color source should move from top left to bottom right after 1 second 107 | await linkedScene.item("color").setTransform({ 108 | alignment: Alignment.BottomRight, 109 | positionX: OBS_WIDTH, 110 | positionY: OBS_HEIGHT, 111 | }); 112 | 113 | await wait(3000); 114 | 115 | // Scenes are sources, and can be used as such! 116 | const finalSceen = new Scene({ 117 | name: "Final Scene", 118 | items: { 119 | main: { 120 | source: mainScene, 121 | positionX: OBS_WIDTH / 2, 122 | positionY: OBS_HEIGHT / 2, 123 | alignment: Alignment.Center, 124 | scaleX: 1, 125 | scaleY: 1, 126 | }, 127 | linked: { 128 | source: linkedScene, 129 | }, 130 | }, 131 | }); 132 | 133 | await finalSceen.create(obs); 134 | await finalSceen.makeCurrentScene(); 135 | 136 | await wait(1000); 137 | 138 | // Operation performed on the 'main' item of 'Final Scene', not the main scene itself 139 | await finalSceen.item("main").setTransform({ 140 | scaleX: 0.5, 141 | scaleY: 0.5, 142 | }); 143 | } 144 | 145 | main(); 146 | 147 | // utility for setTimeout that is nice to use with async/await syntax 148 | const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); 149 | 150 | async function verifyLinkedSceneExists(obs: OBS) { 151 | const { scenes } = await obs.call("GetSceneList"); 152 | 153 | if (!scenes.some((s) => s.sceneName === "Linked Scene")) { 154 | throw new Error("Linked Scene does not exist. Please create it."); 155 | } 156 | 157 | const { sceneItems } = await obs.call("GetSceneItemList", { 158 | sceneName: "Linked Scene", 159 | }); 160 | 161 | if (!sceneItems.some((i) => i.sourceName === "Linked Color Source")) { 162 | throw new Error( 163 | "Linked Color Source does not exist. Please create it inside Linked Scene." 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /packages/core/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { defineScene } from "./definition.js"; 2 | import { 3 | gainFilter, 4 | noiseGateFilter, 5 | noiseSuppressFilter, 6 | sharpenFilter, 7 | // streamfxBlurFilter, 8 | } from "./filters.js"; 9 | import { 10 | browserSource, 11 | coreAudioInputCapture, 12 | macOSScreenCapture, 13 | videoCaptureSource, 14 | } from "./inputs.js"; 15 | import { OBS } from "./obs.js"; 16 | import { FilterDefsOfInputDef, syncScene as syncScene } from "./runtime.js"; 17 | 18 | export const GAP = 20; 19 | 20 | export const webcam = videoCaptureSource.defineInput({ 21 | name: "Webcam", 22 | settings: { 23 | device: "0x122000046d085c", 24 | }, 25 | }); 26 | 27 | const display = macOSScreenCapture.defineInput({ 28 | name: "Display", 29 | settings: { 30 | display_uuid: "37D8832A-2D66-02CA-B9F7-8F30A301B230", 31 | }, 32 | }); 33 | 34 | const micInput = coreAudioInputCapture.defineInput({ 35 | name: "Mic Audio", 36 | settings: { 37 | device_id: 38 | "AppleUSBAudioEngine:Burr-Brown from TI :USB Audio CODEC :130000:2", 39 | enable_downmix: true, 40 | }, 41 | filters: { 42 | gain: gainFilter.defineFilter({ 43 | index: 0, 44 | enabled: true, 45 | name: "Gain", 46 | settings: { db: 2.5 }, 47 | }), 48 | noiseSuppression: noiseSuppressFilter.defineFilter({ 49 | index: 1, 50 | enabled: true, 51 | name: "Noise Suppression", 52 | settings: { method: "speex" }, 53 | }), 54 | }, 55 | }); 56 | 57 | const OUTPUT_WIDTH = 1920; 58 | const OUTPUT_HEIGHT = 1080; 59 | 60 | const DISPLAY_WIDTH = 3456; 61 | const DISPLAY_HEIGHT = 2234; 62 | 63 | const DISPLAY_SCALE = OUTPUT_HEIGHT / DISPLAY_HEIGHT; 64 | const DISPLAY_OFFSET = (OUTPUT_WIDTH - DISPLAY_WIDTH * DISPLAY_SCALE) / 2; 65 | 66 | export const mainScene = defineScene({ 67 | name: "Main", 68 | items: { 69 | display: { 70 | index: 1, 71 | input: display, 72 | scaleX: DISPLAY_SCALE, 73 | scaleY: DISPLAY_SCALE, 74 | positionX: DISPLAY_OFFSET, 75 | positionY: 0, 76 | alignment: "topLeft", 77 | }, 78 | webcam: { 79 | index: 2, 80 | input: webcam, 81 | positionX: OUTPUT_WIDTH - GAP, 82 | positionY: OUTPUT_HEIGHT - GAP, 83 | cropLeft: 300, 84 | cropRight: 300, 85 | alignment: "bottomRight", 86 | }, 87 | discordServer: { 88 | index: 3, 89 | input: browserSource.defineInput({ 90 | name: "Discord Server", 91 | settings: { 92 | width: 312, 93 | height: 64, 94 | url: "https://streamkit.discord.com/overlay/status/1177608475682029568?icon=true&online=true&logo=white&text_color=%23ffffff&text_size=14&text_outline_color=%23000000&text_outline_size=0&text_shadow_color=%23000000&text_shadow_size=0&bg_color=%231e2124&bg_opacity=0.95&bg_shadow_color=%23000000&bg_shadow_size=0&invite_code=XpctyaUgG8&limit_speaking=false&small_avatars=false&hide_names=false&fade_chat=0", 95 | css: "body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; }", 96 | }, 97 | }), 98 | alignment: "topRight", 99 | positionX: OUTPUT_WIDTH - GAP, 100 | positionY: GAP, 101 | }, 102 | mic: { 103 | index: 4, 104 | input: micInput, 105 | }, 106 | // guest: { 107 | // input: browserSource.defineInput({ 108 | // name: "Guest", 109 | // settings: { 110 | // width: 1920, 111 | // height: 1080, 112 | // url: "https://ping.gg/call/brendonovich/embed?view=cl8287d6q12920gmk8bbjyff5", 113 | // }, 114 | // }), 115 | // alignment: "bottomRight", 116 | // }, 117 | // chat: { 118 | // input: browserSource.defineInput({ 119 | // name: "Chat", 120 | // settings: { 121 | // url: "", //import.meta.env.VITE_CHAT_WIDGET_URL, 122 | // width: 1000, 123 | // }, 124 | // }), 125 | // positionX: 1920 - GAP, 126 | // alignment: "bottomRight", 127 | // }, 128 | // streamkitVoice: { 129 | // input: browserSource.defineInput({ 130 | // name: "Streamkit Voice", 131 | // settings: { 132 | // url: "https://streamkit.discord.com/overlay/voice/949090953497567312/966581199025893387?icon=true&online=true&logo=white&text_color=%23ffffff&text_size=14&text_outline_color=%23000000&text_outline_size=0&text_shadow_color=%23000000&text_shadow_size=0&bg_color=%231e2124&bg_opacity=0.95&bg_shadow_color=%23000000&bg_shadow_size=0&invite_code=XpctyaUgG8&limit_speaking=true&small_avatars=false&hide_names=false&fade_chat=0", 133 | // }, 134 | // }), 135 | // }, 136 | }, 137 | }); 138 | 139 | const cameraScene = defineScene({ 140 | name: "Camera", 141 | items: { 142 | webcam: { 143 | index: 0, 144 | input: webcam, 145 | positionX: 0, 146 | positionY: 0, 147 | scaleX: 1, 148 | scaleY: 1, 149 | alignment: "topLeft", 150 | }, 151 | mic: { 152 | index: 1, 153 | input: micInput, 154 | }, 155 | }, 156 | }); 157 | 158 | export const CAM_HEIGHT = 350; 159 | export const CAM_WIDTH = (CAM_HEIGHT * 16) / 9; 160 | 161 | async function main() { 162 | const obs = new OBS(); 163 | await obs.connect("ws://localhost:4455"); 164 | 165 | const main = await syncScene(obs, mainScene); 166 | const camera = await syncScene(obs, cameraScene); 167 | 168 | await obs.setCurrentScene(main); 169 | } 170 | 171 | main(); 172 | -------------------------------------------------------------------------------- /packages/core-old/tests/Filter.test.ts: -------------------------------------------------------------------------------- 1 | import { Filter, Scene, mocks } from "../src"; 2 | import { obs } from "./utils"; 3 | 4 | describe("setSettings", () => { 5 | it("sets the filter's settings", async () => { 6 | const filter = new mocks.MockFilter({ 7 | name: "Filter", 8 | settings: {}, 9 | }); 10 | 11 | const scene = await new Scene({ 12 | name: "Scene", 13 | items: {}, 14 | filters: { 15 | test: filter, 16 | }, 17 | }).create(obs); 18 | 19 | await filter.setSettings({ 20 | a: 1, 21 | }); 22 | 23 | const { filterSettings } = await obs.call("GetSourceFilter", { 24 | sourceName: scene.name, 25 | filterName: filter.name, 26 | }); 27 | 28 | expect(filterSettings).toEqual({ 29 | a: 1, 30 | }); 31 | }); 32 | }); 33 | 34 | describe("setEnabled", () => { 35 | it("sets the filter's enabled state", async () => { 36 | const filter = new mocks.MockFilter({ 37 | name: "Filter", 38 | settings: {}, 39 | }); 40 | 41 | const scene = await new Scene({ 42 | name: "Scene", 43 | items: {}, 44 | filters: { 45 | test: filter, 46 | }, 47 | }).create(obs); 48 | 49 | expect(filter.enabled).toBe(true); 50 | 51 | await filter.setEnabled(false); 52 | 53 | expect(filter.enabled).toBe(false); 54 | 55 | const { filterEnabled } = await obs.call("GetSourceFilter", { 56 | sourceName: scene.name, 57 | filterName: filter.name, 58 | }); 59 | 60 | expect(filterEnabled).toBe(false); 61 | }); 62 | }); 63 | 64 | describe("remove", () => { 65 | it("removes the filter", async () => { 66 | const filter = new mocks.MockFilter({ 67 | name: "Filter", 68 | settings: {}, 69 | }); 70 | 71 | const scene = await new Scene({ 72 | name: "Scene", 73 | items: {}, 74 | filters: { 75 | test: filter, 76 | }, 77 | }).create(obs); 78 | 79 | expect(scene.filters.length).toBe(1); 80 | 81 | await filter.remove(); 82 | 83 | expect(scene.filters.length).toBe(0); 84 | expect(filter.source).toBeUndefined(); 85 | 86 | const { filters } = await obs.call("GetSourceFilterList", { 87 | sourceName: scene.name, 88 | }); 89 | expect(filters).toEqual([]); 90 | }); 91 | }); 92 | 93 | describe("checkSource", () => { 94 | it("throws an error if source is undefined", async () => { 95 | const filter = new Filter({ 96 | name: "Filter", 97 | kind: "test", 98 | settings: {}, 99 | }); 100 | 101 | expect(() => filter.checkSource()).toThrow("does not have a source"); 102 | }); 103 | }); 104 | 105 | describe("create", () => { 106 | it("adds filters to the source", async () => { 107 | const scene = await new Scene({ 108 | name: "Scene", 109 | items: {}, 110 | }).create(obs); 111 | 112 | const filter = new mocks.MockFilter({ 113 | name: "Filter", 114 | settings: {}, 115 | }); 116 | 117 | await filter.create("test", scene); 118 | 119 | expect(filter.source).toBe(scene); 120 | 121 | const { filters } = await obs.call("GetSourceFilterList", { 122 | sourceName: scene.name, 123 | }); 124 | expect(filters).toEqual([ 125 | { 126 | filterName: filter.name, 127 | filterEnabled: filter.enabled, 128 | filterIndex: 0, 129 | filterKind: filter.kind, 130 | filterSettings: {}, 131 | }, 132 | ]); 133 | }); 134 | 135 | it("sets settings on existing filters on the source", async () => { 136 | const scene = await new Scene({ 137 | name: "Scene", 138 | items: {}, 139 | }).create(obs); 140 | 141 | const filter = new mocks.MockFilter({ 142 | name: "Filter", 143 | settings: { 144 | a: 1, 145 | }, 146 | }); 147 | 148 | await obs.call("CreateSourceFilter", { 149 | sourceName: scene.name, 150 | filterName: filter.name, 151 | filterKind: filter.kind, 152 | filterSettings: { 153 | a: 0, 154 | }, 155 | }); 156 | 157 | await filter.create("filter", scene); 158 | 159 | expect(filter.source).toBe(scene); 160 | 161 | const { filters } = await obs.call("GetSourceFilterList", { 162 | sourceName: scene.name, 163 | }); 164 | expect(filters).toEqual([ 165 | { 166 | filterName: filter.name, 167 | filterEnabled: filter.enabled, 168 | filterIndex: 0, 169 | filterKind: filter.kind, 170 | filterSettings: { 171 | a: 1, 172 | }, 173 | }, 174 | ]); 175 | }); 176 | 177 | it("does nothing if filter has already been added to its source", async () => { 178 | const scene = await new Scene({ 179 | name: "Scene", 180 | items: {}, 181 | }).create(obs); 182 | 183 | const filter = new mocks.MockFilter({ 184 | name: "Filter", 185 | settings: {}, 186 | }); 187 | 188 | await filter.create("test", scene); 189 | 190 | await expect(filter.create("test", scene)).resolves.not.toThrow(); 191 | }); 192 | 193 | it("fails if filter has already been added to a source", async () => { 194 | const scene = await new Scene({ 195 | name: "Scene", 196 | items: {}, 197 | }).create(obs); 198 | 199 | const scene2 = await new Scene({ 200 | name: "Scene2", 201 | items: {}, 202 | }).create(obs); 203 | 204 | const filter = new mocks.MockFilter({ 205 | name: "Filter", 206 | settings: {}, 207 | }); 208 | 209 | await filter.create("test", scene); 210 | 211 | await expect(filter.create("test", scene2)).rejects.toThrow( 212 | "already been added" 213 | ); 214 | }); 215 | 216 | it("fails if filter with the same name exists with a different type", async () => { 217 | const scene = await new Scene({ 218 | name: "Scene", 219 | items: {}, 220 | }).create(obs); 221 | 222 | const filter = new Filter({ 223 | name: "Filter", 224 | kind: "test", 225 | settings: {}, 226 | }); 227 | 228 | await obs.call("CreateSourceFilter", { 229 | sourceName: scene.name, 230 | filterName: filter.name, 231 | filterKind: "anotherKind", 232 | }); 233 | 234 | await expect(filter.create("test", scene)).rejects.toThrow( 235 | "different kind" 236 | ); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /packages/core-old/src/OBS.ts: -------------------------------------------------------------------------------- 1 | import ObsWebSocket, { EventSubscription } from "obs-websocket-js"; 2 | 3 | import { 4 | OBSEventTypes, 5 | OBSRequestTypes, 6 | OBSResponseTypes, 7 | Settings, 8 | } from "./types"; 9 | import { Scene } from "./Scene"; 10 | import { Input } from "./Input"; 11 | import { SourceRefs } from "./Source"; 12 | 13 | export class OBS { 14 | /** 15 | * The OBS websocket connection used internally 16 | */ 17 | socket = new ObsWebSocket(); 18 | 19 | /** 20 | * All of the sources that this OBS instance has access to, excluding scenes 21 | */ 22 | inputs = new Map(); 23 | 24 | /** 25 | * All of the scenes that this OBS instance has access to 26 | */ 27 | scenes = new Map(); 28 | 29 | /** @internal */ 30 | rpcVersion!: number; 31 | 32 | /** 33 | * Connect this OBS instance to a websocket 34 | */ 35 | async connect(url: string, password?: string) { 36 | const data = await this.socket.connect(url, password, { 37 | eventSubscriptions: 38 | EventSubscription.Scenes | 39 | EventSubscription.Inputs | 40 | EventSubscription.Filters | 41 | EventSubscription.SceneItems | 42 | EventSubscription.MediaInputs, 43 | }); 44 | 45 | this.rpcVersion = data.negotiatedRpcVersion; 46 | 47 | this.inputs.clear(); 48 | this.scenes.clear(); 49 | } 50 | 51 | /** 52 | * Goes though each source in OBS and removes it if Sceneify owns it, 53 | * and there are no references to the source in code. 54 | */ 55 | async clean() { 56 | const { scenes } = await this.call("GetSceneList"); 57 | const { inputs } = await this.call("GetInputList"); 58 | 59 | const sourcesSettings = await Promise.all( 60 | [ 61 | ...scenes.map((s) => s.sceneName), 62 | ...inputs.map((i) => i.inputName), 63 | ].map(async (sourceName) => { 64 | const { sourceSettings } = await this.call("GetSourcePrivateSettings", { 65 | sourceName, 66 | }).catch(() => ({ 67 | sourceName, 68 | sourceSettings: {} as Settings, 69 | })); 70 | 71 | return { 72 | sourceName, 73 | sourceSettings, 74 | }; 75 | }) 76 | ); 77 | 78 | const sourcesRefs = sourcesSettings.reduce( 79 | (acc, data) => ({ 80 | ...acc, 81 | ...(data.sourceSettings.SCENEIFY_LINKED === false && 82 | data.sourceSettings.SCENEIFY_REFS 83 | ? { [data.sourceName]: data.sourceSettings.SCENEIFY_REFS } 84 | : {}), 85 | }), 86 | {} as Record 87 | ); 88 | 89 | // Delete refs that are actually in use 90 | for (let [_, scene] of this.scenes) { 91 | for (let item of scene.items) { 92 | delete sourcesRefs[item.source.name]?.[scene.name]?.[item.ref]; 93 | 94 | if ( 95 | Object.keys(sourcesRefs[item.source.name]?.[scene.name] ?? {}) 96 | .length === 0 97 | ) { 98 | delete sourcesRefs[item.source.name]?.[scene.name]; 99 | } 100 | 101 | if (Object.keys(sourcesRefs[item.source.name] ?? {}).length === 0) { 102 | delete sourcesRefs[item.source.name]; 103 | } 104 | } 105 | } 106 | 107 | const danglingItems = Object.values(sourcesRefs) 108 | .filter((r) => r !== undefined) 109 | .reduce( 110 | (acc, sourceRefs) => { 111 | let danglingInputItems = []; 112 | 113 | for (let [sceneName, refs] of Object.entries(sourceRefs)) { 114 | for (let sceneItemId of Object.values(refs)) { 115 | if (sourceRefs[sceneName] !== undefined) 116 | danglingInputItems.push({ 117 | sceneName, 118 | sceneItemId, 119 | }); 120 | } 121 | } 122 | 123 | return [...acc, ...danglingInputItems]; 124 | }, 125 | [] as { 126 | sceneName: string; 127 | sceneItemId: number; 128 | }[] 129 | ); 130 | 131 | await Promise.all( 132 | danglingItems.map((data) => 133 | this.call("RemoveSceneItem", data).catch(() => {}) 134 | ) 135 | ); 136 | 137 | const danglingOBSScenes = scenes.filter( 138 | ({ sceneName }) => 139 | !this.scenes.has(sceneName) && sourcesRefs[sceneName] !== undefined 140 | ); 141 | 142 | await Promise.all( 143 | danglingOBSScenes.map(({ sceneName }) => 144 | this.call("RemoveScene", { sceneName }).catch(() => {}) 145 | ) 146 | ); 147 | 148 | for (let danglingCodeScene of this.scenes.keys()) { 149 | if (scenes.every(({ sceneName }) => sceneName !== danglingCodeScene)) 150 | this.scenes.delete(danglingCodeScene); 151 | } 152 | 153 | for (let danglingCodeInputs of this.inputs.keys()) { 154 | if (inputs.every(({ inputName }) => inputName !== danglingCodeInputs)) 155 | this.inputs.delete(danglingCodeInputs); 156 | } 157 | 158 | // TODO: Clean filters 159 | await Promise.all( 160 | [...[...this.inputs.values()], ...[...this.scenes.values()]].map( 161 | (input) => input.refreshRefs().catch(() => {}) 162 | ) 163 | ); 164 | } 165 | 166 | call( 167 | requestType: T, 168 | requestData?: OBSRequestTypes[T] 169 | ): Promise { 170 | return this.socket.call(requestType as any, requestData as any); 171 | } 172 | 173 | on( 174 | event: T, 175 | callback: (data: OBSEventTypes[T]) => void 176 | ) { 177 | this.socket.on(event, callback as any); 178 | return this; 179 | } 180 | 181 | off( 182 | event: T, 183 | callback: (data: OBSEventTypes[T]) => void 184 | ) { 185 | this.socket.off(event, callback as any); 186 | } 187 | 188 | /** 189 | * Streaming state 190 | */ 191 | 192 | streaming = false; 193 | 194 | async startStreaming() { 195 | await this.call("StartStream"); 196 | 197 | this.streaming = true; 198 | } 199 | 200 | async stopStreaming() { 201 | await this.call("StopStream"); 202 | 203 | this.streaming = false; 204 | } 205 | 206 | async toggleStreaming() { 207 | const { outputActive } = await this.call("ToggleStream"); 208 | 209 | this.streaming = outputActive; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /packages/core-old/tests/OBS.test.ts: -------------------------------------------------------------------------------- 1 | import { Scene, mocks } from "../src"; 2 | import { obs } from "./utils"; 3 | 4 | describe("clean", () => { 5 | it("removes dangling scenes from OBS", async () => { 6 | const scene = new Scene({ 7 | name: "Test", 8 | items: { 9 | item: { 10 | source: new mocks.MockInput({ 11 | name: "Source", 12 | }), 13 | }, 14 | }, 15 | }); 16 | 17 | await scene.create(obs); 18 | 19 | const { scenes: scenesBeforeClean } = await obs.call("GetSceneList"); 20 | expect( 21 | scenesBeforeClean.find(({ sceneName }) => sceneName === scene.name) 22 | ).not.toBeUndefined(); 23 | 24 | obs.inputs.clear(); 25 | obs.scenes.clear(); 26 | 27 | await obs.clean(); 28 | 29 | const { scenes: scenesAfterClean } = await obs.call("GetSceneList"); 30 | expect( 31 | scenesAfterClean.find(({ sceneName }) => sceneName === scene.name) 32 | ).toBeUndefined(); 33 | }); 34 | 35 | it("removes dangling scenes from code", async () => { 36 | const scene = new Scene({ 37 | name: "Test", 38 | items: { 39 | item: { 40 | source: new mocks.MockInput({ 41 | name: "Source", 42 | }), 43 | }, 44 | }, 45 | }); 46 | 47 | await scene.create(obs); 48 | 49 | const { scenes: scenesBeforeClean } = await obs.call("GetSceneList"); 50 | expect( 51 | scenesBeforeClean.find(({ sceneName }) => sceneName === scene.name) 52 | ).not.toBeUndefined(); 53 | 54 | await obs.call("RemoveScene", { 55 | sceneName: scene.name, 56 | }); 57 | 58 | const { scenes: scenesAfterRemove } = await obs.call("GetSceneList"); 59 | expect( 60 | scenesAfterRemove.find(({ sceneName }) => sceneName === scene.name) 61 | ).toBeUndefined(); 62 | 63 | await obs.clean(); 64 | 65 | expect(obs.scenes.get(scene.name)).toBeUndefined(); 66 | }); 67 | 68 | it("removes dangling inputs from code", async () => { 69 | const scene = new Scene({ 70 | name: "Scene", 71 | items: { 72 | permanent: { 73 | source: new mocks.MockInput({ 74 | name: "Permanent", 75 | }), 76 | }, 77 | }, 78 | }); 79 | 80 | await scene.create(obs); 81 | 82 | const item = await scene.createItem("test", { 83 | source: new mocks.MockInput({ 84 | name: "Dangling", 85 | }), 86 | }); 87 | 88 | expect(obs.inputs.get(item.source.name)).not.toBeUndefined(); 89 | 90 | await obs.call("RemoveInput", { 91 | inputName: item.source.name, 92 | }); 93 | 94 | await obs.clean(); 95 | 96 | expect(obs.inputs.get(item.source.name)).toBeUndefined(); 97 | }); 98 | 99 | it("doesn't remove linked scenes", async () => { 100 | const sceneName = "Test"; 101 | 102 | await obs.call("CreateScene", { 103 | sceneName, 104 | }); 105 | 106 | const { sceneItemId } = await obs.call("CreateInput", { 107 | sceneName, 108 | inputName: "Test Input", 109 | inputKind: "mock", 110 | }); 111 | 112 | const { scenes: scenesBeforeClean } = await obs.call("GetSceneList"); 113 | expect(scenesBeforeClean.length).toBe(1); 114 | 115 | const scene = new Scene({ 116 | name: sceneName, 117 | items: { 118 | item: { 119 | source: new mocks.MockInput({ 120 | name: "Test Input", 121 | }), 122 | }, 123 | }, 124 | }); 125 | 126 | await scene.link(obs); 127 | 128 | expect(obs.scenes.size).toBe(1); 129 | 130 | await obs.clean(); 131 | 132 | const { scenes: scenesAfterClean } = await obs.call("GetSceneList"); 133 | 134 | expect(scenesAfterClean.length).toBe(1); 135 | expect(scene.item("item").id).toBe(sceneItemId); 136 | }); 137 | 138 | // Similar to create/destroy toggling bug JDude experienced a few times 139 | it("doesn't remove nested scenes", async () => { 140 | const doubleNested = new Scene({ 141 | name: "Double Nested", 142 | items: {}, 143 | }); 144 | 145 | const nested = new Scene({ 146 | name: "Nested", 147 | items: { 148 | doubleNested: { 149 | source: doubleNested, 150 | }, 151 | }, 152 | }); 153 | 154 | const parent = new Scene({ 155 | name: "Parent", 156 | items: { 157 | nested: { 158 | source: nested, 159 | }, 160 | doubleNested: { 161 | source: doubleNested, 162 | }, 163 | }, 164 | }); 165 | 166 | await parent.create(obs); 167 | 168 | await obs.clean(); 169 | 170 | const { sceneItems: parentItemsAfter } = await obs.call( 171 | "GetSceneItemList", 172 | { 173 | sceneName: parent.name, 174 | } 175 | ); 176 | expect(parentItemsAfter.length).toBe(2); 177 | 178 | const { sceneItems: nestedItemsAfter } = await obs.call( 179 | "GetSceneItemList", 180 | { 181 | sceneName: nested.name, 182 | } 183 | ); 184 | expect(nestedItemsAfter.length).toBe(1); 185 | 186 | const { sceneItems: doubleNestedItemsAfter } = await obs.call( 187 | "GetSceneItemList", 188 | { 189 | sceneName: doubleNested.name, 190 | } 191 | ); 192 | expect(doubleNestedItemsAfter.length).toBe(0); 193 | }); 194 | 195 | it("fails silently when sources/items are missing", async () => { 196 | const scene = new Scene({ 197 | name: "Test", 198 | items: { 199 | item: { 200 | source: new mocks.MockInput({ 201 | name: "Source", 202 | }), 203 | }, 204 | }, 205 | }); 206 | 207 | await scene.create(obs); 208 | 209 | await obs.call("RemoveInput", { 210 | inputName: scene.item("item").source.name, 211 | }); 212 | 213 | expect(() => obs.clean()).not.toThrow(); 214 | }); 215 | }); 216 | 217 | describe("startStreaming", () => { 218 | it("makes OBS start streaming", async () => { 219 | await obs.startStreaming(); 220 | 221 | const { outputActive } = await obs.call("GetStreamStatus"); 222 | expect(outputActive).toBe(true); 223 | }); 224 | }); 225 | 226 | describe("stopStreaming", () => { 227 | it("makes OBS stop streaming", async () => { 228 | await obs.stopStreaming(); 229 | 230 | const { outputActive } = await obs.call("GetStreamStatus"); 231 | expect(outputActive).toBe(false); 232 | }); 233 | }); 234 | 235 | describe("toggleStreaming", () => { 236 | it("toggle OBS' streaming state", async () => { 237 | { 238 | const { outputActive } = await obs.call("GetStreamStatus"); 239 | expect(outputActive).toBe(false); 240 | } 241 | 242 | await obs.toggleStreaming(); 243 | 244 | { 245 | const { outputActive } = await obs.call("GetStreamStatus"); 246 | expect(outputActive).toBe(true); 247 | } 248 | 249 | await obs.toggleStreaming(); 250 | 251 | { 252 | const { outputActive } = await obs.call("GetStreamStatus"); 253 | expect(outputActive).toBe(false); 254 | } 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /packages/core-old/tests/Input.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | MonitoringType, 4 | Scene, 5 | SceneItem, 6 | SourceFilters, 7 | mocks 8 | } from "../src"; 9 | import { obs } from "./utils"; 10 | 11 | /** 12 | * Exists to safeguard against pre-189101e, where item instances 13 | * were added inside createSceneItemObject, which isn't great since 14 | * the same thing needs to be done in overrides as well. 15 | */ 16 | it("adds item instances with createSceneItemObject overridden", async () => { 17 | class OverrideInputItem extends SceneItem { 18 | constructor(source: Input, scene: Scene, id: number, ref: string) { 19 | super(source, scene, id, ref); 20 | } 21 | } 22 | 23 | class OverrideInput extends mocks.MockInput { 24 | override createSceneItemObject( 25 | scene: Scene, 26 | id: number, 27 | ref: string 28 | ): SceneItem { 29 | return new OverrideInputItem(this, scene, id, ref); 30 | } 31 | } 32 | 33 | const input = new OverrideInput({ 34 | name: "Input", 35 | }); 36 | 37 | const scene = new Scene({ 38 | name: "Test", 39 | items: { 40 | item: { 41 | source: input, 42 | }, 43 | }, 44 | }); 45 | 46 | await scene.create(obs); 47 | 48 | expect(input.itemInstances.size).toBe(1); 49 | }); 50 | 51 | describe("fetchExists", () => { 52 | it("succeeds if an input exists with the same name", async () => { 53 | const input = new mocks.MockInput({ 54 | name: "Input", 55 | }); 56 | 57 | const scene = new Scene({ 58 | name: "Test", 59 | items: { 60 | item: { 61 | source: input, 62 | }, 63 | }, 64 | }); 65 | 66 | await scene.create(obs); 67 | 68 | input.obs = obs; 69 | expect(input.fetchExists()).resolves.toBe(true); 70 | }); 71 | 72 | it("fails if a scene exists with the same name", async () => { 73 | const input = new Input({ 74 | kind: "test", 75 | name: "Input", 76 | }); 77 | 78 | const scene = new Scene({ 79 | name: "Input", 80 | items: {}, 81 | }); 82 | 83 | await scene.create(obs); 84 | 85 | input.obs = obs; 86 | await expect(input.fetchExists()).rejects.toThrow(); 87 | }); 88 | }); 89 | 90 | describe("remove", () => { 91 | it("removes the input", async () => { 92 | const input = new mocks.MockInput({ 93 | name: "Input", 94 | }); 95 | 96 | const scene = new Scene({ 97 | name: "Test", 98 | items: { 99 | item: { 100 | source: input, 101 | }, 102 | }, 103 | }); 104 | 105 | await scene.create(obs); 106 | await input.remove(); 107 | 108 | expect(input.exists).toBe(false); 109 | expect(scene.item("item")).toBe(undefined); 110 | 111 | const { sceneItems } = await obs.call("GetSceneItemList", { 112 | sceneName: scene.name, 113 | }); 114 | expect(sceneItems.length).toBe(0); 115 | }); 116 | }); 117 | 118 | describe("createFirstSceneItem", () => { 119 | it("sets properties on creation", async () => { 120 | const input = new mocks.MockInput({ 121 | name: "Input", 122 | audioMonitorType: MonitoringType.MonitorAndOutput, 123 | audioSyncOffset: 1, 124 | muted: true, 125 | volume: { 126 | db: 0.5, 127 | }, 128 | }); 129 | 130 | const scene = new Scene({ 131 | name: "Test", 132 | items: { 133 | item: { 134 | source: input, 135 | }, 136 | }, 137 | }); 138 | 139 | await scene.create(obs); 140 | 141 | const { monitorType } = await obs.call("GetInputAudioMonitorType", { 142 | inputName: input.name, 143 | }); 144 | expect(monitorType).toBe(MonitoringType.MonitorAndOutput); 145 | 146 | const { inputAudioSyncOffset } = await obs.call("GetInputAudioSyncOffset", { 147 | inputName: input.name, 148 | }); 149 | expect(inputAudioSyncOffset).toBe(1); 150 | 151 | const { inputMuted } = await obs.call("GetInputMute", { 152 | inputName: input.name, 153 | }); 154 | expect(inputMuted).toBe(true); 155 | 156 | const { inputVolumeDb } = await obs.call("GetInputVolume", { 157 | inputName: input.name, 158 | }); 159 | expect(inputVolumeDb).toBe(0.5); 160 | }); 161 | }); 162 | 163 | describe("toggleMuted", () => { 164 | it("toggles mute state", async () => { 165 | const input = new mocks.MockInput({ 166 | name: "Input", 167 | }); 168 | 169 | const scene = new Scene({ 170 | name: "Test", 171 | items: { 172 | item: { 173 | source: input, 174 | }, 175 | }, 176 | }); 177 | 178 | await scene.create(obs); 179 | 180 | { 181 | const { inputMuted } = await obs.call("GetInputMute", { 182 | inputName: input.name, 183 | }); 184 | expect(inputMuted).toBe(false); 185 | } 186 | 187 | await input.toggleMuted(); 188 | 189 | { 190 | const { inputMuted } = await obs.call("GetInputMute", { 191 | inputName: input.name, 192 | }); 193 | expect(inputMuted).toBe(true); 194 | } 195 | }); 196 | }); 197 | 198 | describe("fetchProperties", () => { 199 | it("fetches properties", async () => { 200 | const input = new mocks.MockInput({ 201 | name: "Input", 202 | }); 203 | 204 | const scene = new Scene({ 205 | name: "Test", 206 | items: { 207 | item: { 208 | source: input, 209 | }, 210 | }, 211 | }); 212 | 213 | await scene.create(obs); 214 | 215 | { 216 | const { monitorType } = await obs.call("GetInputAudioMonitorType", { 217 | inputName: input.name, 218 | }); 219 | expect(monitorType).toBe(MonitoringType.None); 220 | 221 | const { inputAudioSyncOffset } = await obs.call( 222 | "GetInputAudioSyncOffset", 223 | { 224 | inputName: input.name, 225 | } 226 | ); 227 | expect(inputAudioSyncOffset).toBe(0); 228 | 229 | const { inputMuted } = await obs.call("GetInputMute", { 230 | inputName: input.name, 231 | }); 232 | expect(inputMuted).toBe(false); 233 | 234 | const { inputVolumeDb } = await obs.call("GetInputVolume", { 235 | inputName: input.name, 236 | }); 237 | expect(inputVolumeDb).toBe(0); 238 | } 239 | 240 | await obs.call("SetInputAudioMonitorType", { 241 | inputName: input.name, 242 | monitorType: MonitoringType.MonitorAndOutput, 243 | }); 244 | await obs.call("SetInputAudioSyncOffset", { 245 | inputName: input.name, 246 | inputAudioSyncOffset: 1, 247 | }); 248 | await obs.call("SetInputMute", { 249 | inputName: input.name, 250 | inputMuted: true, 251 | }); 252 | await obs.call("SetInputVolume", { 253 | inputName: input.name, 254 | inputVolumeDb: 0.5, 255 | }); 256 | 257 | await input.fetchProperties(); 258 | 259 | expect(input.audioMonitorType).toBe(MonitoringType.MonitorAndOutput); 260 | expect(input.audioSyncOffset).toBe(1); 261 | expect(input.muted).toBe(true); 262 | expect(input.volume.db).toBe(0.5); 263 | }); 264 | }); 265 | 266 | describe("setName", () => { 267 | it("renames the input", async () => { 268 | const input = new mocks.MockInput({ 269 | name: "Input", 270 | }); 271 | 272 | const scene = new Scene({ 273 | name: "Test", 274 | items: { 275 | item: { 276 | source: input, 277 | }, 278 | }, 279 | }); 280 | 281 | await scene.create(obs); 282 | 283 | expect(input.name).toBe("Input"); 284 | 285 | await input.setName("New Input"); 286 | expect(input.name).toBe("New Input"); 287 | }); 288 | 289 | it("reports error if source with name already exists", async () => { 290 | const input = new mocks.MockInput({ 291 | name: "Input", 292 | }); 293 | 294 | const input2 = new mocks.MockInput({ 295 | name: "Input2", 296 | }); 297 | 298 | const scene = new Scene({ 299 | name: "Scene", 300 | items: { 301 | input: { 302 | source: input, 303 | }, 304 | input2: { source: input2 }, 305 | }, 306 | }); 307 | 308 | await scene.create(obs); 309 | 310 | await expect(input.setName("Input2")).rejects.toThrow("Input with name"); 311 | await expect(input.setName("Scene")).rejects.toThrow("Scene with name"); 312 | }); 313 | }); 314 | -------------------------------------------------------------------------------- /packages/core-old/src/Input.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from "./Scene"; 2 | import { DeepPartial, Settings } from "./types"; 3 | import { SourceFilters, Source, SourceArgs } from "./Source"; 4 | import { MonitoringType } from "./constants"; 5 | import { SceneItem } from "./SceneItem"; 6 | import { OBS } from "./OBS"; 7 | 8 | export type CustomInputArgs< 9 | TSettings extends Settings, 10 | Filters extends SourceFilters 11 | > = Omit, "kind">; 12 | 13 | export interface InputArgs< 14 | TSettings extends Settings = {}, 15 | Filters extends SourceFilters = {} 16 | > extends SourceArgs { 17 | settings?: DeepPartial; 18 | volume?: { 19 | db?: number; 20 | mul?: number; 21 | }; 22 | audioMonitorType?: MonitoringType; 23 | audioSyncOffset?: number; 24 | muted?: boolean; 25 | } 26 | 27 | export type PropertyLists = Record; 28 | 29 | export type PropertyList = { 30 | enabled: boolean; 31 | name: string; 32 | value: T; 33 | }[]; 34 | 35 | const inputDefaultSettings = new Map(); 36 | 37 | export class Input< 38 | TSettings extends Settings = {}, 39 | Filters extends SourceFilters = {}, 40 | Properties extends PropertyLists = {} 41 | > extends Source { 42 | volume = { 43 | db: 0, 44 | mul: 0, 45 | }; 46 | audioMonitorType = MonitoringType.None; 47 | audioSyncOffset = 0; 48 | muted = false; 49 | 50 | /** 51 | * Set transitively in initialize if source exists 52 | * Set manually in createFirstSceneItem if source doesn't exist 53 | */ 54 | settings: TSettings = {} as any; 55 | 56 | /** @internal */ 57 | creationArgs: InputArgs; 58 | 59 | constructor(args: InputArgs) { 60 | super(args); 61 | 62 | this.creationArgs = args; 63 | } 64 | 65 | async setSettings(settings: DeepPartial, overlay = true) { 66 | await this.obs.call("SetInputSettings", { 67 | inputName: this.name, 68 | inputSettings: settings, 69 | overlay, 70 | }); 71 | 72 | this.settings = { 73 | ...this.settings, 74 | settings, 75 | }; 76 | } 77 | 78 | async getDefaultSettings() { 79 | const cached = inputDefaultSettings.get(this.kind); 80 | if (cached) 81 | return { 82 | ...cached, 83 | } as TSettings; 84 | 85 | const { defaultInputSettings } = await this.obs.call( 86 | "GetInputDefaultSettings", 87 | { 88 | inputKind: this.kind, 89 | } 90 | ); 91 | 92 | inputDefaultSettings.set(this.kind, defaultInputSettings); 93 | 94 | return defaultInputSettings as TSettings; 95 | } 96 | 97 | async getPropertyListItems( 98 | property: K 99 | ): Promise> { 100 | const resp = await this.obs.call("GetInputPropertiesListPropertyItems", { 101 | inputName: this.name, 102 | propertyName: property as string, 103 | }); 104 | 105 | return resp.propertyItems.map((i) => ({ 106 | enabled: i.itemEnabled, 107 | name: i.itemName, 108 | value: i.itemValue as Properties[K], 109 | })); 110 | } 111 | 112 | async fetchExists() { 113 | try { 114 | await this.obs.call("GetSourcePrivateSettings", { 115 | sourceName: this.name, 116 | }); 117 | } catch { 118 | return false; 119 | } 120 | 121 | // Does exist, check if it's an input 122 | const input = await this.obs 123 | .call("GetInputSettings", { 124 | inputName: this.name, 125 | }) 126 | .then((input) => input) 127 | .catch(() => undefined); 128 | 129 | if (!input) 130 | throw new Error( 131 | `Failed to initiailze input ${this.name}: Scene with this name already exists.` 132 | ); 133 | 134 | return true; 135 | } 136 | 137 | protected async createFirstSceneItem(scene: Scene, enabled?: boolean) { 138 | const { settings, audioMonitorType, audioSyncOffset, muted, volume } = 139 | this.creationArgs; 140 | 141 | const { sceneItemId } = await this.obs.call("CreateInput", { 142 | inputName: this.name, 143 | inputKind: this.kind, 144 | sceneName: scene.name, 145 | inputSettings: settings, 146 | sceneItemEnabled: enabled, 147 | }); 148 | 149 | this.obs.inputs.set(this.name, this); 150 | 151 | await this.setPrivateSettings({ 152 | SCENEIFY_LINKED: false, 153 | }); 154 | 155 | const defaultSettings = await this.getDefaultSettings(); 156 | 157 | this.settings = { 158 | ...defaultSettings, 159 | ...settings, 160 | }; 161 | 162 | let promises: Promise[] = []; 163 | 164 | // TODO: batch 165 | 166 | if (audioMonitorType) 167 | promises.push(this.setAudioMonitorType(audioMonitorType)); 168 | if (audioSyncOffset) 169 | promises.push(this.setAudioSyncOffset(audioSyncOffset)); 170 | if (muted) promises.push(this.setMuted(muted)); 171 | if (volume) promises.push(this.setVolume(volume)); 172 | 173 | Promise.all(promises); 174 | 175 | return sceneItemId; 176 | } 177 | 178 | override async initialize(obs: OBS) { 179 | await super.initialize(obs); 180 | 181 | if (this.exists) { 182 | this.obs.inputs.set(this.name, this); 183 | await this.setSettings(this.creationArgs.settings ?? ({} as any)); 184 | } 185 | } 186 | 187 | /** 188 | * Fetches the input's mute, volume, audio sync offset and 189 | * audio monitor type from OBS and assigns them to the input 190 | */ 191 | async fetchProperties() { 192 | const args = { inputName: this.name }; 193 | const [ 194 | { inputMuted }, 195 | { inputVolumeDb, inputVolumeMul }, 196 | { inputAudioSyncOffset }, 197 | { monitorType }, 198 | ] = await Promise.all([ 199 | this.obs.call("GetInputMute", args), 200 | this.obs.call("GetInputVolume", args), 201 | this.obs.call("GetInputAudioSyncOffset", args), 202 | this.obs.call("GetInputAudioMonitorType", args), 203 | ]); 204 | 205 | this.muted = inputMuted; 206 | this.volume = { 207 | db: inputVolumeDb, 208 | mul: inputVolumeMul, 209 | }; 210 | this.audioSyncOffset = inputAudioSyncOffset; 211 | this.audioMonitorType = monitorType as MonitoringType; 212 | } 213 | 214 | async setMuted(muted: boolean) { 215 | await this.obs.call("SetInputMute", { 216 | inputName: this.name, 217 | inputMuted: muted, 218 | }); 219 | 220 | this.muted = muted; 221 | } 222 | 223 | async toggleMuted() { 224 | const { inputMuted } = await this.obs.call("ToggleInputMute", { 225 | inputName: this.name, 226 | }); 227 | 228 | this.muted = inputMuted; 229 | 230 | return inputMuted; 231 | } 232 | 233 | async setVolume(data: { db?: number; mul?: number }) { 234 | await this.obs.call("SetInputVolume", { 235 | inputName: this.name, 236 | inputVolumeDb: (data as any).db, 237 | inputVolumeMul: (data as any).mul, 238 | }); 239 | 240 | const resp = await this.obs.call("GetInputVolume", { 241 | inputName: this.name, 242 | }); 243 | 244 | this.volume = { 245 | db: resp.inputVolumeDb, 246 | mul: resp.inputVolumeMul, 247 | }; 248 | } 249 | 250 | async setAudioSyncOffset(offset: number) { 251 | await this.obs.call("SetInputAudioSyncOffset", { 252 | inputName: this.name, 253 | inputAudioSyncOffset: offset, 254 | }); 255 | 256 | this.audioSyncOffset = offset; 257 | } 258 | 259 | async setAudioMonitorType(type: MonitoringType) { 260 | await this.obs.call("SetInputAudioMonitorType", { 261 | inputName: this.name, 262 | monitorType: type, 263 | }); 264 | 265 | this.audioMonitorType = type; 266 | } 267 | 268 | async remove() { 269 | await this.obs.call("RemoveInput", { 270 | inputName: this.name, 271 | }); 272 | 273 | this._exists = false; 274 | 275 | this.obs.inputs.delete(this.name); 276 | this.itemInstances.forEach((i) => { 277 | i.scene.items.splice(i.scene.items.indexOf(i), 1); 278 | }); 279 | } 280 | 281 | async setName(name: string) { 282 | if (this.obs.scenes.has(name)) 283 | throw new Error( 284 | `Failed to set name of scene ${this.name}: Scene with name '${name}' already exists` 285 | ); 286 | 287 | if (this.obs.inputs.has(name)) 288 | throw new Error( 289 | `Failed to set name of scene ${this.name}: Input with name '${name}' already exists` 290 | ); 291 | 292 | await this.obs.call("SetInputName", { 293 | inputName: this.name, 294 | newInputName: name, 295 | }); 296 | 297 | this.obs.inputs.delete(this.name); 298 | this.name = name; 299 | this.obs.inputs.set(this.name, this); 300 | } 301 | 302 | /** @internal */ 303 | removeItemInstance(item: SceneItem) { 304 | this.itemInstances.delete(item); 305 | 306 | if (this.itemInstances.size === 0) this._exists = false; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /packages/core/src/definition.ts: -------------------------------------------------------------------------------- 1 | import { OBSMonitoringType, OBSVolumeInput } from "./obs-types.js"; 2 | import { OBS } from "./obs.js"; 3 | import { SceneItemTransform } from "./sceneItem.js"; 4 | 5 | export function defineInputType< 6 | TSettings extends Record, 7 | TKind extends string 8 | >(kind: TKind) { 9 | return new InputType(kind); 10 | } 11 | 12 | abstract class SourceType { 13 | constructor(public id: TKind) {} 14 | 15 | abstract settings(): SourceType; 16 | } 17 | 18 | export type DefineInputArgs< 19 | TSettings, 20 | TFilters extends Record> 21 | > = { 22 | name: string; 23 | settings?: Partial; 24 | filters?: { [K in keyof TFilters]: Filter }; 25 | }; 26 | 27 | export type InputTypeSettings> = 28 | TType extends InputType ? TSettings : never; 29 | 30 | export class InputType< 31 | TKind extends string, 32 | TSettings extends Record 33 | > extends SourceType { 34 | settings>(): InputType< 35 | TKind, 36 | TSettings 37 | > { 38 | return this as any; 39 | } 40 | 41 | defineInput>>( 42 | args: DefineInputArgs 43 | ): Input { 44 | return new Input(this, args as any); 45 | } 46 | 47 | async getDefaultSettings(obs: OBS) { 48 | const res = await obs.ws.call("GetInputDefaultSettings", { 49 | inputKind: this.id, 50 | }); 51 | 52 | return res.defaultInputSettings as TSettings; 53 | } 54 | } 55 | 56 | type DefineFilterArgs = { 57 | name: string; 58 | enabled?: boolean; 59 | index?: number; 60 | settings?: Partial; 61 | }; 62 | 63 | export type FilterTypeSettings> = 64 | TType extends FilterType ? TSettings : never; 65 | 66 | export class FilterType< 67 | TKind extends string = string, 68 | TSettings extends Record = any 69 | > extends SourceType { 70 | settings>(): FilterType< 71 | TKind, 72 | TSettings 73 | > { 74 | return this as any; 75 | } 76 | 77 | defineFilter(args: DefineFilterArgs): Filter { 78 | return new Filter(this, args as any); 79 | } 80 | 81 | async getDefaultSettings(obs: OBS) { 82 | return await obs.ws 83 | .call("GetSourceFilterDefaultSettings") 84 | .then((r) => r.defaultFilterSettings as TSettings); 85 | } 86 | } 87 | 88 | export function defineFilterType< 89 | TSettings extends Record, 90 | TKind extends string 91 | >(kind: TKind) { 92 | return new FilterType(kind); 93 | } 94 | 95 | export type InputSettings> = 96 | TInput extends Input ? InputTypeSettings : never; 97 | 98 | export class Input< 99 | TType extends InputType, 100 | TFilters extends Record 101 | > { 102 | constructor( 103 | public type: TType, 104 | public args: DefineInputArgs, TFilters> 105 | ) {} 106 | 107 | get name() { 108 | return this.args.name; 109 | } 110 | 111 | filter(key: keyof TFilters) { 112 | return this.args.filters?.[key]; 113 | } 114 | 115 | async getSettings(obs: OBS): Promise> { 116 | const settings = await obs.ws 117 | .call("GetInputSettings", { inputName: this.args.name }) 118 | .then((d) => d.inputSettings as any); 119 | 120 | const defaultSettings = await this.type.getDefaultSettings(obs); 121 | 122 | return { ...defaultSettings, ...settings }; 123 | } 124 | 125 | async setSettings( 126 | obs: OBS, 127 | settings: Partial>, 128 | overlay?: boolean 129 | ) { 130 | await obs.ws.call("SetInputSettings", { 131 | inputName: this.args.name, 132 | inputSettings: settings, 133 | overlay, 134 | }); 135 | } 136 | 137 | async getMuted(obs: OBS) { 138 | return await obs.ws 139 | .call("GetInputMute", { inputName: this.args.name }) 140 | .then((r) => r.inputMuted); 141 | } 142 | 143 | async setMuted(obs: OBS, muted: boolean) { 144 | await obs.ws.call("SetInputMute", { 145 | inputName: this.args.name, 146 | inputMuted: muted, 147 | }); 148 | } 149 | 150 | async toggleMuted(obs: OBS) { 151 | return await obs.ws 152 | .call("ToggleInputMute", { inputName: this.args.name }) 153 | .then((r) => r.inputMuted); 154 | } 155 | 156 | async getVolume(obs: OBS) { 157 | return await obs.ws 158 | .call("GetInputVolume", { inputName: this.args.name }) 159 | .then((r) => ({ db: r.inputVolumeDb, mul: r.inputVolumeMul })); 160 | } 161 | 162 | async setVolume(obs: OBS, data: OBSVolumeInput) { 163 | await obs.ws.call("SetInputVolume", { 164 | inputName: this.args.name, 165 | ...("db" in data 166 | ? { inputVolumeDb: data.db } 167 | : { inputVolumeMul: data.mul }), 168 | }); 169 | } 170 | 171 | async getAudioSyncOffset(obs: OBS) { 172 | return await obs.ws 173 | .call("GetInputAudioSyncOffset", { inputName: this.args.name }) 174 | .then((r) => r.inputAudioSyncOffset); 175 | } 176 | 177 | async setAudioSyncOffset(obs: OBS, offset: number) { 178 | await obs.ws.call("SetInputAudioSyncOffset", { 179 | inputName: this.args.name, 180 | inputAudioSyncOffset: offset, 181 | }); 182 | } 183 | 184 | async setAudioMonitorType(obs: OBS, type: OBSMonitoringType) { 185 | await obs.ws.call("SetInputAudioMonitorType", { 186 | inputName: this.args.name, 187 | monitorType: type, 188 | }); 189 | } 190 | 191 | async getSettingListItems & string>( 192 | obs: OBS, 193 | setting: K 194 | ) { 195 | const res = await obs.ws.call("GetInputPropertiesListPropertyItems", { 196 | inputName: this.args.name, 197 | propertyName: setting, 198 | }); 199 | 200 | return res.propertyItems.map((i) => ({ 201 | enabled: i.itemEnabled as boolean, 202 | name: i.itemName as string, 203 | value: i.itemValue as InputTypeSettings[K], 204 | })); 205 | } 206 | 207 | async getFilters(obs: OBS) { 208 | const { filters } = await obs.ws.call("GetSourceFilterList", { 209 | sourceName: this.args.name, 210 | }); 211 | 212 | return filters.map((f) => ({ 213 | enabled: f.filterEnabled as boolean, 214 | index: f.filterIndex as number, 215 | kind: f.filterKind as string, 216 | name: f.filterName as string, 217 | settings: f.filterSettings as any, 218 | })); 219 | } 220 | } 221 | 222 | export type InputFilters> = T extends Input< 223 | any, 224 | infer TFilters 225 | > 226 | ? TFilters 227 | : never; 228 | 229 | export type FilterSettings> = TInput extends Filter< 230 | infer TType 231 | > 232 | ? FilterTypeSettings 233 | : never; 234 | 235 | export class Filter> { 236 | constructor( 237 | public kind: TType, 238 | public args: DefineFilterArgs> 239 | ) {} 240 | 241 | get name() { 242 | return this.args.name; 243 | } 244 | 245 | async setSettings( 246 | obs: OBS, 247 | source: string | { name: string }, 248 | filterSettings: Partial>, 249 | overlay = true 250 | ) { 251 | await obs.ws.call("SetSourceFilterSettings", { 252 | sourceName: typeof source === "string" ? source : source.name, 253 | filterName: this.args.name, 254 | filterSettings, 255 | overlay, 256 | }); 257 | } 258 | 259 | async setIndex( 260 | obs: OBS, 261 | source: string | { name: string }, 262 | filterIndex: number 263 | ) { 264 | await obs.ws.call("SetSourceFilterIndex", { 265 | sourceName: typeof source === "string" ? source : source.name, 266 | filterName: this.args.name, 267 | filterIndex, 268 | }); 269 | } 270 | 271 | async setEnabled( 272 | obs: OBS, 273 | source: string | { name: string }, 274 | filterEnabled: boolean 275 | ) { 276 | await obs.ws.call("SetSourceFilterEnabled", { 277 | sourceName: typeof source === "string" ? source : source.name, 278 | filterName: this.args.name, 279 | filterEnabled, 280 | }); 281 | } 282 | } 283 | 284 | export type DefineSceneItemArgs> = { 285 | input: TInput; 286 | index?: number; 287 | enabled?: boolean; 288 | } & Partial; 289 | 290 | export type SceneItems = Record>; 291 | 292 | type DefineSceneArgs = { 293 | name: string; 294 | items?: { [K in keyof TItems]: DefineSceneItemArgs }; 295 | filters?: Record; 296 | }; 297 | 298 | export type SIOfScene = T extends Scene 299 | ? TItems 300 | : never; 301 | 302 | export class Scene { 303 | constructor(public args: DefineSceneArgs) {} 304 | 305 | get name() { 306 | return this.args.name; 307 | } 308 | 309 | async getItems(obs: OBS) { 310 | return await obs.ws 311 | .call("GetSceneItemList", { sceneName: this.args.name }) 312 | .then( 313 | (res) => 314 | res.sceneItems as Array<{ 315 | sceneItemId: number; 316 | sceneItemIndex: number; 317 | sourceName: string; 318 | inputKind: string; 319 | }> 320 | ); 321 | } 322 | } 323 | 324 | export function defineScene( 325 | args: DefineSceneArgs 326 | ) { 327 | return new Scene(args); 328 | } 329 | --------------------------------------------------------------------------------