initial proof of concept for livestream studio using atem look
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,5 @@
|
||||
<h3>Livestream Studio Configuration</h3>
|
||||
<ul>
|
||||
<li>enable Third-party Controllers under Settings->Hardware Control->Allow Incoming Connections</li>
|
||||
<li>allow the connection from Cue View the first time it connects under the same settings menu</li>
|
||||
</ul>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<header>
|
||||
<h1><%= listName %></h1>
|
||||
</header>
|
||||
|
||||
<% if (data.inputCount) { %>
|
||||
<div class="inputs-container show-small">
|
||||
<h3 class="lss-gray">Preview/Program</h3>
|
||||
<div class="source-wrapper">
|
||||
<% data.inputs.forEach((input)=>{ %>
|
||||
<div id="input-<%= input.number %>" class="lss-input">
|
||||
<div class="source-label"><%= input.name %></div>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="float-left" style="margin-right: 30px;">
|
||||
<div class="program-container hide-small">
|
||||
<h3 class="lss-gray">Program</h3>
|
||||
<div class="source-wrapper">
|
||||
<% data.inputs.forEach((input)=>{ %>
|
||||
<div id="program-input-<%= input.number %>" class="lss-input">
|
||||
<div class="source-label"><%= input.name %></div>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview hide-small">
|
||||
<h3 class="lss-gray">Preview</h3>
|
||||
<div class="source-wrapper">
|
||||
<% data.inputs.forEach((input)=>{ %>
|
||||
<div id="preview-input-<%= input.number %>" class="lss-input">
|
||||
<div class="source-label"><%= input.name %></div>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="transition-container float-left">
|
||||
<div class="transition-settings">
|
||||
<!-- <div class="next-transition">
|
||||
<h3>Next Transition</h3>
|
||||
<div class="source-wrapper" style="width: 260px;">
|
||||
<div class="lss-input clear"></div>
|
||||
<div id="key-1-onair" class="lss-input">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="key-2-onair" class="lss-input">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="key-3-onair" class="lss-input">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="key-4-onair" class="lss-input">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="transition-style">
|
||||
<h3>Transition Style</h3>
|
||||
<div class="source-wrapper" style="width: 260px;">
|
||||
<div id="cut" class="lss-input">
|
||||
<div class="source-label">CUT</div>
|
||||
</div>
|
||||
<div id="auto" class="lss-input">
|
||||
<div class="source-label">AUTO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tbar-container">
|
||||
<div class="tbar-bg"></div>
|
||||
<div id="tbar-handle-div" class="tbar-handle"></div>
|
||||
<div id="tbar-div" class="tbar-div" ></div>
|
||||
</div>
|
||||
<div class="fade-to-black-container">
|
||||
<div class="fade-to-black">
|
||||
<h3 class="lss-gray">Fade to Black</h3>
|
||||
<div class="source-wrapper">
|
||||
<div id="ftb" class="lss-input">
|
||||
<div class="source-label">FTB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||