Commit 0653ef2d authored by Hugo "ThePooN" Denizart's avatar Hugo "ThePooN" Denizart
Browse files

Initial commit

parents
build/
node_modules/
\ No newline at end of file
This diff is collapsed.
# obs-sync
Synchronizes scenes between different OBS instances using [obs-websocket](https://github.com/Palakis/obs-websocket).
## Usage
```
# Clone obs-sync
git clone https://git.cartooncraft.fr/osufrlive/obs-sync.git
cd obs-sync
# Install dependencies and build
npm i
npm run build
# Edit config appropriately (config/default.json or config/user/$USER.json)
# Run
npm run start
```
You can now switch scenes on any of the OBS instances and it will propagate to all the connected OBS instances.
## Server
`obs-sync` also acts as an obs-websocket server, which means you can connect to it with any `obs-websocket` client such as [obs-websocket-js](https://github.com/haganbmj/obs-websocket-js).
Only the SetCurrentScene command and SwitchScenes event are supported.
\ No newline at end of file
/* tslint:disable */
/* eslint-disable */
interface Config {
obsInstances: ObsInstance[];
server: ObsInstance;
stopTimeout: number;
}
interface ObsInstance {
address: string;
port: number;
password: string;
}
\ No newline at end of file
{
"obsInstances": [
{
"address": "127.0.0.1",
"port": 4445,
"password": "test"
},
{
"address": "127.0.0.1",
"port": 4446,
"password": "test"
}
],
"server": {
"address": "127.0.0.1",
"port": 4444,
"password": "test"
},
"stopTimeout": 5
}
\ No newline at end of file
This diff is collapsed.
{
"name": "obs-sync",
"description": "Synchronizes scenes between different OBS instances",
"version": "0.1.0",
"private": true,
"main": "build/index.js",
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"lint": "tslint -c tslint.json src/**/*.ts",
"start": "node build/index.js"
},
"devDependencies": {
"@types/node": "^12.0.3",
"@types/ws": "^6.0.1",
"ts-node": "^8.2.0",
"ts-node-dev": "^1.0.0-pre.39",
"tslint": "^5.16.0",
"typescript": "^3.4.5"
},
"dependencies": {
"node-config-ts": "^2.2.4",
"obs-websocket-js": "^3.0.0",
"osufrlive-common": "https://git.cartooncraft.fr/osufrlive/common/-/jobs/artifacts/v0.5.1/raw/osufrlive-common-0.5.1.tgz?job=pack",
"ws": "^7.0.0"
}
}
import { config } from "node-config-ts";
import * as OBSWebSocket from "obs-websocket-js";
import { Logger } from "osufrlive-common";
import { promisify } from "util";
import * as Websocket from "ws";
import * as events from "./events";
import { OBSWSClient } from "./OBSWSClient";
export class App {
private static readonly logger = new Logger("App");
public readonly obsInstances: OBSWebSocket[] = [];
public currentScene: string;
private wss: Websocket.Server;
private clients: OBSWSClient[] = [];
public async start() {
for(const configId in config.obsInstances)
this.handleObsInstance(Number(configId), true);
this.wss = new Websocket.Server({
host: config.server.address,
port: config.server.port,
});
this.wss.on("connection", (ws, req) => {
const client = new OBSWSClient(this, ws);
this.clients.push(client);
ws.on("close", (code, reason) => {
const index = this.clients.indexOf(client);
if(index !== 1)
this.clients.splice(index, 1);
});
});
App.logger.info(`WebSocket server listening on ${config.server.address}:${config.server.port}`);
}
public async stop() {
await promisify(this.wss.close.bind(this.wss))();
}
public broadcastEvent(updateType: string, data: object) {
return this.broadcast({ ...data, "update-type": updateType });
}
private broadcast(data: object) {
for(const client of this.clients)
client.ws.send(JSON.stringify(data));
}
private handleObsInstance(instanceId: number, exitOnConnectionFailure: boolean) {
const conf = config.obsInstances[instanceId];
const obs = new OBSWebSocket();
for(const name of Object.keys(events))
obs.on(name as any, (data) => events[name](obs, this, data));
obs.connect({ address: `${conf.address}:${conf.port}`, password: conf.password }).catch((err) => {
if(exitOnConnectionFailure) {
App.logger.error(`Unable to connect to instance #${instanceId}`, { err });
process.exit(1);
}
});
this.obsInstances[instanceId] = obs;
obs.on("ConnectionOpened", () => {
App.logger.info(`OBS Connection #${instanceId} opened!`);
if(this.currentScene)
obs.send("SetCurrentScene", { "scene-name": this.currentScene });
});
obs.on("ConnectionClosed", () => {
App.logger.warn(`OBS Connection #${instanceId} closed!`);
this.obsInstances[instanceId] = null;
setTimeout(() => this.handleObsInstance(instanceId, false), 5000);
});
}
public get primaryOBSInstance() {
return this.obsInstances[0];
}
}
import * as Websocket from "ws";
import { App } from "./App";
import * as authenticatedHandlers from "./requests/authenticated";
import * as unauthenticatedHandlers from "./requests/unauthenticated";
export class OBSWSClient {
public salt: string;
public challenge: string;
public authenticated: boolean = false;
public constructor(public readonly app: App, public readonly ws: Websocket) {
ws.on("message", async (str) => {
const data = JSON.parse(str.toString());
const handlers = this.authenticated ? authenticatedHandlers : unauthenticatedHandlers;
const handler = Object.keys(handlers).find((handlerName) => data["request-type"] === handlerName) ? handlers[data["request-type"]] : null;
if(handler) {
const response = await handler(this, data) || {};
ws.send(JSON.stringify({ "status": "success", ...response, "message-id": data["message-id"] }));
}
});
}
}
import * as OBSWebSocket from "obs-websocket-js";
import { App } from "../App";
export function SwitchScenes(socket: OBSWebSocket, app: App, data: { "scene-name": string, "sources": OBSWebSocket.Source[] }) {
if(app.currentScene !== data["scene-name"]) {
app.currentScene = data["scene-name"];
app.broadcastEvent("SwitchScenes", { "scene-name": data["scene-name"], "sources": data.sources });
for(const obs of app.obsInstances)
obs.send("SetCurrentScene", { "scene-name": data["scene-name"] });
}
}
import { config } from "node-config-ts";
import { Logger } from "osufrlive-common";
import { App } from "./App";
export const app = new App();
process.on("SIGINT", async () => {
Logger.info("Shutting down app...");
setTimeout(() => {
Logger.error(`Stop timeout (${config.stopTimeout} seconds) exceeded - forcing shutdown!`);
process.exit(2);
}, config.stopTimeout * 1000);
try {
await app.stop();
} catch(err) {
Logger.error("An error occured while shutting down!", { err });
process.nextTick(() => process.exit(1));
}
Logger.info("App successfully shut down!");
process.nextTick(() => process.exit(0));
});
Logger.info("Starting app...");
app.start()
.then(() => Logger.info("App started!"))
.catch((err) => {
Logger.error("An error occured while starting!", { err });
process.exit(1);
});
import { OBSWSClient } from "../../OBSWSClient";
export async function SetCurrentScene(client: OBSWSClient, data: { "scene-name": string }) {
try {
await Promise.all(client.app.obsInstances.map((obs) => obs.send("SetCurrentScene", { "scene-name": data["scene-name"] })));
// const eventData = await client.app.primaryOBSInstance.send("GetCurrentScene");
// client.app.broadcastEvent("SwitchScenes", { "scene-name": eventData.name, "sources": eventData.sources });
} catch(err) {
return err;
}
}
import { createHash, randomBytes } from "crypto";
import { config } from "node-config-ts";
import { OBSWSClient } from "../../OBSWSClient";
export function GetAuthRequired(client: OBSWSClient) {
if(!config.server.password) {
client.authenticated = true;
return { authRequired: false };
}
client.salt = randomBytes(32).toString("base64");
client.challenge = randomBytes(32).toString("base64");
return { authRequired: true, challenge: client.challenge, salt: client.salt };
}
export function Authenticate(client: OBSWSClient, data: { auth: string }) {
if(!config.server.password || !client.salt || !client.challenge || client.authenticated)
return { status: "error" };
const secret = createHash("sha256").update(config.server.password + client.salt).digest("base64");
const authResponse = createHash("sha256").update(secret + client.challenge).digest("base64");
if(data.auth === authResponse) {
client.authenticated = true;
return;
} else
return { status: "error" };
}
const OBSWebSocket = require("obs-websocket-js");
(async () => {
try {
const obs = new OBSWebSocket();
obs.on("SwitchScenes", (data) => console.log("SwitchScenes OK", data["scene-name"]));
console.log("connecting");
await obs.connect({ address: "localhost:4444", "password": "test" });
console.log("connected");
await obs.send("SetCurrentScene", { "scene-name": "Scene 2" });
} catch(err) {
console.error("err", err);
}
})();
{
"compilerOptions": {
"outDir": "build",
"sourceMap": true,
"target": "es2015",
"moduleResolution": "node",
"module": "commonjs",
"lib": [
"es2018"
],
"noUnusedLocals": true
},
"files": [
"src/index.ts"
],
"include": [
"./config/**/*"
]
}
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"curly": false,
"max-line-length": false,
"member-ordering": [true, {
"order": [
"public-static-field",
"protected-static-field",
"private-static-field",
"public-instance-field",
"protected-instance-field",
"private-instance-field",
"constructor",
"instance-method",
"static-method"
]
}],
"whitespace": [true,
"check-decl", "check-operator", "check-module", "check-separator", "check-rest-spread",
"check-type", "check-typecast", "check-type-operator", "check-preblock"],
"object-literal-sort-keys": false,
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"],
"interface-name": false
},
"rulesDirectory": []
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment