wifi-tally_Oostendam/wifi-tally/hub/lib/TallyDriver.js
Eljakim Herrewijnen 50b5fc1824 Initial commit
2021-09-27 21:52:27 +02:00

226 lines
7.6 KiB
JavaScript

const Tally = require('../domain/Tally')
const dgram = require('dgram');
const tallyHighlightTime = 1000 // ms
// the more keep alives you send the less likely it is that
// the tally shows a wrong state, but you send more packages
// over the network.
const keepAlivesPerSecond = 10
const updateTally = function(tally, io, programs, previews) {
if(tally.isActive()) {
let command = "release"
if(tally.isHighlighted()) {
command = "highlight"
} else if(programs === null && tally.isPatched()) {
// mixer is disconnected
command = "unknown"
} else if (programs !== null && tally.isIn(programs)) {
command = "on-air"
} else if (previews !== null && tally.isIn(previews)) {
command = "preview"
}
io.send(command, tally.port, tally.address)
}
}
class InvalidCommandError extends Error {
constructor(...args) {
super(...args)
this.message = `Received an invalid command: "${this.message}"`
}
}
// - handles connections with Tallies.
// - emits signals when tallies connect, go missing or disconnect
class TallyDriver {
constructor(tallies, emitter) {
this.tallies = new Map();
(tallies || []).forEach(tally => {
tally = Tally.fromValueObject(tally)
this.tallies.set(tally.name, tally)
})
this.emitter = emitter
this.lastPrograms = null
this.lastPreviews = null
this.io = dgram.createSocket('udp4')
this.io.on('error', (err) => {
console.log(`server error: ${err.stack}`);
this.io.close();
});
this.io.on('message', (msg, rinfo) => {
try {
msg = msg.toString().trim()
if (msg.startsWith("tally-ho")) {
const tallyName = TallyDriver.parseTallyHo(msg)
this._tallyReported(tallyName, rinfo)
} else if (msg.startsWith("log")) {
const [tallyName, severity, message] = TallyDriver.parseLog(msg)
const tally = this._tallyReported(tallyName, rinfo)
const log = tally.addLog(new Date(), severity, message)
this.emitter.emit('tally.logged', tally, log)
} else {
throw new InvalidCommandError(msg)
}
} catch (e) {
if (e instanceof InvalidCommandError) {
console.warn(e.message)
} else {
throw e
}
}
});
this.io.on('listening', () => {
const address = this.io.address()
console.log(`Listening for Tallies on ${address.address}:${address.port}`)
});
this.io.bind(7411)
// watchdog to check if a tally disconnected
const lastTallyReport = new Map();
this.emitter.on('tally.reported', tally => {
lastTallyReport.set(tally.name, new Date())
tally.state = Tally.CONNECTED
})
setInterval(() => {
const now = new Date()
this.tallies.forEach(tally => {
if(!lastTallyReport.has(tally.name)) {
tally.state = Tally.DISCONNECTED
} else {
const diff = now - lastTallyReport.get(tally.name) // milliseconds
if(diff > 30000) {
if(tally.state !== Tally.DISCONNECTED) {
tally.state = Tally.DISCONNECTED
this.emitter.emit('tally.timedout', tally, diff)
}
} else if(diff > 3000) {
if(tally.state !== Tally.MISSING) {
tally.state = Tally.MISSING
this.emitter.emit('tally.missing', tally, diff)
}
}
}
})
}, 500)
// send keep-alive messages
// - show the tally, we are still here
// - compensate for lost packages
setInterval(() => {
this.updateTallies()
}, 1000 / keepAlivesPerSecond)
}
_tallyReported(tallyName, rinfo) {
if (!this.tallies.has(tallyName)) {
const tally = new Tally(tallyName, -1)
this.tallies.set(tallyName, tally)
}
const tally = this.tallies.get(tallyName)
if(tally.state !== Tally.CONNECTED) {
tally.state = Tally.CONNECTED
tally.address = rinfo.address;
tally.port = rinfo.port;
this.emitter.emit('tally.connected', tally)
}
if (tally.address !== rinfo.address || tally.port !== rinfo.port) {
tally.address = rinfo.address;
tally.port = rinfo.port;
this.emitter.emit('tally.changed', tally)
}
this.emitter.emit('tally.reported', this.tallies.get(tallyName))
return tally
}
highlight(tallyName) {
console.log("highlight", tallyName)
if(this.tallies.has(tallyName)) {
const tally = this.tallies.get(tallyName)
setTimeout(() => {
tally.setHighlight(false)
this.updateTally(tallyName)
}, tallyHighlightTime)
tally.setHighlight(true)
this.updateTally(tallyName)
}
}
setState(programs, previews) {
this.lastPrograms = programs
this.lastPreviews = previews
this.updateTallies()
}
patchTally(tallyName, channelId) {
if(this.tallies.has(tallyName)) {
const tally = this.tallies.get(tallyName)
tally.channelId = channelId
this.emitter.emit('tally.changed', tally)
}
}
removeTally(tallyName) {
if(this.tallies.has(tallyName)) {
const tally = this.tallies.get(tallyName)
this.tallies.delete(tallyName)
this.emitter.emit('tally.removed', tally)
}
}
updateTally(tallyName) {
const tally = this.tallies.get(tallyName)
updateTally(tally, this.io, this.lastPrograms, this.lastPreviews)
}
updateTallies() {
this.tallies.forEach(tally => updateTally(tally, this.io, this.lastPrograms, this.lastPreviews))
}
toValueObjects() {
return Array.from(this.tallies.values()).map(tally => tally.toValueObject())
}
toValueObjectsForSave() {
return Array.from(this.tallies.values()).map(tally => {
tally = tally.toValueObject()
delete tally.state
delete tally.address
delete tally.port
return tally
})
}
getTally(tallyName) {
if(this.tallies.has(tallyName)) {
const tally = this.tallies.get(tallyName)
return tally
}
}
}
TallyDriver.parseTallyHo = function(cmd) {
const result = cmd.match(/^([^ ]+) "(.+)"/)
if (result === null) {
throw new InvalidCommandError(cmd)
} else {
const [_, command, name] = result
if (command !== "tally-ho") {
throw new InvalidCommandError(command)
}
return name
}
}
TallyDriver.parseLog = function(cmd) {
const result = cmd.match(/^([^ ]+) "(.+)" ([^ ]+) "(.*)"/)
if (result === null) {
throw new InvalidCommandError(cmd)
} else {
const [_, command, name, severity, message] = result
if (command !== "log") {
throw new InvalidCommandError(command)
}
return [name, severity, message]
}
}
module.exports = TallyDriver;