initial commit

This commit is contained in:
Franz Heinzmann (Frando) 2021-06-07 16:22:13 +02:00
commit 0024f44c80
34 changed files with 1991 additions and 0 deletions

View file

@ -0,0 +1,104 @@
import { spawn } from 'child_process'
import split2 from 'split2'
import { get } from 'http'
import { Readable } from 'streamx'
import { fixPath } from 'os-dependent-path-delimiter'
import kill from 'tree-kill'
const eventRegexps = {
callEstablished: /Call established: (.+)/,
callReceived: /Incoming call from: ([\+\w]+ )?(\S+) -/,
hangUp: /(.+): session closed/,
ready: /baresip is ready/,
serverConnected: /\[\d+ bindings?\]/
}
const options = { host: '127.0.0.1', port: '8000', agent: false }
const nop = () => {}
const executeCommand = (command) => {
options.path = `/?${command}`
get(options, nop)
}
export default class Baresip {
constructor (opts) {
const { command, args, callbacks } = opts
this.connected = false
this.processPath = fixPath(command)
this.args = args
this.callbacks = {}
this.stream = new Readable()
Object.keys(eventRegexps).forEach((event) => {
this.on(event, callbacks[event] === undefined ? () => {} : callbacks[event])
});
[
'on',
'connect',
'kill',
'reload'
].forEach((method) => {
this[method] = this[method].bind(this)
})
}
accept () {
executeCommand('a')
}
dial (phoneNumber) {
executeCommand(`d${phoneNumber}`)
}
hangUp () {
executeCommand('b')
}
toggleCallMuted () {
executeCommand('m')
}
on (event, callback) {
this.callbacks[event] = callback
}
kill (callback) {
kill(this.process.pid, 'SIGKILL', (err) => {
if (!err) {
this.connected = false
if (callback !== undefined) {
callback()
}
}
})
}
reload () {
this.kill(() => this.connect())
}
connect () {
this.connected = true
this.process = spawn(this.processPath, this.args)
this.process.stdout.pipe(split2()).on('data', (data) => {
const parsedData = `${data}`
Object.keys(eventRegexps).forEach((event) => {
const matches = parsedData.match(eventRegexps[event])
if ((matches !== null) && (matches.length > 0)) {
this.callbacks[event](matches[matches.length - 1])
}
})
this.stream.push(parsedData)
// console.log(parsedData)
})
// this.process.stderr.pipe(split2()).on('data', (data) => this.stream.push(data))
}
}

View file

@ -0,0 +1,100 @@
import { spawn } from 'child_process'
import createDebug from 'debug'
import { EventEmitter } from 'events'
import process from 'process'
const debug = createDebug('meter')
export class AlsaMeter extends EventEmitter {
constructor (opts = {}) {
super()
if (!opts.input) this.input = { host: 'alsa', device: 'default' }
else if (typeof opts.input === 'string') this.input = { host: 'alsa', input: opts.input }
else this.input = opts.input
// setInterval(() => console.log(this._lastMessage), 1000)
}
open () {
if (!this._opening) this._opening = this._open()
return this._opening
}
async _open () {
const cmd = 'ffmpeg'
let args = [
'-nostats',
'-f',
this.input.host,
'-i',
this.input.device,
'-filter_complex',
'ebur128=peak=true',
'-f',
'null',
'-'
]
debug('spawn: ' + `${cmd} ${args.map(a => `"${a}"`).join(' ')}`)
this.proc = spawn(cmd, args)
// const onclose = (ev) => {
// this._closed = true
// this.proc.kill()
// }
// process.on('exit', onclose.bind('exit'))
// process.on('SIGINT', onclose.bind('SIGINT'))
// process.on('SIGTERM', onclose.bind('SIGTERM'))
this.proc.stderr.on('data', msg => {
this.parseMessage(msg)
})
return new Promise((resolve, reject) => {
this.proc.stderr.once('data', msg => {
this._open = true
resolve()
})
})
}
parseMessage (msg) {
let str = msg.toString('utf-8')
let match = str.match(/\[.*\](.*)/)
if (!match || match.length !== 2) return null
str = match[1].trim()
let parts = str.split(/([a-zA-Z]+:)/).filter(f => f)
let last = null
let map = parts.reduce((agg, token, i) => {
token = token.trim()
if (last) {
agg[last] = token
last = null
} else {
last = token.replace(/[:\s]/g, '')
}
return agg
}, {})
let res = {}
if (map.LRA) res.LRA = parseFloat(map.LRA.replace('LU', ''))
if (map.t) res.t = parseFloat(map.t)
if (map.TARGET) res.TARGET = parseFloat(map.TARGET.replace('LUFS', ''))
if (map.I) res.I = parseFloat(map.I.replace('LUFS', ''))
if (map.M) res.M = parseFloat(map.M)
if (map.S) res.S = parseFloat(map.S)
for (let key of ['FTPK', 'TPK']) {
if (map[key]) {
let parts = map[key].split(/\s+/)
if (parts.length >= 2) {
res[key] = [parseFloat(parts[0]), parseFloat(parts[1])]
} else {
res[key] = null
}
}
}
if (Object.keys(res).length) {
// debug(Object.entries(res).map(([k, v]) => `${k}: ${v}`).join(' | '))
this._lastMessage = res
this.emit('meter', res)
}
}
}

34
backend/lib/is-online.mjs Normal file
View file

@ -0,0 +1,34 @@
import isOnline from 'is-online'
import { Readable } from 'streamx'
import { action, CONNECTIVITY } from '../state.mjs'
export class ConnectivityCheck extends Readable {
constructor (opts = {}) {
super()
this.opts = opts
this.retryInterval = opts.retryInterval || 2000
this._action({ internet: false })
this._update()
}
async _update () {
try {
const hasInternet = await isOnline(this.opts)
this._action({ internet: hasInternet })
} catch (err) {
this._action({ internet: false })
} finally {
this._timeout = setTimeout(() => this._update(), this.retryInterval)
}
}
_destroy () {
if (this._timeout) clearTimeout(this._timeout)
}
_action (data) {
if (this.destroyed) return
this.push(action(CONNECTIVITY, data))
}
}