77 lines
3.8 KiB
JavaScript
77 lines
3.8 KiB
JavaScript
'use strict';
|
|
// OpenAction / Stream Deck protocol; Node 22+, no npm dependencies.
|
|
const args = process.argv.slice(2);
|
|
const arg = name => args[args.indexOf(name) + 1];
|
|
const port = Number(arg('-port'));
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Missing OpenDeck port');
|
|
const socket = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
const visible = new Map(), rendered = new Map();
|
|
let cached = null, refreshing = false, queue = Promise.resolve();
|
|
function send(event, context, payload) {
|
|
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({event,context,payload}));
|
|
}
|
|
async function state() {
|
|
const response = await fetch('http://127.0.0.1:8765/api/state',{signal:AbortSignal.timeout(3000)});
|
|
if (!response.ok) throw new Error('VoicePult unavailable');
|
|
return response.json();
|
|
}
|
|
function itemFor(settings, data) {
|
|
return (settings.kind === 'target' ? data.targets : data.channels)?.find(c=>c.id === settings.id);
|
|
}
|
|
function paint(context, settings, data) {
|
|
const item = data && itemFor(settings, data);
|
|
const label = item?.name || settings.name || 'Kanal wählen';
|
|
const lines = label.replace(/(.{1,12})(?:\s+|$)/g,'$1\n').trim();
|
|
const status = !data ? 'OFFLINE' : !item ? 'FEHLT' : item.muted ? 'AUS' : 'AN';
|
|
const selected = !item || item.muted || !data ? 1 : 0;
|
|
const title = `${lines}\n${status}`;
|
|
const signature = JSON.stringify([selected,title]);
|
|
if (rendered.get(context) === signature) return;
|
|
send('setTitle',context,{title,target:0,state:0});
|
|
send('setTitle',context,{title,target:0,state:1});
|
|
send('setState',context,{state:selected});
|
|
rendered.set(context,signature);
|
|
}
|
|
async function refresh() {
|
|
if (refreshing || !visible.size) return;
|
|
refreshing = true;
|
|
try { cached = await state(); }
|
|
catch { cached = null; }
|
|
finally { refreshing = false; }
|
|
for (const [context, settings] of visible) paint(context,settings,cached);
|
|
}
|
|
async function toggle(context, settings) {
|
|
try {
|
|
// Server toggles atomically; don't infer the next state from a stale key image.
|
|
const before = await state();
|
|
if (!itemFor(settings,before)) throw new Error('Channel removed');
|
|
const response = await fetch('http://127.0.0.1:8765/api/action',{
|
|
method:'POST', headers:{'Content-Type':'application/json','X-VoicePult-Token':before.token},
|
|
body:JSON.stringify({action:settings.kind==='target'?'toggle_target':'toggle_channel',id:settings.id}),
|
|
signal:AbortSignal.timeout(15000)
|
|
});
|
|
if (!response.ok) throw new Error('VoicePult action failed');
|
|
cached = await response.json();
|
|
for (const [key,settings] of visible) paint(key,settings,cached);
|
|
} catch (error) { send('showAlert',context); console.error(error.message); await refresh(); }
|
|
}
|
|
socket.addEventListener('open',()=>socket.send(JSON.stringify({event:arg('-registerEvent'),uuid:arg('-pluginUUID')})));
|
|
socket.addEventListener('message',async event=>{
|
|
try {
|
|
const msg=JSON.parse(event.data), settings=msg.payload?.settings || visible.get(msg.context) || {};
|
|
if (msg.event==='willAppear' || msg.event==='didReceiveSettings') {
|
|
visible.set(msg.context,settings); rendered.delete(msg.context); await refresh();
|
|
} else if (msg.event==='willDisappear') {
|
|
visible.delete(msg.context); rendered.delete(msg.context);
|
|
} else if (msg.event==='keyDown') {
|
|
queue=queue.then(()=>toggle(msg.context,settings)).catch(error=>console.error(error.message));
|
|
} else if (msg.event==='sendToPlugin') {
|
|
const data=await state();
|
|
send('sendToPropertyInspector',msg.context,{items:[...data.channels.map(c=>({...c,kind:'channel'})),...data.targets.map(c=>({...c,kind:'target'}))]});
|
|
}
|
|
} catch(error) {console.error(error.message);}
|
|
});
|
|
socket.addEventListener('close',()=>process.exit(0));
|
|
socket.addEventListener('error',()=>console.error('OpenDeck connection failed'));
|
|
setInterval(refresh,1000);
|