├── .eslintrc
├── .github
└── workflows
│ └── package.yml
├── .gitignore
├── .releaserc
├── PAGE.md
├── README.md
├── icon.svg
├── package-lock.json
├── package.json
├── plugin.json
├── scripts
└── bump-manifest.ts
├── src
└── index.ts
└── tsconfig.json
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2020": true
5 | },
6 | "extends": [
7 | "plugin:prettier/recommended",
8 | "plugin:@typescript-eslint/eslint-recommended",
9 | "plugin:@typescript-eslint/recommended"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaVersion": 2020,
14 | "sourceType": "module"
15 | },
16 | "plugins": ["@typescript-eslint", "prettier"],
17 | "rules": {
18 | "prettier/prettier": "warn"
19 | },
20 | "settings": {
21 | "import/resolver": {
22 | "node": {
23 | "extensions": [".js", ".ts"]
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/package.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - "main"
5 |
6 | jobs:
7 | build:
8 | name: Build and pack
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - run: npm ci
13 | - run: npm run build
14 | - run: npx semantic-release
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 |
84 | # Gatsby files
85 | .cache/
86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
87 | # https://nextjs.org/blog/next-9-1#public-directory-support
88 | # public
89 |
90 | # vuepress build output
91 | .vuepress/dist
92 |
93 | # Serverless directories
94 | .serverless/
95 |
96 | # FuseBox cache
97 | .fusebox/
98 |
99 | # DynamoDB Local files
100 | .dynamodb/
101 |
102 | # TernJS port file
103 | .tern-port
104 |
105 | # Build output
106 | lib/
107 | *.midiMixerPlugin
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "main"
4 | ],
5 | "plugins": [
6 | [
7 | "@semantic-release/commit-analyzer",
8 | {
9 | "releaseRules": [
10 | {
11 | "type": "breaking",
12 | "release": "major"
13 | },
14 | {
15 | "type": "feat",
16 | "release": "minor"
17 | },
18 | {
19 | "message": "*",
20 | "release": "patch"
21 | },
22 | {
23 | "scope": "no-release",
24 | "release": false
25 | }
26 | ]
27 | }
28 | ],
29 | "@semantic-release/release-notes-generator",
30 | [
31 | "@semantic-release/npm",
32 | {
33 | "npmPublish": false
34 | }
35 | ],
36 | [
37 | "@semantic-release/github",
38 | {
39 | "assets": [
40 | "*.midiMixerPlugin"
41 | ]
42 | }
43 | ],
44 | [
45 | "@semantic-release/git",
46 | {
47 | "assets": [
48 | "package.json",
49 | "package-lock.json",
50 | "plugin.json"
51 | ]
52 | }
53 | ]
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/PAGE.md:
--------------------------------------------------------------------------------
1 | ## This Requires the OBS Websocket version 5
2 |
3 | # OBS Plugin
4 |
5 | A proof-of-concept plugin that provides very basic OBS integration.
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OBS Plugin
2 |
3 | Currently this is a proof-of-concept plugin to show some practical usage of [midi-mixer-plugin](https://github.com/midi-mixer/midi-mixer-plugin). It provides an exceedingly basic OBS integration by adding a list of all sources to your MIDI Mixer assignments list and button types for switching between scenes.
4 |
5 | ## Usage
6 |
7 | 1. Download and install [obs-websocket](https://obsproject.com/forum/resources/obs-websocket-remote-control-obs-studio-from-websockets.466/) for OBS
8 | 2. [Download the plugin](https://github.com/midi-mixer/plugin-obs/releases/latest)
9 | 3. Open the `.MidiMixerPlugin` file with midi mixer
10 |
11 | For more information on how this plugin was made (or to make your own) check out [midi-mixer-plugin](https://github.com/midi-mixer/midi-mixer-plugin) and the [midi-mixer/plugin-template](https://github.com/midi-mixer/plugin-template) repository.
12 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.midi-mixer.obs",
3 | "description": "An OBS integration for MIDI Mixer.",
4 | "scripts": {
5 | "test": "echo \"Error: no test specified\" && exit 1",
6 | "clean": "rimraf dist",
7 | "prestart": "npm run clean",
8 | "build": "tsc",
9 | "semantic-release": "semantic-release",
10 | "postversion": "npx ts-node scripts/bump-manifest.ts && midi-mixer pack",
11 | "pack": "midi-mixer pack"
12 | },
13 | "files": [
14 | "icon.svg",
15 | "PAGE.md",
16 | "plugin.json",
17 | "lib"
18 | ],
19 | "license": "ISC",
20 | "devDependencies": {
21 | "@semantic-release/exec": "6.0.3",
22 | "@semantic-release/git": "10.0.1",
23 | "@types/node": "17.0.5",
24 | "@typescript-eslint/eslint-plugin": "^4.14.2",
25 | "@typescript-eslint/parser": "^4.14.2",
26 | "eslint": "^7.19.0",
27 | "eslint-config-prettier": "^7.2.0",
28 | "eslint-plugin-import": "^2.22.1",
29 | "eslint-plugin-prettier": "^3.3.1",
30 | "midi-mixer-cli": "^1.0.5",
31 | "prettier": "^2.2.1",
32 | "rimraf": "^3.0.2",
33 | "semantic-release": "18.0.1",
34 | "typescript": "^4.1.3"
35 | },
36 | "dependencies": {
37 | "midi-mixer-plugin": "^1.0.2",
38 | "obs-websocket-js": "^5.0.2",
39 | "ws": "8.4.0"
40 | },
41 | "bundledDependencies": [
42 | "midi-mixer-plugin",
43 | "obs-websocket-js",
44 | "ws"
45 | ],
46 | "volta": {
47 | "node": "14.15.4",
48 | "npm": "8.19.2"
49 | },
50 | "version": "1.0.0",
51 | "bundleDependencies": [
52 | "midi-mixer-plugin",
53 | "obs-websocket-js",
54 | "ws"
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/midi-mixer-plugin/plugin.schema.json",
3 | "id": "com.midi-mixer.obs",
4 | "name": "OBS",
5 | "type": "node",
6 | "version": "1.0.0",
7 | "author": "MIDI Mixer",
8 | "main": "lib/index.js",
9 | "icon": "icon.svg",
10 | "settings": {
11 | "address": {
12 | "label": "OBS WebSocket Address",
13 | "required": false,
14 | "type": "text",
15 | "fallback": "ws://localhost:4455"
16 | },
17 | "password": {
18 | "label": "OBS WebSocket Password",
19 | "required": false,
20 | "type": "password"
21 | },
22 | "status": {
23 | "label": "Status",
24 | "type": "status",
25 | "fallback": "Disconnected"
26 | },
27 | "reconnect": {
28 | "label": "Reconnect",
29 | "type": "button"
30 | },
31 | "meterMultiplier": {
32 | "label": "Meter Scaling Multiplier",
33 | "type": "text",
34 | "fallback": "1"
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/scripts/bump-manifest.ts:
--------------------------------------------------------------------------------
1 | import { writeFileSync } from "fs";
2 | import { version } from "../package.json";
3 | import manifest from "../plugin.json";
4 |
5 | writeFileSync("plugin.json", JSON.stringify({ ...manifest, version }, null, 2));
6 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Assignment, ButtonType } from "midi-mixer-plugin";
2 | import OBSWebSocket, { OBSWebSocketError, EventSubscription, OBSEventTypes, OBSResponseTypes } from "obs-websocket-js";
3 |
4 | interface Settings {
5 | address?: string;
6 | password?: string;
7 | meterMultiplier?: string
8 | meterScaling?: number
9 | }
10 |
11 | const obs = new OBSWebSocket();
12 | let inputs: Record = {};
13 | let scenes: Record = {};
14 | const settingsP: Promise = $MM.getSettings();
15 | let settings: Settings;
16 |
17 | const connect = async () => {
18 | settings = await settingsP;
19 |
20 | // We need to use a text string to allow decimals. If given value is not a valid number set it to 1
21 | if (!(settings.meterScaling = Number(settings.meterMultiplier))) {
22 | settings.meterScaling = 1;
23 | }
24 |
25 | let address = (settings.address ?? "ws://localhost:4455")
26 | if (!address.startsWith("ws://") && !address.startsWith("wss://")) {
27 | address = `ws://${address}`;
28 | }
29 |
30 | return obs.connect(address, settings.password ?? "", { eventSubscriptions: EventSubscription.All | EventSubscription.InputVolumeMeters })
31 | };
32 |
33 |
34 | const registerListeners = () => {
35 | obs.on("InputVolumeChanged", (data) => {
36 | const source = inputs[data.inputName];
37 | if (!source) return;
38 |
39 | source.volume = data.inputVolumeMul;
40 | });
41 |
42 | obs.on("InputMuteStateChanged", (data) => {
43 | const source = inputs[data.inputName];
44 | if (!source) return;
45 |
46 | source.muted = data.inputMuted;
47 | });
48 |
49 | // TODO: Test this thoroughly
50 | obs.on("InputVolumeMeters", (data) => {
51 | data.inputs.forEach((input: any) => {
52 | // Only update if non-zero audio levels
53 | if (input.inputLevelsMul.length == 0 || input.inputLevelsMul[0][0] == 0 || !settings.meterScaling || settings.meterScaling == 0) return;
54 | // I think [0] is left channel and [1] is right channel.
55 | // console.log(input.inputLevelsMul[0][1]);
56 | inputs[input.inputName].meter = input.inputLevelsMul[0][1] * settings.meterScaling;
57 | });
58 | });
59 |
60 | obs.on("CurrentProgramSceneChanged", (data) => {
61 | Object.values(scenes).forEach((button) => {
62 | button.active = data.sceneName === button.id;
63 | });
64 | });
65 |
66 | obs.on("ExitStarted", () => {
67 | disconnect();
68 | init();
69 | })
70 | };
71 |
72 | const mapSources = async () => {
73 | const data = await obs.call("GetInputList");
74 |
75 | // TODO: Would prefer this not be "any" but the actual type is "JsonObject" which sucks to use
76 | data.inputs?.forEach(async (input: any) => {
77 | try {
78 | const [volume, muted] = await Promise.all([
79 | obs
80 | .call("GetInputVolume", {
81 | inputName: input.inputName,
82 | })
83 | .then((res) => res.inputVolumeMul),
84 | obs
85 | .call("GetInputMute", {
86 | inputName: input.inputName,
87 | })
88 | .then((res) => res.inputMuted),
89 | ]);
90 |
91 | const assignment = new Assignment(input.inputName, {
92 | name: input.inputName,
93 | muted,
94 | volume,
95 | });
96 |
97 | assignment.on("volumeChanged", (level: number) => {
98 | assignment.volume = level;
99 | obs.call("SetInputVolume", {
100 | inputName: input.inputName,
101 | inputVolumeMul: level,
102 | });
103 | });
104 |
105 | assignment.on("mutePressed", () => {
106 | obs.call("SetInputMute", {
107 | inputName: input.inputName,
108 | inputMuted: !assignment.muted,
109 | });
110 | });
111 |
112 | inputs[input.inputName] = assignment;
113 | }
114 | catch (e: any) {
115 | if (e instanceof OBSWebSocketError) {
116 | if (e.code == 604) {
117 | // https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatusinvalidresourcestate
118 | // Usual cause is that input does not support audio
119 | }
120 | else {
121 | console.log(e);
122 | console.log(input);
123 | }
124 | }
125 | }
126 | });
127 | };
128 |
129 | const mapScenes = async () => {
130 | const data = await obs.call("GetSceneList");
131 |
132 | // TODO: Would prefer this not be "any" but the actual type is "JsonObject" which sucks to use
133 | data.scenes.forEach((scene: any) => {
134 |
135 | const button = new ButtonType(scene.sceneName, {
136 | name: `OBS: Switch to "${scene.sceneName}" scene`,
137 | active: scene.sceneName === data.currentProgramSceneName,
138 | });
139 |
140 | button.on("pressed", () => {
141 | obs.call("SetCurrentProgramScene", {
142 | sceneName: scene.sceneName,
143 | });
144 |
145 | button.active = true;
146 | });
147 |
148 | scenes[scene.sceneName] = button;
149 | });
150 | };
151 |
152 | function disconnect() {
153 | console.log("Disconnecting");
154 | obs.disconnect();
155 | for (let k in inputs) {
156 | let s = inputs[k];
157 | s.remove();
158 | }
159 |
160 | for (let k in scenes) {
161 | let s = scenes[k];
162 | s.remove();
163 | }
164 | }
165 |
166 |
167 | const init = async () => {
168 | console.log("Initializing");
169 | obs.disconnect();
170 | inputs = {};
171 | scenes = {};
172 |
173 | try {
174 | $MM.setSettingsStatus("status", "Connecting...");
175 |
176 | await connect();
177 | registerListeners();
178 | await Promise.all([mapSources(), mapScenes()]);
179 |
180 | $MM.setSettingsStatus("status", "Connected");
181 | } catch (err: any) {
182 | console.warn("OBS error:", err);
183 | $MM.setSettingsStatus("status", err.description || err.message || err);
184 | }
185 | };
186 |
187 | $MM.onSettingsButtonPress("reconnect", init);
188 |
189 | init();
190 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "src/**/*"
4 | ],
5 | "exclude": [
6 | "node_modules"
7 | ],
8 | "compilerOptions": {
9 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
10 | /* Basic Options */
11 | // "incremental": true, /* Enable incremental compilation */
12 | "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
13 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
14 | "lib": [
15 | "ES2020",
16 | "DOM"
17 | ] /* Specify library files to be included in the compilation. */,
18 | // "allowJs": true, /* Allow javascript files to be compiled. */
19 | // "checkJs": true, /* Report errors in .js files. */
20 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
21 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
22 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
23 | // "sourceMap": true, /* Generates corresponding '.map' file. */
24 | // "outFile": "./", /* Concatenate and emit output to single file. */
25 | "outDir": "./lib", /* Redirect output structure to the directory. */
26 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
27 | // "composite": true, /* Enable project compilation */
28 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
29 | // "removeComments": true, /* Do not emit comments to output. */
30 | "noEmit": false /* Do not emit outputs. */,
31 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
32 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
33 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
34 | /* Strict Type-Checking Options */
35 | "strict": true /* Enable all strict type-checking options. */,
36 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
37 | // "strictNullChecks": true, /* Enable strict null checks. */
38 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
39 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
40 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
41 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
42 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
43 | /* Additional Checks */
44 | // "noUnusedLocals": true, /* Report errors on unused locals. */
45 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
46 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
47 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
48 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
49 | /* Module Resolution Options */
50 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
51 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
52 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
53 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
54 | // "typeRoots": [], /* List of folders to include type definitions from. */
55 | // "types": [], /* Type declaration files to be included in compilation. */
56 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
57 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
59 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
60 | /* Source Map Options */
61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
65 | /* Experimental Options */
66 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
67 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
68 | /* Advanced Options */
69 | "skipLibCheck": true /* Skip type checking of declaration files. */,
70 | "resolveJsonModule": true,
71 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
72 | }
73 | }
74 |
--------------------------------------------------------------------------------