Merge pull request #78 from stagehacks/plugin-atem
This has passed tests and has been cleaned up, send it!
@@ -30,6 +30,7 @@
|
||||
"prettier": "^2.8.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"atem-connection": "^3.1.3",
|
||||
"bonjour": "^3.5.0",
|
||||
"electron-updater": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 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,11 @@
|
||||
<h3>ATEM Configuration</h3>
|
||||
<ul>
|
||||
<li>Set the IP Address, Subnet Mask, and Gateway of the ATEM using the <em>ATEM Setup utility</em></li>
|
||||
<li>The ATEM must be connected to a computer using a USB cable to change its Network settings</li>
|
||||
</ul>
|
||||
|
||||
<h3>ATEM Software Download</h3>
|
||||
<button href="https://www.blackmagicdesign.com/support/family/atem-live-production-switchers">
|
||||
Blackmagic Support
|
||||
</button>
|
||||
(Look for <em>ATEM Switchers Update</em>)
|
||||
@@ -0,0 +1,240 @@
|
||||
const { TransitionStyle, TransitionSelection } = require('atem-connection/dist/enums');
|
||||
|
||||
let timerFrameRate;
|
||||
|
||||
exports.config = {
|
||||
defaultName: 'ATEM',
|
||||
connectionType: 'atem',
|
||||
defaultPort: 9910,
|
||||
mayChangePort: false,
|
||||
searchOptions: {
|
||||
type: 'UDPScan',
|
||||
searchBuffer: Buffer.from([
|
||||
0x10, 0x14, 0x53, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
]),
|
||||
listenPort: 0,
|
||||
devicePort: 9910,
|
||||
validateResponse(msg, info) {
|
||||
// This is a tad bit hacky but works from what I can see.
|
||||
return Buffer.compare(msg.slice(0, 4), Buffer.from([16, 20, 83, 171])) === 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
exports.ready = function ready(_device) {
|
||||
console.log('atem ready');
|
||||
const device = _device;
|
||||
device.data = device.connection.state;
|
||||
device.draw();
|
||||
|
||||
// TODO: this is a little hacky but it works? gotta be a way to hook into device.draw() to trigger update
|
||||
setInterval(() => {
|
||||
device.update('inputs', device.data);
|
||||
device.update('fadeToBlack', device.data);
|
||||
device.update(`transitionPosition`, device.data);
|
||||
device.update('downstreamKeyers', device.data);
|
||||
device.update('upstreamKeyers', device.data);
|
||||
device.update('transitionProperties', device.data);
|
||||
device.update('deviceInfo', device.data);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
exports.update = function update(device, _document, updateType, data) {
|
||||
const document = _document;
|
||||
|
||||
if (updateType.includes('transitionPosition')) {
|
||||
for (let i = 0; i < data.video.mixEffects.length; i++) {
|
||||
const mixEffect = data.video.mixEffects[i];
|
||||
const tbarId = `me-${i}-tbar-div`;
|
||||
const tbarHandleId = `me-${i}-tbar-handle-div`;
|
||||
if (document.getElementById(tbarId)) {
|
||||
document.getElementById(tbarId).style.height = `${mixEffect.transitionPosition.handlePosition / 100}%`;
|
||||
}
|
||||
if (document.getElementById(tbarHandleId)) {
|
||||
document.getElementById(tbarHandleId).style.bottom = `${mixEffect.transitionPosition.handlePosition / 100}%`;
|
||||
}
|
||||
document.getElementById(`me-${i}-transition-rate`).textContent = framesToTime(
|
||||
mixEffect.transitionPosition.remainingFrames
|
||||
);
|
||||
|
||||
if (mixEffect.transitionPosition.inTransition) {
|
||||
document.getElementById(`me-${i}-auto`).classList.add('atem-red');
|
||||
} else {
|
||||
document.getElementById(`me-${i}-auto`).classList.remove('atem-red');
|
||||
}
|
||||
}
|
||||
} else if (updateType.includes('downstreamKeyers')) {
|
||||
for (let i = 0; i < data.video.downstreamKeyers.length; i++) {
|
||||
const dsk = data.video.downstreamKeyers[i];
|
||||
if (dsk.isAuto) {
|
||||
document.getElementById(`dsk-${i}-auto`).classList.add('atem-red');
|
||||
} else {
|
||||
document.getElementById(`dsk-${i}-auto`).classList.remove('atem-red');
|
||||
}
|
||||
|
||||
if (dsk.onAir) {
|
||||
document.getElementById(`dsk-${i}-onair`).classList.add('atem-red');
|
||||
} else {
|
||||
document.getElementById(`dsk-${i}-onair`).classList.remove('atem-red');
|
||||
}
|
||||
|
||||
if (dsk.properties.tie) {
|
||||
document.getElementById(`dsk-${i}-tie`).classList.add('atem-yellow');
|
||||
} else {
|
||||
document.getElementById(`dsk-${i}-tie`).classList.remove('atem-yellow');
|
||||
}
|
||||
|
||||
document.getElementById(`dsk-${i}-rate`).textContent = framesToTime(dsk.remainingFrames);
|
||||
}
|
||||
} else if (updateType.includes('fadeToBlack')) {
|
||||
for (let i = 0; i < data.video.mixEffects.length; i++) {
|
||||
const fadeToBlack = data.video.mixEffects[i].fadeToBlack;
|
||||
const ftbRateId = `me-${i}-ftb-rate`;
|
||||
const ftbId = `me-${i}-ftb`;
|
||||
|
||||
document.getElementById(ftbRateId).textContent = framesToTime(fadeToBlack.remainingFrames);
|
||||
|
||||
if (fadeToBlack.isFullyBlack) {
|
||||
if (document.getElementById(ftbId).classList.contains('atem-red')) {
|
||||
document.getElementById(ftbId).classList.remove('atem-red');
|
||||
} else {
|
||||
document.getElementById(ftbId).classList.add('atem-red');
|
||||
}
|
||||
} else if (fadeToBlack.inTransition) {
|
||||
document.getElementById(ftbId).classList.add('atem-red');
|
||||
} else {
|
||||
document.getElementById(ftbId).classList.remove('atem-red');
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
updateType.includes('programInput') ||
|
||||
updateType.includes('previewInput') ||
|
||||
updateType.includes('inputs')
|
||||
) {
|
||||
for (let i = 0; i < data.video.mixEffects.length; i++) {
|
||||
const mixEffect = data.video.mixEffects[0];
|
||||
Object.keys(device.data.inputs)
|
||||
.map((key) => Number(key))
|
||||
.forEach((inputId) => {
|
||||
if (mixEffect.programInput === inputId) {
|
||||
document.getElementById(`me-${i}-program-input-${inputId}`).classList.add('atem-red');
|
||||
document.getElementById(`me-${i}-input-${inputId}`).classList.add('atem-red');
|
||||
} else {
|
||||
document.getElementById(`me-${i}-program-input-${inputId}`).classList.remove('atem-red');
|
||||
document.getElementById(`me-${i}-input-${inputId}`).classList.remove('atem-red');
|
||||
}
|
||||
|
||||
if (mixEffect.previewInput === inputId) {
|
||||
document.getElementById(`me-${i}-preview-input-${inputId}`).classList.add('atem-green');
|
||||
document.getElementById(`me-${i}-input-${inputId}`).classList.add('atem-green');
|
||||
} else {
|
||||
document.getElementById(`me-${i}-preview-input-${inputId}`).classList.remove('atem-green');
|
||||
document.getElementById(`me-${i}-input-${inputId}`).classList.remove('atem-green');
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (updateType.includes('transitionProperties')) {
|
||||
for (let i = 0; i < data.video.mixEffects.length; i++) {
|
||||
const transitionProperties = data.video.mixEffects[i].transitionProperties;
|
||||
Object.keys(TransitionStyle)
|
||||
.filter((key) => !isNaN(Number(key)))
|
||||
.map((key) => Number(key))
|
||||
.forEach((style) => {
|
||||
if (style === transitionProperties.style) {
|
||||
document.getElementById(`me-${i}-transition-style-${style}`).classList.add('atem-yellow');
|
||||
} else {
|
||||
document.getElementById(`me-${i}-transition-style-${style}`).classList.remove('atem-yellow');
|
||||
}
|
||||
if (style === transitionProperties.nextStyle) {
|
||||
document.getElementById(`me-${i}-transition-style-${style}`).classList.add('atem-yellow');
|
||||
} else {
|
||||
document.getElementById(`me-${i}-transition-style-${style}`).classList.remove('atem-yellow');
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(TransitionSelection)
|
||||
.filter((key) => !isNaN(Number(key)))
|
||||
.map((key) => Number(key))
|
||||
.forEach((selection) => {
|
||||
if (transitionProperties.selection.includes(selection)) {
|
||||
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.add('atem-yellow');
|
||||
} else {
|
||||
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.remove('atem-yellow');
|
||||
}
|
||||
|
||||
if (transitionProperties.nextSelection.includes(selection)) {
|
||||
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.add('atem-yellow');
|
||||
} else {
|
||||
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.remove('atem-yellow');
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (updateType.includes('upstreamKeyers')) {
|
||||
for (let i = 0; i < data.video.mixEffects.length; i++) {
|
||||
const upstreamKeyers = data.video.mixEffects[i].upstreamKeyers;
|
||||
upstreamKeyers.forEach((upstreamKeyer) => {
|
||||
if (upstreamKeyer.onAir) {
|
||||
document.getElementById(`me-${i}-key-${upstreamKeyer.upstreamKeyerId + 1}-onair`).classList.add('atem-red');
|
||||
} else {
|
||||
document
|
||||
.getElementById(`me-${i}-key-${upstreamKeyer.upstreamKeyerId + 1}-onair`)
|
||||
.classList.remove('atem-red');
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (updateType.includes('transitionPreview')) {
|
||||
for (let i = 0; i < data.video.mixEffects.length; i++) {
|
||||
const mixEffect = data.video.mixEffects[i];
|
||||
if (mixEffect.transitionPreview) {
|
||||
document.getElementById(`me-${i}-transition-preview`).classList.add('atem-red');
|
||||
} else {
|
||||
document.getElementById(`me-${i}-transition-preview`).classList.remove('atem-red');
|
||||
}
|
||||
}
|
||||
} else if (updateType.includes('deviceInfo')) {
|
||||
// videoModes pulled from https://github.com/nrkno/sofie-atem-connection/blob/master/src/enums/index.ts#L238
|
||||
if ([27, 26, 23, 25, 19, 13, 11, 7, 5].includes(data.settings.videoMode)) {
|
||||
timerFrameRate = 30;
|
||||
} else if ([24, 22, 18, 16, 12, 10, 6, 4].includes(data.settings.videoMode)) {
|
||||
timerFrameRate = 25;
|
||||
} else if ([21, 20, 15, 14, 9, 8].includes(data.settings.videoMode)) {
|
||||
timerFrameRate = 24;
|
||||
} else {
|
||||
timerFrameRate = 30;
|
||||
}
|
||||
|
||||
if (!device.displayName) {
|
||||
this.deviceInfoUpdate(device, 'displayName', data.info.productIdentifier);
|
||||
}
|
||||
} else {
|
||||
console.log('unhandled update');
|
||||
console.log(updateType);
|
||||
console.log(device.data);
|
||||
}
|
||||
};
|
||||
|
||||
exports.data = function data(_device, msg) {
|
||||
const device = _device;
|
||||
this.deviceInfoUpdate(device, 'status', 'ok');
|
||||
device.data = msg.state;
|
||||
|
||||
msg.pathToChange.forEach((path) => {
|
||||
device.update(path, device.data);
|
||||
});
|
||||
};
|
||||
|
||||
function framesToTime(total) {
|
||||
if (timerFrameRate) {
|
||||
const seconds = Math.floor(total / timerFrameRate);
|
||||
const frames = total - seconds * timerFrameRate;
|
||||
let framesString = String(frames);
|
||||
if (frames < 10) {
|
||||
framesString = `0${framesString}`;
|
||||
} else if (framesString === '0') {
|
||||
framesString = '00';
|
||||
}
|
||||
return `${seconds}:${framesString}`;
|
||||
}
|
||||
return '0:00';
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.me-label {
|
||||
margin-top: 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.atem-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;
|
||||
}
|
||||
|
||||
.atem-red {
|
||||
background-image: url('img/button_red.png');
|
||||
box-shadow: 0px 0px 10px 1px #ff0000;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.atem-green {
|
||||
background-image: url('img/button_green.png');
|
||||
box-shadow: 0px 0px 10px 1px #06c300;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.atem-yellow {
|
||||
background-image: url('img/button_yellow.png');
|
||||
box-shadow: 0px 0px 10px 1px #c7ca00;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.atem-disabled {
|
||||
background-image: url('img/button_off.png');
|
||||
color: #3a3a3a;
|
||||
}
|
||||
|
||||
.atem-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;
|
||||
}
|
||||
|
||||
.dsk {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dsk .source-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dsk h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fade-to-black-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.fade-to-black .source-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.fade-to-black h3 {
|
||||
text-align: center;
|
||||
}
|
||||
.rate-heading {
|
||||
text-align: center;
|
||||
font-size: small;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dsk-rate,
|
||||
.ftb-rate,
|
||||
.transition-rate {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dsk-rate-label,
|
||||
.ftb-rate-label,
|
||||
.transition-rate-label {
|
||||
color: rgb(235, 110, 0);
|
||||
background-color: black;
|
||||
border-radius: 3px;
|
||||
padding: 6px 2px;
|
||||
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,189 @@
|
||||
<header>
|
||||
<h1><%= listName %></h1>
|
||||
</header>
|
||||
|
||||
<% if (data.video.mixEffects) { %>
|
||||
<% Object.entries(data.video.mixEffects).forEach(([meIndex, state])=>{ %>
|
||||
<div class="mixeffect_wrapper" id="me-<%= meIndex %>">
|
||||
<h1 class="atem-gray me-label">ME<%= Number(meIndex) + 1 %></h1>
|
||||
<div class="inputs-container show-small">
|
||||
<h3 class="atem-gray">Preview/Program</h3>
|
||||
<div class="source-wrapper">
|
||||
<% Object.values(data.inputs).sort((a,b)=>a.internalPortType<b.internalPortType).forEach((input)=>{ %>
|
||||
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
|
||||
<div id="me-<%= meIndex %>-input-<%= input.inputId %>" class="atem-input">
|
||||
<div class="source-label"><%= input.shortName %></div>
|
||||
</div>
|
||||
<% } else {%>
|
||||
<div id="me-<%= meIndex %>-input-<%= input.inputId %>" class="atem-input atem-disabled">
|
||||
<div class="source-label"><%= input.shortName %></div>
|
||||
</div>
|
||||
<% }%>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="float-left" style="margin-right: 30px;">
|
||||
<div class="program-container hide-small">
|
||||
<h3 class="atem-gray">Program</h3>
|
||||
<div class="source-wrapper">
|
||||
<% Object.values(data.inputs).sort((a,b)=>a.internalPortType<b.internalPortType).forEach((input)=>{ %>
|
||||
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
|
||||
<div id="me-<%= meIndex %>-program-input-<%= input.inputId %>" class="atem-input">
|
||||
<div class="source-label"><%= input.shortName %></div>
|
||||
</div>
|
||||
<% } else {%>
|
||||
<div id="me-<%= meIndex %>-program-input-<%= input.inputId %>" class="atem-input atem-disabled">
|
||||
<div class="source-label"><%= input.shortName %></div>
|
||||
</div>
|
||||
<% }%>
|
||||
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview hide-small">
|
||||
<h3 class="atem-gray">Preview</h3>
|
||||
<div class="source-wrapper">
|
||||
<% Object.values(data.inputs).forEach((input)=>{ %>
|
||||
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
|
||||
<div id="me-<%= meIndex %>-preview-input-<%= input.inputId %>" class="atem-input">
|
||||
<div class="source-label"><%= input.shortName %></div>
|
||||
</div>
|
||||
<% } else {%>
|
||||
<div id="me-<%= meIndex %>-preview-input-<%= input.inputId %>" class="atem-input atem-disabled">
|
||||
<div class="source-label"><%= input.shortName %></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="atem-input clear"></div>
|
||||
<div id="me-<%= meIndex %>-key-1-onair" class="atem-input">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-key-2-onair" class="atem-input <% if(data.video.downstreamKeyers.length<2){ %> atem-disabled <% } %>">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-key-3-onair" class="atem-input <% if(data.video.downstreamKeyers.length<3){ %> atem-disabled <% } %>">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-key-4-onair" class="atem-input <% if(data.video.downstreamKeyers.length<4){ %> atem-disabled <% } %>">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="me-<%= meIndex %>-transition-selection-1" class="atem-input">
|
||||
<div class="source-label">BKGD</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-selection-2" class="atem-input">
|
||||
<div class="source-label">KEY 1</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-selection-4" class="atem-input <% if(data.video.downstreamKeyers.length<2){ %> atem-disabled <% } %>">
|
||||
<div class="source-label">KEY 2</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-selection-8" class="atem-input <% if(data.video.downstreamKeyers.length<3){ %> atem-disabled <% } %>">
|
||||
<div class="source-label">KEY 3</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-selection-16" class="atem-input <% if(data.video.downstreamKeyers.length<4){ %> atem-disabled <% } %>">
|
||||
<div class="source-label">KEY 4</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transition-style">
|
||||
<h3>Transition Style</h3>
|
||||
<div class="source-wrapper" style="width: 260px;">
|
||||
<div id="me-<%= meIndex %>-transition-style-0" class="atem-input">
|
||||
<div class="source-label">MIX</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-style-1" class="atem-input">
|
||||
<div class="source-label">DIP</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-style-2" class="atem-input">
|
||||
<div class="source-label">WIPE</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-style-4" class="atem-input">
|
||||
<div class="source-label">STING</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-style-3" class="atem-input">
|
||||
<div class="source-label">DVE</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-transition-preview" class="atem-input">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>PREV</div> <div>TRANS</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="atem-input clear"></div>
|
||||
<div id="me-<%= meIndex %>-cut" class="atem-input">
|
||||
<div class="source-label">CUT</div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-auto" class="atem-input">
|
||||
<div class="source-label">AUTO</div>
|
||||
</div>
|
||||
<div class="transition-rate atem-input clear">
|
||||
<div class="atem-gray rate-heading">Rate</div>
|
||||
<div id="me-<%= meIndex %>-transition-rate" class="transition-rate-label"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tbar-container">
|
||||
<div class="tbar-bg"></div>
|
||||
<div id="me-<%= meIndex %>-tbar-handle-div" class="tbar-handle"></div>
|
||||
<div id="me-<%= meIndex %>-tbar-div" class="tbar-div" ></div>
|
||||
</div>
|
||||
<div class="fade-to-black-container">
|
||||
<% if (data.video.downstreamKeyers) { %>
|
||||
<% Object.entries(data.video.downstreamKeyers).forEach(([dskIndex, state])=>{ %>
|
||||
<div class="dsk">
|
||||
<h3 class="atem-gray">DSK<%= Number(dskIndex) + 1 %></h3>
|
||||
<div class="source-wrapper">
|
||||
<div id="dsk-<%= dskIndex %>-tie" class="atem-input">
|
||||
<div class="source-label">TIE</div>
|
||||
</div>
|
||||
<div class="dsk-rate atem-input clear">
|
||||
<div class="atem-gray rate-heading">Rate</div>
|
||||
<div id="dsk-<%= dskIndex %>-rate" class="dsk-rate-label"> </div>
|
||||
</div>
|
||||
<div id="dsk-<%= dskIndex %>-onair" class="atem-input">
|
||||
<div class="source-label" style="flex-direction: column;">
|
||||
<div>ON</div> <div>AIR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dsk-<%= dskIndex %>-auto" class="atem-input">
|
||||
<div class="source-label">AUTO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
<div class="fade-to-black">
|
||||
<h3 class="atem-gray">Fade to Black</h3>
|
||||
<div class="source-wrapper">
|
||||
<div class="ftb-rate atem-input clear">
|
||||
<div class="atem-gray rate-heading">Rate</div>
|
||||
<div id="me-<%= meIndex %>-ftb-rate" class="ftb-rate-label"> </div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-ftb" class="atem-input">
|
||||
<div class="source-label">FTB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
@@ -2,7 +2,7 @@ const { v4: uuid } = require('uuid');
|
||||
const osc = require('osc');
|
||||
const net = require('net');
|
||||
const udp = require('dgram');
|
||||
|
||||
const { Atem } = require('atem-connection');
|
||||
const PLUGINS = require('./plugins.js');
|
||||
const VIEW = require('./view.js');
|
||||
const SAVESLOTS = require('./saveSlots.js');
|
||||
@@ -193,6 +193,26 @@ function initDeviceConnection(id) {
|
||||
});
|
||||
|
||||
device.send = (data) => {};
|
||||
} else if (plugins[type].config.connectionType === 'atem') {
|
||||
device.connection = new Atem({
|
||||
// this gets around the no workers nodejs error
|
||||
disableMultithreaded: true,
|
||||
});
|
||||
device.connection.connect(device.addresses[0]);
|
||||
|
||||
device.connection.on('connected', () => {
|
||||
infoUpdate(device, 'status', 'ok');
|
||||
plugins[type].ready(device);
|
||||
});
|
||||
|
||||
device.connection.on('stateChanged', (state, pathToChange) => {
|
||||
device.lastMessage = Date.now();
|
||||
plugins[type].data(device, {
|
||||
pathToChange,
|
||||
state,
|
||||
});
|
||||
infoUpdate(device, 'status', 'ok');
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -5,6 +5,7 @@ const net = require('net');
|
||||
const os = require('os');
|
||||
const ip = require('ip');
|
||||
|
||||
const { Netmask } = require('netmask');
|
||||
const DEVICE = require('./device.js');
|
||||
const PLUGINS = require('./plugins.js');
|
||||
|
||||
@@ -91,6 +92,8 @@ function searchAll() {
|
||||
searchUDP(pluginType, plugin.config);
|
||||
} else if (searchType === 'multicast') {
|
||||
searchMulticast(pluginType, plugin.config);
|
||||
} else if (searchType === 'UDPScan') {
|
||||
searchUDPScan(pluginType, plugin.config);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Unable to search for plugin ${pluginType}`);
|
||||
@@ -167,6 +170,36 @@ function TCPtest(ipAddr, pluginType, pluginConfig) {
|
||||
});
|
||||
}
|
||||
|
||||
function searchUDPScan(pluginType, pluginConfig) {
|
||||
for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
|
||||
const interfaceID = Object.keys(validInterfaces)[i];
|
||||
const interfaceObj = validInterfaces[interfaceID];
|
||||
interfaceObj.forEach((netInterface) => {
|
||||
const udpSocket = dgram.createSocket('udp4');
|
||||
udpSocket.bind(pluginConfig.searchOptions.listenPort, netInterface.address);
|
||||
|
||||
udpSocket.on('message', (msg, info) => {
|
||||
if (pluginConfig.searchOptions.validateResponse(msg, info, DEVICE.all)) {
|
||||
udpSocket.close();
|
||||
DEVICE.registerDevice(
|
||||
{
|
||||
type: pluginType,
|
||||
defaultName: pluginConfig.defaultName,
|
||||
port: pluginConfig.defaultPort,
|
||||
addresses: [info.address],
|
||||
},
|
||||
'fromSearch'
|
||||
);
|
||||
}
|
||||
});
|
||||
const interfaceBlock = new Netmask(netInterface.cidr);
|
||||
interfaceBlock.forEach((address, long, index) => {
|
||||
udpSocket.send(pluginConfig.searchOptions.searchBuffer, pluginConfig.searchOptions.devicePort, address);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function searchUDP(pluginType, pluginConfig) {
|
||||
for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
|
||||
const interfaceID = Object.keys(validInterfaces)[i];
|
||||
|
||||