From 3efa1beb4dad991681dd07ae22b8b52175d64721 Mon Sep 17 00:00:00 2001 From: flifloo Date: Tue, 9 Mar 2021 17:18:39 +0100 Subject: [PATCH] Add OBS actions, refactor some code and add configuration panel --- package-lock.json | 94 ++++++++++++++++ package.json | 1 + public/javascripts/settings/config.js | 67 +++++++++++ public/javascripts/settings/decks.js | 38 ++----- public/javascripts/tools/formMaker.js | 63 +++++++++++ routes/settings.js | 6 +- sockets/index.js | 1 + sockets/setTypeConfig.js | 21 ++++ types/Base.js | 16 ++- types/Deck.js | 7 +- types/ExecCommand.js | 3 +- types/Keys.js | 1 + types/OBS.js | 155 ++++++++++++++++++++++++++ types/index.js | 3 +- views/settings/config.pug | 7 ++ views/settings/decks.pug | 9 +- views/settings/settings.pug | 4 +- 17 files changed, 449 insertions(+), 47 deletions(-) create mode 100644 public/javascripts/settings/config.js create mode 100644 public/javascripts/tools/formMaker.js create mode 100644 sockets/setTypeConfig.js create mode 100644 types/OBS.js create mode 100644 views/settings/config.pug diff --git a/package-lock.json b/package-lock.json index 7131bf3..b8657e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "materialize-css": "^1.0.0-rc.2", "morgan": "~1.9.1", "node-sass-middleware": "0.11.0", + "obs-websocket-js": "^4.0.2", "pug": "2.0.0-beta11", "socket.io": "^3.1.2" }, @@ -1308,6 +1309,14 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "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": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1726,6 +1735,38 @@ "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": { "version": "2.3.0", "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", "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": { "version": "3.0.3", "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", "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": { "version": "0.1.2", "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", "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": { "version": "2.3.0", "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", "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": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", diff --git a/package.json b/package.json index 59d6f1b..4e97c3d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "materialize-css": "^1.0.0-rc.2", "morgan": "~1.9.1", "node-sass-middleware": "0.11.0", + "obs-websocket-js": "^4.0.2", "pug": "2.0.0-beta11", "socket.io": "^3.1.2" }, diff --git a/public/javascripts/settings/config.js b/public/javascripts/settings/config.js new file mode 100644 index 0000000..8f3fa50 --- /dev/null +++ b/public/javascripts/settings/config.js @@ -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(); +}); diff --git a/public/javascripts/settings/decks.js b/public/javascripts/settings/decks.js index 2ac9722..3d9ceb6 100644 --- a/public/javascripts/settings/decks.js +++ b/public/javascripts/settings/decks.js @@ -1,3 +1,5 @@ +import { inputFieldMaker } from "/javascripts/tools/formMaker.js" + const socket = io(); const deckSelect = document.getElementById("deck-select"); const deck = document.getElementById("deck"); @@ -217,37 +219,13 @@ function customFields(values) { customs.innerHTML = ""; let t = types.find(v => v.type === type.value); for (const [name, field] of Object.entries(t.fields)) { - let e; - switch (field.type) { - case "text": - e = document.createElement("input"); - e.type = "text"; - break; - case "select": - e = document.createElement("select"); - for (let option of field.options) - e.insertAdjacentHTML("beforeend", ``); - 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", ``); - if (field.helper) - d.insertAdjacentHTML("beforeend", `${field.helper}`); - 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 (values && name in values) + field.value = values[name]; + customs.insertAdjacentElement("beforeend", inputFieldMaker(type.value, field.type, name, field)); if (field.type === "select") { - M.FormSelect.init(e); - e.style.display = "none"; + const sel = customs.querySelector("select"); + M.FormSelect.init(sel); + sel.style.display = "none"; } } M.updateTextFields(); diff --git a/public/javascripts/tools/formMaker.js b/public/javascripts/tools/formMaker.js new file mode 100644 index 0000000..1db1011 --- /dev/null +++ b/public/javascripts/tools/formMaker.js @@ -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]; +} diff --git a/routes/settings.js b/routes/settings.js index 6b43665..b0297d3 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -1,8 +1,10 @@ const express = require("express"); const router = express.Router(); -router.get("/", function(req, res) { - res.render("settings/decks", { title: "Settings"}); +router.get("/", (req, res) => { + res.render("settings/decks", { title: "Decks", path: "/settings"}); +}).get("/config", (req, res) => { + res.render("settings/config", {title:"Configuration", path: "/settings/config"}) }); module.exports = router; diff --git a/sockets/index.js b/sockets/index.js index 02811c7..daf170a 100644 --- a/sockets/index.js +++ b/sockets/index.js @@ -6,6 +6,7 @@ module.exports = socket => { socket.on("setSlot", require("./setSlot")(socket)); socket.on("addDeck", require("./addDeck")(socket)); socket.on("deleteDeck", require("./deleteDeck")(socket)); + socket.on("setTypeConfig", require("./setTypeConfig")(socket)); socket.on("trigger", require("./trigger")(socket)); socket.on("uploadImage", require("./uploadImage")(socket)); console.log("New connection !"); diff --git a/sockets/setTypeConfig.js b/sockets/setTypeConfig.js new file mode 100644 index 0000000..b661684 --- /dev/null +++ b/sockets/setTypeConfig.js @@ -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); + } +}; diff --git a/types/Base.js b/types/Base.js index 8b6a9f2..852abca 100644 --- a/types/Base.js +++ b/types/Base.js @@ -21,27 +21,33 @@ class Base { if (!(position[0] in db.decks[name].rows)) db.decks[name].rows[position[0]] = {}; 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) { 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]]; - Base.#write(); + Base.write(); return true; } return false; } - static #write() { + static write() { fs.writeFileSync("./db.json", JSON.stringify(db)); } - static staticToJSON(name, type, fields) { + static staticToJSON(name, type, fields, config) { return { "name": name, "type": type, - "fields": fields + "fields": fields, + "configuration": config } } diff --git a/types/Deck.js b/types/Deck.js index 3d8b9a8..7ddfefa 100644 --- a/types/Deck.js +++ b/types/Deck.js @@ -13,9 +13,10 @@ class Deck extends Base { static fields() { return { deck: { - "type": "select", - "options": Object.keys(db.decks), - "name": "Deck" + type: "select", + options: Object.keys(db.decks), + name: "Deck", + required: true, } }; } diff --git a/types/ExecCommand.js b/types/ExecCommand.js index 68a0afe..dc6e453 100644 --- a/types/ExecCommand.js +++ b/types/ExecCommand.js @@ -7,7 +7,8 @@ class ExecCommand extends Base { static fields = { cmd: { type: "text", - name: "Command" + name: "Command", + required: true } }; diff --git a/types/Keys.js b/types/Keys.js index 354f3cc..c864b11 100644 --- a/types/Keys.js +++ b/types/Keys.js @@ -9,6 +9,7 @@ class Keys extends Base { keys: { type: "text", name: "Keys", + required: true, helper: "Key separated by a comma, if combo use +" } }; diff --git a/types/OBS.js b/types/OBS.js new file mode 100644 index 0000000..cfeeaba --- /dev/null +++ b/types/OBS.js @@ -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; diff --git a/types/index.js b/types/index.js index 60bcde2..894a03b 100644 --- a/types/index.js +++ b/types/index.js @@ -3,7 +3,8 @@ const db = require("../db.json"); const types = { "execCommand": require("./ExecCommand"), "deck": require("./Deck"), - "keys": require("./Keys") + "keys": require("./Keys"), + "obs": require("./OBS") }; module.exports.types = types; diff --git a/views/settings/config.pug b/views/settings/config.pug new file mode 100644 index 0000000..0c5838f --- /dev/null +++ b/views/settings/config.pug @@ -0,0 +1,7 @@ +extends ./settings + +block settings + .container + ul.collapsible + + script(src="/javascripts/settings/config.js" type="module") diff --git a/views/settings/decks.pug b/views/settings/decks.pug index 8980afb..e3f8403 100644 --- a/views/settings/decks.pug +++ b/views/settings/decks.pug @@ -1,9 +1,10 @@ extends ./settings block settings - .input-field - select#deck-select - label Deck + .container + .input-field + select#deck-select + label Deck .container#deck @@ -70,4 +71,4 @@ block settings button(data-target="modalAdd")#clearAdd.modal-close.waves-effect.waves-grey.btn-flat Cancel a#add.waves-effect.waves-green.btn-flat Save - script(src="/javascripts/settings/decks.js") + script(src="/javascripts/settings/decks.js" type="module") diff --git a/views/settings/settings.pug b/views/settings/settings.pug index 3d84d22..e94e01b 100644 --- a/views/settings/settings.pug +++ b/views/settings/settings.pug @@ -5,6 +5,8 @@ block content .nav-wrapper a.brand-logo.right(href="/") OpenDeck ul#nav-mobile.left.hide-on-med-and-down - li + li(class=path==="/settings"? "active": "") a(href="/settings") Decks + li(class=path==="/settings/config"? "active": "") + a(href="/settings/config") Configuration block settings