Add OBS actions, refactor some code and add configuration panel

This commit is contained in:
Ethanell 2021-03-09 17:18:39 +01:00
parent d4029c8ef0
commit 3efa1beb4d
17 changed files with 449 additions and 47 deletions

94
package-lock.json generated
View file

@ -15,6 +15,7 @@
"materialize-css": "^1.0.0-rc.2", "materialize-css": "^1.0.0-rc.2",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"node-sass-middleware": "0.11.0", "node-sass-middleware": "0.11.0",
"obs-websocket-js": "^4.0.2",
"pug": "2.0.0-beta11", "pug": "2.0.0-beta11",
"socket.io": "^3.1.2" "socket.io": "^3.1.2"
}, },
@ -1308,6 +1309,14 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
}, },
"node_modules/isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/isstream": { "node_modules/isstream": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -1726,6 +1735,38 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/obs-websocket-js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-4.0.2.tgz",
"integrity": "sha512-e+tGp0DQNXSnitc5lfuzEC1dG3VdWy7VLePUVb6aq7bC33Sgjoi695k0eOg4UPTIQI71Z9aJ+yn3nvBX9dQkEg==",
"dependencies": {
"debug": "^4.1.0",
"isomorphic-ws": "^4.0.1",
"sha.js": "^2.4.9",
"ws": "^7.2.0"
}
},
"node_modules/obs-websocket-js/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/obs-websocket-js/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@ -2332,6 +2373,18 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
}, },
"node_modules/sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dependencies": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
},
"bin": {
"sha.js": "bin.js"
}
},
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
@ -4042,6 +4095,12 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
}, },
"isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"requires": {}
},
"isstream": { "isstream": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -4377,6 +4436,32 @@
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
}, },
"obs-websocket-js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-4.0.2.tgz",
"integrity": "sha512-e+tGp0DQNXSnitc5lfuzEC1dG3VdWy7VLePUVb6aq7bC33Sgjoi695k0eOg4UPTIQI71Z9aJ+yn3nvBX9dQkEg==",
"requires": {
"debug": "^4.1.0",
"isomorphic-ws": "^4.0.1",
"sha.js": "^2.4.9",
"ws": "^7.2.0"
},
"dependencies": {
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"on-finished": { "on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@ -4877,6 +4962,15 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
}, },
"sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"signal-exit": { "signal-exit": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",

View file

@ -13,6 +13,7 @@
"materialize-css": "^1.0.0-rc.2", "materialize-css": "^1.0.0-rc.2",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"node-sass-middleware": "0.11.0", "node-sass-middleware": "0.11.0",
"obs-websocket-js": "^4.0.2",
"pug": "2.0.0-beta11", "pug": "2.0.0-beta11",
"socket.io": "^3.1.2" "socket.io": "^3.1.2"
}, },

View file

@ -0,0 +1,67 @@
import { formMaker, inputFieldMaker } from "/javascripts/tools/formMaker.js"
const socket = io();
const collapsible = document.querySelector(".collapsible");
socket.on("connected", () => {
console.log("Connected !");
socket.emit("getType");
});
socket.on("getType", data => {
collapsible.innerHTML = "";
for (const type of data)
if (type.configuration) {
const [formRow, form] = formMaker();
for (const [name, conf] of Object.entries(type.configuration)) {
let secondRow = document.createElement("div");
secondRow.classList.add("row");
form.insertAdjacentElement("beforeend", secondRow);
secondRow.insertAdjacentElement("beforeend", inputFieldMaker(type.type, conf.type, name, conf));
}
const save = document.createElement("a");
save.classList.add("waves-effect", "waves-light", "btn", "blue");
save.innerText = "Save";
save.addEventListener("click", ev => {
ev.stopPropagation();
let data = {};
for (const e of new FormData(form))
data[e[0]] = e[1];
socket.emit("setTypeConfig", {type: type.type, configuration: data})
});
formRow.insertAdjacentElement("beforeend", save);
addCollapsible(type.name, formRow);
}
M.updateTextFields();
});
socket.on("setTypeConfig", data => {
if (data && data.configuration) {
for (const [name, value] of Object.entries(data.configuration))
document.getElementById(data.type + "-" + name).value = value;
M.updateTextFields();
}
});
function addCollapsible(title, content) {
const li = document.createElement("li");
const divTitle = document.createElement("div");
divTitle.classList.add("collapsible-header");
divTitle.innerText = title;
li.insertAdjacentElement("beforeend", divTitle);
const divContent = document.createElement("div");
divContent.classList.add("collapsible-body");
if (content)
if (typeof content !== "string")
divContent.insertAdjacentElement("beforeend", content);
else
divContent.insertAdjacentHTML("beforeend", content);
li.insertAdjacentElement("beforeend", divContent);
collapsible.insertAdjacentElement("beforeend", li);
return li;
}
document.addEventListener("DOMContentLoaded", () => {
M.AutoInit();
});

View file

@ -1,3 +1,5 @@
import { inputFieldMaker } from "/javascripts/tools/formMaker.js"
const socket = io(); const socket = io();
const deckSelect = document.getElementById("deck-select"); const deckSelect = document.getElementById("deck-select");
const deck = document.getElementById("deck"); const deck = document.getElementById("deck");
@ -217,37 +219,13 @@ function customFields(values) {
customs.innerHTML = ""; customs.innerHTML = "";
let t = types.find(v => v.type === type.value); let t = types.find(v => v.type === type.value);
for (const [name, field] of Object.entries(t.fields)) { for (const [name, field] of Object.entries(t.fields)) {
let e; if (values && name in values)
switch (field.type) { field.value = values[name];
case "text": customs.insertAdjacentElement("beforeend", inputFieldMaker(type.value, field.type, name, field));
e = document.createElement("input");
e.type = "text";
break;
case "select":
e = document.createElement("select");
for (let option of field.options)
e.insertAdjacentHTML("beforeend", `<option value="${option}">${option}</option>`);
break;
}
e.name = name;
e.id = name;
e.required = true;
e.classList.add("validate");
let d = document.createElement("div");
d.classList.add("input-field");
d.insertAdjacentElement("beforeend", e);
d.insertAdjacentHTML("beforeend", `<label for="${name}">${field.name}</label>`);
if (field.helper)
d.insertAdjacentHTML("beforeend", `<span class="helper-text">${field.helper}</span>`);
customs.insertAdjacentElement("beforeend", d);
if (values && name in values) {
e.value = values[name];
if (field.type === "select")
e.querySelector(`option[value=${values[name]}]`).selected = true;
}
if (field.type === "select") { if (field.type === "select") {
M.FormSelect.init(e); const sel = customs.querySelector("select");
e.style.display = "none"; M.FormSelect.init(sel);
sel.style.display = "none";
} }
} }
M.updateTextFields(); M.updateTextFields();

View file

@ -0,0 +1,63 @@
export function inputFieldMaker(Type, type, name, options) {
const inputField = document.createElement("div");
inputField.classList.add("input-field", "col", "s12");
let input;
switch (type) {
case "text":
input = document.createElement("input");
input.type = "text";
break;
case "select":
input = document.createElement("select");
for (const option of options.options) {
const opt = document.createElement("option");
opt.value = opt.innerText = option;
input.insertAdjacentElement("beforeend", opt);
}
break;
case "number":
input = document.createElement("input");
input.type = "number";
break;
case "password":
input = document.createElement("input");
input.type = "password";
break;
}
input.id = Type + "-" + name;
input.name = name;
input.classList.add("validate");
if (options.required)
input.required = true;
if (options.value) {
input.value = options.value;
if (type === "select")
input.querySelectorAll(`option[value=${options.value}]`).forEach(o => o.selected = true);
}
inputField.insertAdjacentElement("beforeend", input);
const label = document.createElement("label");
label.for = input.id;
label.innerText = name;
inputField.insertAdjacentElement("beforeend", label);
if (options.helper) {
const helper = document.createElement("span");
helper.classList.add("helper-text");
helper.innerText = options.helper;
inputField.insertAdjacentElement("beforeend", helper);
}
return inputField;
}
export function formMaker() {
const row = document.createElement("div");
row.classList.add("row");
const form = document.createElement("form");
form.classList.add("col", "s12");
row.insertAdjacentElement("beforeend", form);
return [row, form];
}

View file

@ -1,8 +1,10 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
router.get("/", function(req, res) { router.get("/", (req, res) => {
res.render("settings/decks", { title: "Settings"}); res.render("settings/decks", { title: "Decks", path: "/settings"});
}).get("/config", (req, res) => {
res.render("settings/config", {title:"Configuration", path: "/settings/config"})
}); });
module.exports = router; module.exports = router;

View file

@ -6,6 +6,7 @@ module.exports = socket => {
socket.on("setSlot", require("./setSlot")(socket)); socket.on("setSlot", require("./setSlot")(socket));
socket.on("addDeck", require("./addDeck")(socket)); socket.on("addDeck", require("./addDeck")(socket));
socket.on("deleteDeck", require("./deleteDeck")(socket)); socket.on("deleteDeck", require("./deleteDeck")(socket));
socket.on("setTypeConfig", require("./setTypeConfig")(socket));
socket.on("trigger", require("./trigger")(socket)); socket.on("trigger", require("./trigger")(socket));
socket.on("uploadImage", require("./uploadImage")(socket)); socket.on("uploadImage", require("./uploadImage")(socket));
console.log("New connection !"); console.log("New connection !");

21
sockets/setTypeConfig.js Normal file
View file

@ -0,0 +1,21 @@
const { types } = require("../types");
module.exports = socket => {
return data => {
if (data.type && data.configuration) {
try {
const type = types[data.type], config = type.config();
for (const [name, value] of Object.entries(data.configuration))
config[name] = value;
type.saveConfig(config);
} catch (err) {
console.error(err);
socket.emit("setTypeConfig", {error: err.code});
return;
}
}
socket.emit("setTypeConfig", data);
socket.broadcast.emit("setTypeConfig", data);
}
};

View file

@ -21,27 +21,33 @@ class Base {
if (!(position[0] in db.decks[name].rows)) if (!(position[0] in db.decks[name].rows))
db.decks[name].rows[position[0]] = {}; db.decks[name].rows[position[0]] = {};
db.decks[name].rows[position[0]][position[1]] = this.toJSON(); db.decks[name].rows[position[0]][position[1]] = this.toJSON();
Base.#write() Base.write()
}
static saveConfig(type, configuration) {
db.types[type] = configuration;
Base.write();
} }
remove(name, position) { remove(name, position) {
if (position[0] in db.decks[name].rows && position[1] in db.decks[name].rows[position[0]]) { if (position[0] in db.decks[name].rows && position[1] in db.decks[name].rows[position[0]]) {
delete db.decks[name].rows[position[0]][position[1]]; delete db.decks[name].rows[position[0]][position[1]];
Base.#write(); Base.write();
return true; return true;
} }
return false; return false;
} }
static #write() { static write() {
fs.writeFileSync("./db.json", JSON.stringify(db)); fs.writeFileSync("./db.json", JSON.stringify(db));
} }
static staticToJSON(name, type, fields) { static staticToJSON(name, type, fields, config) {
return { return {
"name": name, "name": name,
"type": type, "type": type,
"fields": fields "fields": fields,
"configuration": config
} }
} }

View file

@ -13,9 +13,10 @@ class Deck extends Base {
static fields() { static fields() {
return { return {
deck: { deck: {
"type": "select", type: "select",
"options": Object.keys(db.decks), options: Object.keys(db.decks),
"name": "Deck" name: "Deck",
required: true,
} }
}; };
} }

View file

@ -7,7 +7,8 @@ class ExecCommand extends Base {
static fields = { static fields = {
cmd: { cmd: {
type: "text", type: "text",
name: "Command" name: "Command",
required: true
} }
}; };

View file

@ -9,6 +9,7 @@ class Keys extends Base {
keys: { keys: {
type: "text", type: "text",
name: "Keys", name: "Keys",
required: true,
helper: "Key separated by a comma, if combo use +" helper: "Key separated by a comma, if combo use +"
} }
}; };

155
types/OBS.js Normal file
View file

@ -0,0 +1,155 @@
const Base = require("./Base");
const OBSWebSocket = require('obs-websocket-js');
const db = require("../db.json");
class OBS extends Base {
static name = "OBS";
static type = "obs";
static fields = {
cmd: {
type: "select",
options: ["startStreaming", "stopStreaming", "toggleStreaming", "startRecording", "stopRecording", "toggleRecording"],
name: "Action",
required: true
}
};
static obs = new OBSWebSocket();
constructor(text, image = null, options = null) {
super(text, image, options);
}
static config() {
return {
host: {
type: "text",
name: "Address",
required: true,
value: db.types[OBS.type].host,
},
port: {
type: "number",
name: "Port",
required: true,
value: db.types[OBS.type].port,
},
password: {
type: "password",
name: "password",
value: db.types[OBS.type].password
}
}
};
/**
* @override
*/
static saveConfig(configuration) {
super.saveConfig(OBS.type, configuration)
}
/**
* @override
*/
static staticToJSON() {
return super.staticToJSON(OBS.name, OBS.type, OBS.fields, OBS.config());
}
/**
* @override
*/
toJSON() {
return super.toJSON(OBS.type)
}
static async connect() {
const config = OBS.config();
try {
await OBS.obs.connect({
address: config.host.value + ":" + config.port.value,
password: config.password.value
});
return true;
} catch (err) {
console.error("Fail to connect !");
console.error(err);
return false;
}
}
/**
* @override
*/
async trigger(socket) {
try {
switch (this.options.cmd) {
case "startStreaming":
await OBS.obs.send("StartStreaming");
break;
case "stopStreaming":
await OBS.obs.send("StopStreaming");
break;
case "toggleStreaming":
try {
await OBS.obs.send("StartStreaming");
} catch (err) {
if (err.error === "streaming already active")
await OBS.obs.send("StopStreaming");
else
throw err;
}
break;
case "startRecording":
await OBS.obs.send("StartRecording");
break;
case "stopRecording":
await OBS.obs.send("StopRecording");
break;
case "toggleRecording":
try {
await OBS.obs.send("StartRecording");
} catch (err) {
if (err.error === "recording already active")
await OBS.obs.send("StopRecording");
else
throw err;
}
break;
}
} catch (err) {
switch (err.error) {
case "There is no Socket connection available.":
if (!await OBS.connect())
socket.emit("trigger", {error: "failToConnectOBS"});
else
this.trigger(socket);
break;
case "streaming already active":
console.error("Streaming already active");
break;
case "streaming not active":
console.error("Streaming not active");
break;
case "recording already active":
console.error("Recording already active");
break;
case "recording not active":
console.error("Recording not active");
break;
default:
throw err;
}
}
}
}
if (!(OBS.type in db.types)) {
db.types[OBS.type] = {};
OBS.write();
}
OBS.obs.on("error", err => {
console.error("socket error:", err);
});
module.exports = OBS;

View file

@ -3,7 +3,8 @@ const db = require("../db.json");
const types = { const types = {
"execCommand": require("./ExecCommand"), "execCommand": require("./ExecCommand"),
"deck": require("./Deck"), "deck": require("./Deck"),
"keys": require("./Keys") "keys": require("./Keys"),
"obs": require("./OBS")
}; };
module.exports.types = types; module.exports.types = types;

View file

@ -0,0 +1,7 @@
extends ./settings
block settings
.container
ul.collapsible
script(src="/javascripts/settings/config.js" type="module")

View file

@ -1,6 +1,7 @@
extends ./settings extends ./settings
block settings block settings
.container
.input-field .input-field
select#deck-select select#deck-select
label Deck label Deck
@ -70,4 +71,4 @@ block settings
button(data-target="modalAdd")#clearAdd.modal-close.waves-effect.waves-grey.btn-flat Cancel button(data-target="modalAdd")#clearAdd.modal-close.waves-effect.waves-grey.btn-flat Cancel
a#add.waves-effect.waves-green.btn-flat Save a#add.waves-effect.waves-green.btn-flat Save
script(src="/javascripts/settings/decks.js") script(src="/javascripts/settings/decks.js" type="module")

View file

@ -5,6 +5,8 @@ block content
.nav-wrapper .nav-wrapper
a.brand-logo.right(href="/") OpenDeck a.brand-logo.right(href="/") OpenDeck
ul#nav-mobile.left.hide-on-med-and-down ul#nav-mobile.left.hide-on-med-and-down
li li(class=path==="/settings"? "active": "")
a(href="/settings") Decks a(href="/settings") Decks
li(class=path==="/settings/config"? "active": "")
a(href="/settings/config") Configuration
block settings block settings