diff --git a/plugins/livestream-studio/icon.png b/plugins/livestream-studio/icon.png
new file mode 100644
index 0000000..aa91358
Binary files /dev/null and b/plugins/livestream-studio/icon.png differ
diff --git a/plugins/livestream-studio/img/button_green.png b/plugins/livestream-studio/img/button_green.png
new file mode 100644
index 0000000..2bf0f70
Binary files /dev/null and b/plugins/livestream-studio/img/button_green.png differ
diff --git a/plugins/livestream-studio/img/button_off.png b/plugins/livestream-studio/img/button_off.png
new file mode 100644
index 0000000..078ebab
Binary files /dev/null and b/plugins/livestream-studio/img/button_off.png differ
diff --git a/plugins/livestream-studio/img/button_red.png b/plugins/livestream-studio/img/button_red.png
new file mode 100644
index 0000000..e215389
Binary files /dev/null and b/plugins/livestream-studio/img/button_red.png differ
diff --git a/plugins/livestream-studio/img/button_white.png b/plugins/livestream-studio/img/button_white.png
new file mode 100644
index 0000000..71ac9b5
Binary files /dev/null and b/plugins/livestream-studio/img/button_white.png differ
diff --git a/plugins/livestream-studio/img/button_yellow.png b/plugins/livestream-studio/img/button_yellow.png
new file mode 100644
index 0000000..4c34feb
Binary files /dev/null and b/plugins/livestream-studio/img/button_yellow.png differ
diff --git a/plugins/livestream-studio/img/tbar_bg.png b/plugins/livestream-studio/img/tbar_bg.png
new file mode 100644
index 0000000..a38efdf
Binary files /dev/null and b/plugins/livestream-studio/img/tbar_bg.png differ
diff --git a/plugins/livestream-studio/img/tbar_handle.png b/plugins/livestream-studio/img/tbar_handle.png
new file mode 100644
index 0000000..b377159
Binary files /dev/null and b/plugins/livestream-studio/img/tbar_handle.png differ
diff --git a/plugins/livestream-studio/info.html b/plugins/livestream-studio/info.html
new file mode 100644
index 0000000..683805d
--- /dev/null
+++ b/plugins/livestream-studio/info.html
@@ -0,0 +1,5 @@
+
Livestream Studio Configuration
+
+ - enable Third-party Controllers under Settings->Hardware Control->Allow Incoming Connections
+ - allow the connection from Cue View the first time it connects under the same settings menu
+
diff --git a/plugins/livestream-studio/main.js b/plugins/livestream-studio/main.js
new file mode 100644
index 0000000..fa6eb1a
--- /dev/null
+++ b/plugins/livestream-studio/main.js
@@ -0,0 +1,147 @@
+exports.config = {
+ defaultName: 'Livestream Studio',
+ connectionType: 'TCPsocket',
+ remotePort: 9923,
+ mayChangePort: false,
+ searchOptions: {
+ type: 'TCPport',
+ searchBuffer: Buffer.from(''),
+ testPort: 9923,
+ validateResponse(msg, info) {
+ return msg.toString().startsWith('ILCC:');
+ },
+ },
+};
+
+exports.ready = function ready(_device) {
+ console.log('livestream studio ready');
+ const device = _device;
+ device.draw();
+
+ setInterval(() => {
+ device.update('inputs', device.data);
+ device.update('fadeToBlack', device.data);
+ }, 1000);
+};
+
+exports.update = function update(device, _document, updateType, data) {
+ const document = _document;
+ if (updateType === 'programInput' || updateType === 'previewInput' || updateType === 'inputs') {
+ device.data.inputs.forEach((input) => {
+ if (device.data.program === input.number) {
+ document.getElementById(`program-input-${input.number}`).classList.add('lss-red');
+ document.getElementById(`input-${input.number}`).classList.add('lss-red');
+ } else {
+ document.getElementById(`program-input-${input.number}`).classList.remove('lss-red');
+ document.getElementById(`input-${input.number}`).classList.remove('lss-red');
+ }
+
+ if (device.data.preview === input.number) {
+ document.getElementById(`preview-input-${input.number}`).classList.add('lss-green');
+ document.getElementById(`input-${input.number}`).classList.add('lss-green');
+ } else {
+ document.getElementById(`preview-input-${input.number}`).classList.remove('lss-green');
+ document.getElementById(`input-${input.number}`).classList.remove('lss-green');
+ }
+ });
+ } else if (updateType === 'fadeToBlack') {
+ const ftbId = 'ftb';
+
+ if (device.data.fadeToBlack) {
+ if (document.getElementById(ftbId).classList.contains('lss-red')) {
+ document.getElementById(ftbId).classList.remove('lss-red');
+ } else {
+ document.getElementById(ftbId).classList.add('lss-red');
+ }
+ } else {
+ document.getElementById(ftbId).classList.remove('lss-red');
+ }
+ } else if (updateType === 'tbar') {
+ const tbarId = `tbar-div`;
+ const tbarHandleId = `tbar-handle-div`;
+ if (document.getElementById(tbarId)) {
+ document.getElementById(tbarId).style.height = `${device.data.tBar.percent}%`;
+ }
+ if (document.getElementById(tbarHandleId)) {
+ document.getElementById(tbarHandleId).style.bottom = `${device.data.tBar.percent}%`;
+ }
+
+ if (device.data.tBar.status === 'Automatic') {
+ document.getElementById(`auto`).classList.add('lss-red');
+ } else {
+ document.getElementById(`auto`).classList.remove('lss-red');
+ }
+ } else if (updateType === 'cut') {
+ document.getElementById('cut').classList.add('lss-red');
+ setTimeout(() => {
+ document.getElementById('cut').classList.remove('lss-red');
+ }, 250);
+ }
+};
+
+exports.data = function data(_device, msg) {
+ const device = _device;
+
+ const packets = msg.toString().split('\n');
+
+ packets.forEach((packet) => {
+ const packetParts = packet.split(':');
+ const packetType = packetParts[0];
+ if (packetType === 'ILC') {
+ if (device.data.inputs === undefined) {
+ device.data.inputs = [];
+ }
+ device.data.inputs[packetParts[1]] = {
+ number: Number.parseInt(packetParts[1], 10) + 1,
+ name: packetParts[2].replaceAll('"', ''),
+ audio: {
+ level: parseFloat(packetParts[3]) / 1000,
+ gain: parseFloat(packetParts[4]) / 1000,
+ mute: packetParts[5] === '1',
+ solo: packetParts[6] === '1',
+ programLock: packetParts[7] === '1',
+ },
+ type: packetParts[8],
+ };
+ device.draw();
+ device.update('inputs', device.data);
+ } else if (packetType === 'ILCC') {
+ if (device.data.inputCount !== undefined) {
+ device.data.inputs = [];
+ }
+ device.data.inputCount = Number.parseInt(packetParts[1], 10);
+ } else if (packetType === 'PmIS') {
+ device.data.program = Number.parseInt(packetParts[1], 10) + 1;
+ device.update('programInput', device.data);
+ } else if (packetType === 'PwIS') {
+ device.data.preview = Number.parseInt(packetParts[1], 10) + 1;
+ device.update('previewInput', device.data);
+ } else if (packetType === 'Cut') {
+ [device.data.preview, device.data.program] = [device.data.program, device.data.preview];
+ device.update('cut');
+ device.update('program', device.data);
+ } else if (packetType === 'FOut') {
+ device.data.fadeToBlack = true;
+ device.update('fadeToBlack', device.data);
+ } else if (packetType === 'FIn') {
+ device.data.fadeToBlack = false;
+ device.update('fadeToBlack', device.data);
+ } else if (packetType === 'TrASp' || packetType === 'TrMSp') {
+ if (device.data.tBar === undefined) {
+ device.data.tBar = {};
+ }
+ device.data.tBar.status = packetType === 'TrASp' ? 'Automatic' : 'Manual';
+ device.data.tBar.percent = Number.parseInt(packetParts[1], 10) / 10;
+ if (device.data.tBar.percent === 0) {
+ device.data.tBar.status = 'Stop';
+ }
+ device.update('tbar', device.data);
+ } else if (packetType === 'TrAStart' || packetType === 'TrAStop') {
+ device.data.tBar.status = packetType.slice(3);
+ if (packetType === 'TrAStop') {
+ device.data.tBar.percent = 0;
+ }
+ device.update('tbar', device.data);
+ }
+ });
+};
diff --git a/plugins/livestream-studio/styles.css b/plugins/livestream-studio/styles.css
new file mode 100644
index 0000000..c242184
--- /dev/null
+++ b/plugins/livestream-studio/styles.css
@@ -0,0 +1,159 @@
+body {
+ background-color: #282828;
+}
+h1 {
+ font-family: 'Open Sans', sans-serif;
+}
+h3 {
+ color: #6d6d6d !important;
+ margin: 30px 0px 10px 9px;
+ font-size: 0.9em;
+ font-weight: 400;
+ font-family: 'Open Sans', sans-serif;
+}
+
+.lss-input {
+ width: 52px;
+ height: 52px;
+ background-image: url('img/button_white.png');
+}
+
+.source-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ background-color: #1f1f1f;
+ border: #1a1a1a 2px solid;
+ border-radius: 8px;
+ max-width: 416px;
+ padding: 12px;
+}
+
+.lss-red {
+ background-image: url('img/button_red.png');
+ box-shadow: 0px 0px 10px 1px #ff0000;
+ z-index: 10000;
+}
+
+.lss-green {
+ background-image: url('img/button_green.png');
+ box-shadow: 0px 0px 10px 1px #06c300;
+ z-index: 10000;
+}
+
+.lss-yellow {
+ background-image: url('img/button_yellow.png');
+ box-shadow: 0px 0px 10px 1px #c7ca00;
+ z-index: 10000;
+}
+
+.lss-disabled {
+ background-image: url('img/button_off.png');
+ color: #3a3a3a;
+}
+
+.lss-gray {
+ color: #575757;
+}
+
+.source-label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.transition-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.transition-settings {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ margin-right: 30px;
+}
+
+.tbar-container {
+ display: flex;
+ flex-direction: column-reverse;
+ background-color: #1f1f1f;
+ border: 2px solid;
+ border-color: #1a1a1a;
+ border-radius: 6px;
+ width: 88px;
+ height: 429px;
+ position: relative;
+ margin-top: 58px;
+ margin-right: 60px;
+}
+
+.tbar-bg {
+ position: absolute;
+ background-image: url('img/tbar_bg.png');
+ width: 88px;
+ height: 429px;
+}
+.tbar-handle {
+ position: absolute;
+ background-image: url('img/tbar_handle.png');
+ width: 126px;
+ height: 50px;
+ left: -3px;
+}
+.tbar-div {
+ width: 100%;
+ background-color: #7aff58;
+ overflow: hidden;
+}
+
+.fade-to-black-container {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+}
+
+.fade-to-black .source-wrapper {
+ display: flex;
+ width: 100%;
+ padding: 5px;
+ justify-content: center;
+}
+
+.fade-to-black h3 {
+ text-align: center;
+}
+
+.clear {
+ background: transparent;
+ border-color: transparent;
+ background-color: transparent;
+}
+
+.hide {
+ display: none;
+}
+
+.no-wrap {
+ flex-wrap: nowrap;
+}
+
+.show-small {
+ display: none;
+}
+
+@media screen and (min-width: 0px) and (max-width: 480px) {
+ .hide-small {
+ display: none;
+ }
+ .show-small {
+ display: block;
+ }
+}
+
+.float-left {
+ float: left;
+}
diff --git a/plugins/livestream-studio/template.ejs b/plugins/livestream-studio/template.ejs
new file mode 100644
index 0000000..08a70bf
--- /dev/null
+++ b/plugins/livestream-studio/template.ejs
@@ -0,0 +1,95 @@
+
+
+<% if (data.inputCount) { %>
+
+
+
+
Program
+
+ <% data.inputs.forEach((input)=>{ %>
+
+ <% }) %>
+
+
+
+
Preview
+
+ <% data.inputs.forEach((input)=>{ %>
+
+ <% }) %>
+
+
+
+
+<% } %>