226 lines
7.6 KiB
JavaScript
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;
|