Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
b026a23de5 | |||
b47a824ce8 | |||
2a89ab0987 | |||
4656d201bd | |||
d767f6b82c | |||
671c2d177f | |||
1ac61de439 | |||
0f0dca1814 | |||
19baa706fc |
24 changed files with 570 additions and 454 deletions
|
@ -3,21 +3,36 @@ jobs:
|
|||
build:
|
||||
docker:
|
||||
- image: circleci/node:latest
|
||||
working_directory: ~/caspar-sup
|
||||
working_directory: ~/app
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: Build docker image
|
||||
command: docker build -t nfpis/caspar-sup:build_${CIRCLE_BUILD_NUM} -t nfpis/caspar-sup:${CIRCLE_SHA1} -t nfpis/caspar-sup:latest .
|
||||
- deploy:
|
||||
name: Push to docker
|
||||
name: Install npm deployment app
|
||||
command: sudo npm install -g github-release-cli @babel/runtime
|
||||
- run:
|
||||
name: Build client javascript
|
||||
command: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASS
|
||||
docker push nfpis/caspar-sup
|
||||
npm install
|
||||
npm run build
|
||||
- deploy:
|
||||
name: Create a release
|
||||
command: |
|
||||
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[", ]//g')
|
||||
echo "Packaging to ${CIRCLE_PROJECT_REPONAME}_build-sc.zip"
|
||||
zip "${CIRCLE_PROJECT_REPONAME}_build-sc.zip" index.mjs package.json public/* api/**/* api/*
|
||||
echo "Creating release '${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}'"
|
||||
github-release upload \
|
||||
--commitish $CIRCLE_SHA1 \
|
||||
--token $GITHUB_TOKEN \
|
||||
--owner $CIRCLE_PROJECT_USERNAME \
|
||||
--repo $CIRCLE_PROJECT_REPONAME \
|
||||
--tag "v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}" \
|
||||
--release-name "v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}" \
|
||||
--body "Automatic CircleCI Build of v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM} from ${CIRCLE_SHA1}" \
|
||||
"${CIRCLE_PROJECT_REPONAME}_build-sc.zip"
|
||||
workflows:
|
||||
version: 2
|
||||
build_deploy:
|
||||
jobs:
|
||||
- build:
|
||||
context: org-global
|
||||
context: github-thething
|
|
@ -22,7 +22,7 @@ FROM node:13-alpine
|
|||
|
||||
ENV HOME=/app
|
||||
|
||||
COPY .babelrc config.js log.js index.js package.json $HOME/
|
||||
COPY index.mjs package.json $HOME/
|
||||
|
||||
WORKDIR $HOME
|
||||
|
||||
|
|
|
@ -1,69 +1,219 @@
|
|||
import CasparConnection from 'casparcg-connection'
|
||||
|
||||
const CasparCG = CasparConnection.CasparCG
|
||||
const AMCP = CasparConnection.AMCP
|
||||
|
||||
const timeoutDuration = 5000
|
||||
import net from 'net'
|
||||
import parser from 'p3x-xml2json'
|
||||
|
||||
let io
|
||||
let logger
|
||||
|
||||
let connection
|
||||
let casparIsPlaying
|
||||
let casparIsConnected
|
||||
let currentHost
|
||||
let client
|
||||
let db
|
||||
|
||||
export function initialise(log, db, socket) {
|
||||
io = socket
|
||||
logger = log
|
||||
db = db
|
||||
let queue = []
|
||||
let reconnectInterval = 1000
|
||||
let isReconnecting = false
|
||||
let connected = false
|
||||
let playing = false
|
||||
let lastError = ''
|
||||
|
||||
connect(db)
|
||||
function startReconnecting() {
|
||||
connected = false
|
||||
playing = false
|
||||
if (queue.length) {
|
||||
queue.splice(0, queue.length)
|
||||
}
|
||||
if(isReconnecting !== false) return
|
||||
reconnectInterval = Math.min(reconnectInterval * 1.5, 1000 * 60 * 5)
|
||||
isReconnecting = setTimeout(connect, reconnectInterval)
|
||||
}
|
||||
|
||||
export function connect(db) {
|
||||
currentHost = db.get('settings').value().casparhost
|
||||
casparIsPlaying = false
|
||||
casparIsConnected = false
|
||||
logger.info('CasparCG: Connectiong to', currentHost + ':' + 5250)
|
||||
function clearReconnect() {
|
||||
if(isReconnecting === false) return
|
||||
clearTimeout(isReconnecting)
|
||||
isReconnecting = false
|
||||
}
|
||||
|
||||
connection = new CasparCG({
|
||||
host: currentHost,
|
||||
port: 5250,
|
||||
queueMode: 2,
|
||||
autoReconnectInterval: timeoutDuration,
|
||||
onError: err => {
|
||||
logger.error(err, 'CasparCG: Error')
|
||||
},
|
||||
onConnectionStatus: data => {
|
||||
if (casparIsPlaying) return
|
||||
casparIsConnected = data.connected
|
||||
export function queueCommand(command) {
|
||||
return new Promise((res, rej) => {
|
||||
if (isReconnecting) {
|
||||
return rej(new Error('CasparCG is not connected, unable to play command'))
|
||||
}
|
||||
let request = {
|
||||
command: command,
|
||||
res: res,
|
||||
rej: rej,
|
||||
started: new Date(),
|
||||
finished: null,
|
||||
timeout: null,
|
||||
}
|
||||
queue.push(request)
|
||||
|
||||
if (!casparIsConnected) {
|
||||
logger.warn(`CasparCG: connection down, retrying in ${timeoutDuration / 1000} seconds`)
|
||||
request.timeout = setTimeout(function() {
|
||||
if (request.finished) return
|
||||
queue.splice(queue.indexOf(request), 1)
|
||||
rej(new Error(`CasparCGCommand "${command}" timed out after 15 seconds`))
|
||||
}, 15000)
|
||||
|
||||
logger.info('CasparCG Command:', command)
|
||||
client.write(command + '\r\n')
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkPlaying(db, io, wasSuccess) {
|
||||
if (!connected) return
|
||||
let path = `http://${db.get('settings.casparplayhost').value()}/client.html`
|
||||
|
||||
try {
|
||||
logger.info('CasparCG: Checking if already playing')
|
||||
let res = await queueCommand('INFO 1-100')
|
||||
if (res.body.channel
|
||||
&& res.body.channel.stage
|
||||
&& res.body.channel.stage.layer
|
||||
&& res.body.channel.stage.layer.layer_100
|
||||
&& res.body.channel.stage.layer.layer_100.foreground
|
||||
&& res.body.channel.stage.layer.layer_100.foreground.file
|
||||
&& res.body.channel.stage.layer.layer_100.foreground.file.path === path) {
|
||||
logger.info('CasparCG: Player is playing')
|
||||
playing = true
|
||||
lastError = ''
|
||||
io.emit('casparcg.status', currentStatus())
|
||||
return
|
||||
}
|
||||
if (wasSuccess) {
|
||||
logger.warn(res.body, 'CasparCG: Playing was marked as succeeded but could not verify it')
|
||||
playing = true
|
||||
lastError = 'Sending play command succeeded but was unable to verify'
|
||||
io.emit('casparcg.status', currentStatus())
|
||||
return
|
||||
}
|
||||
|
||||
playing = false
|
||||
lastError = 'Sending play command'
|
||||
io.emit('casparcg.status', currentStatus())
|
||||
logger.info(res.body, 'CasparCG: Sending play command')
|
||||
res = await queueCommand(`PLAY 1-100 [HTML] "${path}" CUT 1 LINEAR RIGHT`)
|
||||
return setTimeout(function() {
|
||||
checkPlaying(db, io, true).then()
|
||||
}, 300)
|
||||
} catch (err) {
|
||||
playing = false
|
||||
lastError = `CasparCG: Error checking if playing: ${err.message}. Checking again in 5seconds`
|
||||
logger.error(err, 'CasparCG: Error checking if playing')
|
||||
io.emit('casparcg.status', currentStatus())
|
||||
}
|
||||
},
|
||||
onConnected: async connected => {
|
||||
if (casparIsPlaying) return
|
||||
logger.info('CasparCG: connected', connected)
|
||||
if (!casparIsPlaying) {
|
||||
startPlaying(db).then()
|
||||
} else {
|
||||
logger.warn('CasparCG: Stopped from starting play again.')
|
||||
|
||||
return setTimeout(function() {
|
||||
checkPlaying(db, io, false).then()
|
||||
}, 5000)
|
||||
}
|
||||
},
|
||||
|
||||
export function initialise(log, database, ioOrg) {
|
||||
io = ioOrg
|
||||
logger = log
|
||||
db = database
|
||||
|
||||
client = new net.Socket()
|
||||
client.setEncoding('utf8')
|
||||
|
||||
client.on('connect', function () {
|
||||
clearReconnect()
|
||||
connected = true
|
||||
lastError = ''
|
||||
reconnectInterval = 1000
|
||||
logger.info('CasparCG: Connected to server')
|
||||
io.emit('casparcg.status', currentStatus())
|
||||
checkPlaying(db, io, false).then()
|
||||
// client.write('INFO 1-100\r\n');
|
||||
})
|
||||
|
||||
client.on('data', function (data) {
|
||||
let request = null
|
||||
|
||||
if (queue.length > 0) {
|
||||
request = queue[0]
|
||||
queue.splice(0, 1)
|
||||
}
|
||||
|
||||
if (!request) {
|
||||
return logger.warn({ data }, 'Received unknown response with no command')
|
||||
}
|
||||
|
||||
let status
|
||||
let splitted
|
||||
let header
|
||||
let body
|
||||
let parsed
|
||||
|
||||
try {
|
||||
splitted = data.split('\n')
|
||||
header = splitted[0].replace('\r', '')
|
||||
status = Number(header.split(' ')[0])
|
||||
body = splitted.slice(1)
|
||||
parsed = JSON.parse(parser.toJson(body.join('\n')))
|
||||
} catch (err) {
|
||||
return request.rej(err)
|
||||
}
|
||||
|
||||
request.finished = new Date()
|
||||
clearTimeout(request.timeout)
|
||||
if (status && status < 300) {
|
||||
request.res({
|
||||
status: status,
|
||||
header: header,
|
||||
body: parsed || {},
|
||||
raw: data
|
||||
})
|
||||
} else {
|
||||
request.err({
|
||||
status: status,
|
||||
header: header,
|
||||
body: parsed || {},
|
||||
raw: data
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
client.on('error', function (err) {
|
||||
lastError = 'CasparCG TCP Error: ' + err.code + ', retrying in ' + Math.round(reconnectInterval / 1000) + ' sec'
|
||||
logger.warn(lastError)
|
||||
io.emit('casparcg.status', currentStatus())
|
||||
startReconnecting()
|
||||
})
|
||||
client.on('close', function() {
|
||||
startReconnecting()
|
||||
})
|
||||
client.on('end', function() {
|
||||
startReconnecting()
|
||||
})
|
||||
|
||||
connect()
|
||||
}
|
||||
|
||||
export function connect() {
|
||||
clearReconnect()
|
||||
currentHost = db.get('settings').value().casparhost
|
||||
lastError = 'CasparCG: Connecting to ' + currentHost + ':' + 5250
|
||||
logger.info(lastError)
|
||||
io.emit('casparcg.status', currentStatus())
|
||||
|
||||
client.connect({
|
||||
port: 5250,
|
||||
host: currentHost
|
||||
})
|
||||
}
|
||||
|
||||
export function currentStatus(e) {
|
||||
return {
|
||||
connected: casparIsConnected,
|
||||
playing: casparIsPlaying,
|
||||
error: e,
|
||||
connected: connected,
|
||||
playing: playing,
|
||||
error: lastError,
|
||||
}
|
||||
}
|
||||
|
||||
export function sendCommand(command) {
|
||||
return new Promise(function(res, rej) {
|
||||
|
||||
})
|
||||
}
|
||||
/*
|
||||
export async function startPlaying(db) {
|
||||
let ip = db.get('settings').value().casparplayhost
|
||||
|
||||
|
@ -96,9 +246,9 @@ export async function startPlaying(db) {
|
|||
io.emit('casparcg.status', currentStatus())
|
||||
logger.info('CasparCG: client is up and playing')
|
||||
/* console.log(connection)
|
||||
for (var key in connection) {
|
||||
for (let key in connection) {
|
||||
console.log(key, '=', typeof(connection[key]))
|
||||
} */
|
||||
}
|
||||
connection.autoConnect = false
|
||||
// connection.close()
|
||||
} else {
|
||||
|
@ -106,4 +256,4 @@ export async function startPlaying(db) {
|
|||
casparIsPlaying = false
|
||||
io.emit('casparcg.status', currentStatus(e))
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { currentStatus } from './client.mjs'
|
||||
|
||||
export async function casparConnection(ctx) {
|
||||
export async function casparStatus(ctx) {
|
||||
ctx.socket.emit('casparcg.status', currentStatus())
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import nconf from 'nconf'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
// Helper method for global usage.
|
||||
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||
|
||||
// Config follow the following priority check order:
|
||||
// 1. Arguments
|
||||
// 2. package.json
|
||||
// 3. Enviroment variables
|
||||
// 4. config/config.json
|
||||
// 5. default settings
|
||||
|
||||
|
||||
// Load arguments as highest priority
|
||||
nconf.argv()
|
||||
|
||||
|
||||
// Load package.json for name and such
|
||||
let pckg = JSON.parse(readFileSync('./package.json'))
|
||||
let project = {
|
||||
name: pckg.name,
|
||||
version: pckg.version,
|
||||
description: pckg.description,
|
||||
author: pckg.author,
|
||||
license: pckg.license,
|
||||
homepage: pckg.homepage,
|
||||
}
|
||||
|
||||
// If we have global.it, there's a huge chance
|
||||
// we're in test mode so we force node_env to be test.
|
||||
if (typeof global.it === 'function') {
|
||||
project.NODE_ENV = 'test'
|
||||
}
|
||||
|
||||
|
||||
// Load overrides as second priority
|
||||
nconf.overrides(project)
|
||||
|
||||
|
||||
// Load enviroment variables as third priority
|
||||
nconf.env()
|
||||
|
||||
|
||||
// Load any overrides from the appropriate config file
|
||||
let configFile = 'config/config.json'
|
||||
|
||||
if (nconf.get('NODE_ENV') === 'test') {
|
||||
configFile = 'config/config.test.json'
|
||||
}
|
||||
|
||||
|
||||
nconf.file('main', configFile)
|
||||
|
||||
// Load defaults
|
||||
nconf.file('default', 'config/config.default.json')
|
||||
|
||||
|
||||
// Final sanity checks
|
||||
/* istanbul ignore if */
|
||||
if (typeof global.it === 'function' & !nconf.inTest()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Critical: potentially running test on production enviroment. Shutting down.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
export default nconf
|
162
api/db.mjs
162
api/db.mjs
|
@ -1,162 +0,0 @@
|
|||
import lowdb from 'lowdb'
|
||||
import FileAsync from 'lowdb/adapters/FileAsync.js'
|
||||
import log from './log.mjs'
|
||||
|
||||
let lastId = -1
|
||||
|
||||
// Take from https://github.com/typicode/lodash-id/blob/master/src/index.js
|
||||
// from package lodash-id
|
||||
const lodashId = {
|
||||
// Empties properties
|
||||
__empty: function (doc) {
|
||||
this.forEach(doc, function (value, key) {
|
||||
delete doc[key]
|
||||
})
|
||||
},
|
||||
|
||||
// Copies properties from an object to another
|
||||
__update: function (dest, src) {
|
||||
this.forEach(src, function (value, key) {
|
||||
dest[key] = value
|
||||
})
|
||||
},
|
||||
|
||||
// Removes an item from an array
|
||||
__remove: function (array, item) {
|
||||
var index = this.indexOf(array, item)
|
||||
if (index !== -1) array.splice(index, 1)
|
||||
},
|
||||
|
||||
__id: function () {
|
||||
var id = this.id || 'id'
|
||||
return id
|
||||
},
|
||||
|
||||
getById: function (collection, id) {
|
||||
var self = this
|
||||
return this.find(collection, function (doc) {
|
||||
if (self.has(doc, self.__id())) {
|
||||
return doc[self.__id()] === id
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
createId: function (collection, doc) {
|
||||
let next = new Date().getTime()
|
||||
if (next <= lastId) {
|
||||
next = lastId + 1
|
||||
}
|
||||
lastId = next
|
||||
return next
|
||||
},
|
||||
|
||||
insert: function (collection, doc) {
|
||||
doc[this.__id()] = doc[this.__id()] || this.createId(collection, doc)
|
||||
var d = this.getById(collection, doc[this.__id()])
|
||||
if (d) throw new Error('Insert failed, duplicate id')
|
||||
collection.push(doc)
|
||||
return doc
|
||||
},
|
||||
|
||||
upsert: function (collection, doc) {
|
||||
if (doc[this.__id()]) {
|
||||
// id is set
|
||||
var d = this.getById(collection, doc[this.__id()])
|
||||
if (d) {
|
||||
// replace properties of existing object
|
||||
this.__empty(d)
|
||||
this.assign(d, doc)
|
||||
} else {
|
||||
// push new object
|
||||
collection.push(doc)
|
||||
}
|
||||
} else {
|
||||
// create id and push new object
|
||||
doc[this.__id()] = this.createId(collection, doc)
|
||||
collection.push(doc)
|
||||
}
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
updateById: function (collection, id, attrs) {
|
||||
var doc = this.getById(collection, id)
|
||||
|
||||
if (doc) {
|
||||
this.assign(doc, attrs, {id: doc.id})
|
||||
}
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
updateWhere: function (collection, predicate, attrs) {
|
||||
var self = this
|
||||
var docs = this.filter(collection, predicate)
|
||||
|
||||
docs.forEach(function (doc) {
|
||||
self.assign(doc, attrs, {id: doc.id})
|
||||
})
|
||||
|
||||
return docs
|
||||
},
|
||||
|
||||
replaceById: function (collection, id, attrs) {
|
||||
var doc = this.getById(collection, id)
|
||||
|
||||
if (doc) {
|
||||
var docId = doc.id
|
||||
this.__empty(doc)
|
||||
this.assign(doc, attrs, {id: docId})
|
||||
}
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
removeById: function (collection, id) {
|
||||
var doc = this.getById(collection, id)
|
||||
|
||||
this.__remove(collection, doc)
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
removeWhere: function (collection, predicate) {
|
||||
var self = this
|
||||
var docs = this.filter(collection, predicate)
|
||||
|
||||
docs.forEach(function (doc) {
|
||||
self.__remove(collection, doc)
|
||||
})
|
||||
|
||||
return docs
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = new FileAsync('db.json')
|
||||
|
||||
export default function GetDB() {
|
||||
return lowdb(adapter)
|
||||
.then(function(db) {
|
||||
db._.mixin(lodashId)
|
||||
|
||||
db.defaults({
|
||||
graphics: [],
|
||||
presets: [],
|
||||
playing: [],
|
||||
schedule: [],
|
||||
settings: {
|
||||
casparplayhost: 'localhost:3000',
|
||||
casparhost: 'host.docker.internal',
|
||||
},
|
||||
version: 1,
|
||||
trash: [],
|
||||
})
|
||||
.write()
|
||||
.then(
|
||||
function() { },
|
||||
function(e) { log.error(e, 'Error writing defaults to lowdb') }
|
||||
)
|
||||
|
||||
return db
|
||||
})
|
||||
}
|
|
@ -45,7 +45,7 @@ export async function create(ctx, data) {
|
|||
await graphics.insert(data).write()
|
||||
let graphic = graphics.last().value()
|
||||
|
||||
ctx.io.emit('graphic.single', graphic)
|
||||
ctx.io.emit('graphic.created', graphic)
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -7,7 +7,11 @@ export function register(ctx, name, method) {
|
|||
}
|
||||
|
||||
ctx.socket.on(name, async (data) => {
|
||||
if (name.indexOf('list') > 0 || name.indexOf('all') || name.indexOf('total')) {
|
||||
ctx.log.debug('Got event', name)
|
||||
} else {
|
||||
ctx.log.info('Got event', name)
|
||||
}
|
||||
|
||||
try {
|
||||
await method(ctx, data)
|
||||
|
|
20
api/log.mjs
20
api/log.mjs
|
@ -1,20 +0,0 @@
|
|||
import bunyan from 'bunyan-lite'
|
||||
import defaults from './defaults.mjs'
|
||||
import config from './config.mjs'
|
||||
|
||||
// Clone the settings as we will be touching
|
||||
// on them slightly.
|
||||
let settings = defaults(config.get('bunyan'), null)
|
||||
|
||||
// Replace any instance of 'process.stdout' with the
|
||||
// actual reference to the process.stdout.
|
||||
for (let i = 0; i < settings.streams.length; i++) {
|
||||
if (settings.streams[i].stream === 'process.stdout') {
|
||||
settings.streams[i].stream = process.stdout
|
||||
}
|
||||
}
|
||||
|
||||
// Create our logger.
|
||||
const logger = bunyan.createLogger(settings)
|
||||
|
||||
export default logger
|
|
@ -1,7 +1,6 @@
|
|||
import logger from './log.mjs'
|
||||
import { register } from './io/helper.mjs'
|
||||
import { contentConnection } from './content/connection.mjs'
|
||||
import { casparConnection } from './casparcg/connection.mjs'
|
||||
import { casparStatus } from './casparcg/status.mjs'
|
||||
|
||||
import * as content from './content/routes.mjs'
|
||||
import * as engine from './engine/routes.mjs'
|
||||
|
@ -10,7 +9,7 @@ import * as preset from './preset/routes.mjs'
|
|||
import * as settings from './settings/routes.mjs'
|
||||
import * as schedule from './schedule/routes.mjs'
|
||||
|
||||
function onConnection(server, db, data) {
|
||||
function onConnection(server, db, logger, data) {
|
||||
const io = server
|
||||
const socket = data
|
||||
const log = logger.child({
|
||||
|
@ -20,7 +19,7 @@ function onConnection(server, db, data) {
|
|||
let ctx = { io, socket, log, db }
|
||||
|
||||
contentConnection(ctx)
|
||||
casparConnection(ctx)
|
||||
casparStatus(ctx)
|
||||
|
||||
register(ctx, 'content', content)
|
||||
register(ctx, 'engine', engine)
|
||||
|
|
|
@ -1,30 +1,53 @@
|
|||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import socket from 'socket.io-serveronly'
|
||||
import http from 'http'
|
||||
import nStatic from 'node-static'
|
||||
import * as casparcg from './casparcg/client.mjs'
|
||||
import nStatic from 'node-static-lib'
|
||||
|
||||
import lowdb from './db.mjs'
|
||||
import config from './config.mjs'
|
||||
import log from './log.mjs'
|
||||
import * as casparcg from './casparcg/client.mjs'
|
||||
import onConnection from './routerio.mjs'
|
||||
|
||||
export function run(config, db, log, core, http, orgPort) {
|
||||
log.info('Server: Opening database db.json')
|
||||
|
||||
lowdb().then(function(db) {
|
||||
const fileServer = new nStatic.Server('./public')
|
||||
db.defaults({
|
||||
graphics: [],
|
||||
presets: [],
|
||||
playing: [],
|
||||
schedule: [],
|
||||
settings: {
|
||||
casparplayhost: 'localhost:3000',
|
||||
casparhost: 'localhost',
|
||||
},
|
||||
version: 1,
|
||||
trash: [],
|
||||
})
|
||||
.write()
|
||||
.then(
|
||||
function() { },
|
||||
function(e) { log.error(e, 'Error writing defaults to lowdb') }
|
||||
)
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const staticRoot = path.join(__dirname,'../public')
|
||||
const fileServer = new nStatic.Server(staticRoot)
|
||||
|
||||
const server = http.createServer(function (req, res) {
|
||||
const child = log.child({})
|
||||
|
||||
const d1 = new Date().getTime()
|
||||
|
||||
let isFinished = false
|
||||
|
||||
var done = function () {
|
||||
if (isFinished) return
|
||||
isFinished = true
|
||||
var requestTime = new Date().getTime() - d1
|
||||
|
||||
let level = 'info'
|
||||
if (res.status >= 400) {
|
||||
let level = 'debug'
|
||||
if (res.statusCode >= 400) {
|
||||
level = 'warn'
|
||||
}
|
||||
if (res.status >= 500) {
|
||||
if (res.statusCode >= 500) {
|
||||
level = 'error'
|
||||
}
|
||||
|
||||
|
@ -45,7 +68,9 @@ lowdb().then(function(db) {
|
|||
|
||||
fileServer.serve(req, res, function (err) {
|
||||
if (err) {
|
||||
log.error(err);
|
||||
if (err.status !== 404) {
|
||||
log.error(err, req.url);
|
||||
}
|
||||
|
||||
res.writeHead(err.status, err.headers);
|
||||
res.end(err.message);
|
||||
|
@ -55,21 +80,20 @@ lowdb().then(function(db) {
|
|||
})
|
||||
|
||||
const io = new socket(server)
|
||||
io.on('connection', onConnection.bind(this, io, db))
|
||||
io.on('connection', onConnection.bind(this, io, db, log))
|
||||
|
||||
casparcg.initialise(log, db, io)
|
||||
|
||||
server.listen(config.get('server:port'), '0.0.0.0', function(err) {
|
||||
let port = orgPort || 3000
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
server.listen(port, '0.0.0.0', function(err) {
|
||||
if (err) {
|
||||
log.fatal(err)
|
||||
return process.exit(2)
|
||||
return reject(err)
|
||||
}
|
||||
log.info(`Server is listening on ${config.get('server:port')}`)
|
||||
log.event.info(`Server is listening on ${port} serving files on ${staticRoot}`)
|
||||
log.info(`Server is listening on ${port} serving files on ${staticRoot}`)
|
||||
resolve()
|
||||
})
|
||||
}, function(e) {
|
||||
log.fatal(e, 'Critical error loading database')
|
||||
process.exit(1)
|
||||
}).catch(function(e) {
|
||||
log.fatal(e, 'Critical error starting server')
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -25,12 +25,12 @@ export async function update(ctx, data) {
|
|||
return
|
||||
}
|
||||
|
||||
await Settings.setValue(data.name, data.value)
|
||||
await ctx.db.set('settings.' + data.name, data.value).write()
|
||||
|
||||
let output = await Settings.getSettings()
|
||||
let output = ctx.db.get('settings').value()
|
||||
ctx.io.emit('settings.all', output)
|
||||
|
||||
if (data.name === 'casparcg') {
|
||||
if (data.name.startsWith('caspar')) {
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
const m = require('mithril')
|
||||
const createModule = require('../common/module')
|
||||
const Module = require('../module')
|
||||
const components = require('../common/components')
|
||||
const socket = require('../../shared/socket')
|
||||
const store = require('../store')
|
||||
|
||||
const Add = createModule({
|
||||
const Add = Module({
|
||||
init: function() {
|
||||
this.monitor('engines', 'engine.all', [])
|
||||
store.listen('graphic.single', data => {
|
||||
if (data.name === this.graphic.name) {
|
||||
m.route.set(`/graphic/${data.id}`)
|
||||
this.engines = []
|
||||
this.graphic = { }
|
||||
this._socketOn(() => this.socketOpen())
|
||||
},
|
||||
|
||||
socketOpen: function() {
|
||||
socket.on('engine.all', (res) => {
|
||||
this.engines = res
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
socket.on('graphic.created', (res) => {
|
||||
if (res.name === this.graphic.name) {
|
||||
m.route.set(`/graphic/${res.id}`)
|
||||
}
|
||||
})
|
||||
this.graphic = { }
|
||||
|
||||
socket.emit('engine.all', {})
|
||||
},
|
||||
|
||||
updated: function(name, control) {
|
||||
|
@ -34,7 +46,7 @@ const Add = createModule({
|
|||
removing: function() {
|
||||
store.unlisten('graphic.single')
|
||||
},
|
||||
}, function() {
|
||||
view: function() {
|
||||
return [
|
||||
m('h4.header', 'Add graphic'),
|
||||
components.error(this.error),
|
||||
|
@ -53,5 +65,6 @@ const Add = createModule({
|
|||
onclick: () => this.create(),
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
module.exports = Add
|
||||
|
|
|
@ -7,10 +7,9 @@ exports.error = function(error) {
|
|||
}
|
||||
|
||||
exports.presetOnlyList = function(module, graphic, title, color = 'green', button = 'Display now', schedule = 'Schedule') {
|
||||
return [
|
||||
m('label.graphic-label', { key: 'first' }, title),
|
||||
return m('div', [
|
||||
m('label.graphic-label', title),
|
||||
m('div.graphic-presetlist', {
|
||||
key: `second-${graphic.id}`,
|
||||
oncreate: control => module.presetlistInit(control),
|
||||
},
|
||||
module.presets.map(item =>
|
||||
|
@ -37,10 +36,9 @@ exports.presetOnlyList = function(module, graphic, title, color = 'green', butto
|
|||
),
|
||||
module.presets.length &&
|
||||
m('button.red.graphic-presetremove', {
|
||||
key: 'third',
|
||||
onclick: () => (module.displayRemove = !module.displayRemove),
|
||||
}, 'Remove entries') || null,
|
||||
]
|
||||
])
|
||||
}
|
||||
|
||||
exports.presetButtons = function(module, green, blue) {
|
||||
|
|
|
@ -105,7 +105,7 @@ exports.settings = function(module, graphic) {
|
|||
m('div.graphic-property', [
|
||||
m('input#graphic-newproperty[type=text]', {
|
||||
value: module.newProperty,
|
||||
oninput: m.withAttr('value', val => (module.newProperty = val)),
|
||||
oninput: (control) => { module.newProperty = control.target.value },
|
||||
}),
|
||||
m('button', {
|
||||
onclick: module.addProperty.bind(module),
|
||||
|
|
|
@ -20,13 +20,15 @@ exports.view = function(module, graphic) {
|
|||
return [
|
||||
m('div.graphic-presetadd', [
|
||||
m('h3.graphic-presetadd-header', 'Create preset/display graphic'),
|
||||
m.fragment(
|
||||
graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [
|
||||
m('label', { for: `preset-add-${index}` }, prop),
|
||||
m(`input#preset-add-${index}[type=text]`, {
|
||||
value: module.current[prop] || '',
|
||||
oninput: module.updated.bind(module, prop, 'current'),
|
||||
}),
|
||||
])),
|
||||
]))
|
||||
),
|
||||
components.presetButtons(module, 'Display live now', 'Add to preset list'),
|
||||
]),
|
||||
components.presetOnlyList(module, graphic, 'Presets'),
|
||||
|
@ -105,7 +107,7 @@ exports.settings = function(module, graphic) {
|
|||
m('div.graphic-property', [
|
||||
m('input#graphic-newproperty[type=text]', {
|
||||
value: module.newProperty,
|
||||
oninput: m.withAttr('value', val => (module.newProperty = val)),
|
||||
oninput: (control) => { module.newProperty = control.target.value },
|
||||
}),
|
||||
m('button', {
|
||||
onclick: module.addProperty.bind(module),
|
||||
|
|
|
@ -1,20 +1,54 @@
|
|||
const m = require('mithril')
|
||||
const createModule = require('./common/module')
|
||||
const Module = require('./module')
|
||||
// const createModule = require('./common/module')
|
||||
const socket = require('../shared/socket')
|
||||
|
||||
const Menu = createModule({
|
||||
const Menu = Module({
|
||||
init: function() {
|
||||
this.monitor('list', 'graphic.all', [])
|
||||
this.monitor('settings', 'settings.all', {})
|
||||
this.monitor('schedule', 'schedule.total', { total: 0 })
|
||||
this.monitor('status', 'casparcg.status', {
|
||||
this.list = []
|
||||
this.settings = {}
|
||||
this.totalSchedule = 0
|
||||
this.status = {
|
||||
connected: false,
|
||||
playing: false,
|
||||
})
|
||||
error: '',
|
||||
}
|
||||
this._socketOn(() => this.socketOpen())
|
||||
this.newHost = ''
|
||||
this.enableEdit = false
|
||||
},
|
||||
|
||||
socketOpen: function() {
|
||||
socket.on('graphic.all', (res) => {
|
||||
this.list = res
|
||||
m.redraw()
|
||||
})
|
||||
socket.on('graphic.created', (res) => {
|
||||
this.list.push(res)
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
this.on('settings.all', (res) => {
|
||||
this.settings = res
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
this.on('schedule.total', (res) => {
|
||||
this.totalSchedule = res.total
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
this.on('casparcg.status', (res) => {
|
||||
this.status = res
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
socket.emit('graphic.all', {})
|
||||
socket.emit('settings.all', {})
|
||||
socket.emit('schedule.total', {})
|
||||
socket.emit('casparcg.status', {})
|
||||
},
|
||||
|
||||
setHost(value) {
|
||||
this.newHost = value
|
||||
this.enableEdit = true
|
||||
|
@ -29,25 +63,22 @@ const Menu = createModule({
|
|||
this.newHost = ''
|
||||
this.enableEdit = false
|
||||
},
|
||||
}, function() {
|
||||
view: function() {
|
||||
return [
|
||||
m('a', {
|
||||
m(m.route.Link, {
|
||||
href: '/',
|
||||
oncreate: m.route.link,
|
||||
class: m.route.get() === '/' && 'active' || '',
|
||||
}, `Schedule (${this.schedule.total})` ),
|
||||
}, `Schedule (${this.totalSchedule})` ),
|
||||
m('h4.header.header--space', 'Graphics'),
|
||||
this.list.map((item) =>
|
||||
m('a', {
|
||||
m(m.route.Link, {
|
||||
href: `/graphic/${item.id}`,
|
||||
oncreate: m.route.link,
|
||||
class: m.route.get() === `/graphic/${item.id}` && 'active' || '',
|
||||
}, item.name)
|
||||
),
|
||||
m('h5.header.header--space', 'Other'),
|
||||
m('a', {
|
||||
m(m.route.Link, {
|
||||
href: '/add',
|
||||
oncreate: m.route.link,
|
||||
class: m.route.get() === '/add' && 'active' || '',
|
||||
}, 'Add graphic' ),
|
||||
m('h5.header.header--space', 'CasparCG Status'),
|
||||
|
@ -65,6 +96,8 @@ const Menu = createModule({
|
|||
m('div.status', {
|
||||
class: this.status.playing && 'green',
|
||||
}, 'playing'),
|
||||
m('div.status-error', { hidden: !this.status.error }, this.status.error)
|
||||
]
|
||||
}
|
||||
})
|
||||
module.exports = Menu
|
||||
|
|
81
app/main/module.js
Normal file
81
app/main/module.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
const defaults = require('../shared/defaults')
|
||||
const socket = require('../shared/socket')
|
||||
|
||||
// https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript/4819886#4819886
|
||||
// LOL
|
||||
function is_touch_device() {
|
||||
var prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
|
||||
var mq = function(query) {
|
||||
return window.matchMedia(query).matches
|
||||
}
|
||||
|
||||
if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
|
||||
return true
|
||||
}
|
||||
|
||||
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
|
||||
// https://git.io/vznFH
|
||||
var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('')
|
||||
return mq(query)
|
||||
}
|
||||
|
||||
module.exports = function Module(module) {
|
||||
return defaults(module, {
|
||||
init: function() {},
|
||||
|
||||
oninit: function(vnode) {
|
||||
this._listeners = []
|
||||
this.init(vnode)
|
||||
},
|
||||
|
||||
_listeners: null,
|
||||
|
||||
_socketOn: function(cb) {
|
||||
socket.on('connect', () => cb())
|
||||
|
||||
if (socket.connected) {
|
||||
cb()
|
||||
}
|
||||
},
|
||||
|
||||
_initDragula: function(control, cb) {
|
||||
let dragContainer = document.getElementById('dragcontainer')
|
||||
let out = dragula([control.dom], {
|
||||
mirrorContainer: dragContainer,
|
||||
invalid: el => el.className !== 'graphic-preset-reorder'
|
||||
&& el.className !== 'graphic-preset',
|
||||
})
|
||||
out.on('dragend', () => {
|
||||
if (is_touch_device()) {
|
||||
document.body.style.cssText = ''
|
||||
window.scroll(0, document.body.data)
|
||||
}
|
||||
})
|
||||
out.on('drag', () => {
|
||||
if (is_touch_device()) {
|
||||
document.body.data = window.scrollY
|
||||
document.body.style.cssText = `position: fixed; left: 0; right: 0; overflow: hidden; top: -${window.scrollY}px;`
|
||||
dragContainer.style.marginTop = `${document.body.data}px`
|
||||
}
|
||||
})
|
||||
out.on('drop', (a, b, c, d) => {
|
||||
cb(a, d)
|
||||
})
|
||||
},
|
||||
|
||||
on: function(name, cb) {
|
||||
this._listeners.push([name, cb])
|
||||
socket.on(name, cb)
|
||||
},
|
||||
|
||||
remove: function() {},
|
||||
|
||||
onremove: function() {
|
||||
this.remove()
|
||||
if (!this._listeners) return
|
||||
for (let i = 0; i < this._listeners.length; i++) {
|
||||
socket.removeListener(this._listeners[0], this._listeners[1])
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
34
app/shared/defaults.js
Normal file
34
app/shared/defaults.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
|
||||
// taken from isobject npm library
|
||||
function isObject(val) {
|
||||
return val != null && typeof val === 'object' && Array.isArray(val) === false
|
||||
}
|
||||
|
||||
module.exports = function defaults(options, def) {
|
||||
let out = { }
|
||||
|
||||
if (options) {
|
||||
Object.keys(options || {}).forEach(key => {
|
||||
out[key] = options[key]
|
||||
|
||||
if (Array.isArray(out[key])) {
|
||||
out[key] = out[key].map(item => {
|
||||
if (isObject(item)) return defaults(item)
|
||||
return item
|
||||
})
|
||||
} else if (out[key] && typeof out[key] === 'object') {
|
||||
out[key] = defaults(options[key], def && def[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (def) {
|
||||
Object.keys(def).forEach(function(key) {
|
||||
if (typeof out[key] === 'undefined') {
|
||||
out[key] = def[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
9
config.json
Normal file
9
config.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "caspar-sup",
|
||||
"serviceName": "Caspar Sup",
|
||||
"description": "Caspar Sup manager",
|
||||
"port": 3000,
|
||||
"managePort": 3001,
|
||||
"appRepository": null,
|
||||
"manageRepository": null
|
||||
}
|
12
index.mjs
12
index.mjs
|
@ -1,9 +1,5 @@
|
|||
import log from './api/log.mjs'
|
||||
|
||||
// Run the database script automatically.
|
||||
log.info('Starting server.')
|
||||
|
||||
import('./api/server.mjs').catch((error) => {
|
||||
log.error(error, 'Error while starting server')
|
||||
process.exit(1)
|
||||
export function start(config, db, log, core, http, port) {
|
||||
return import('./api/server.mjs').then(function(module) {
|
||||
return module.run(config, db, log, core, http, port)
|
||||
})
|
||||
}
|
||||
|
|
23
package.json
23
package.json
|
@ -9,14 +9,10 @@
|
|||
"js:build:client": "asbundle app/client/index.js public/client.js",
|
||||
"js:build:status": "asbundle app/status/index.js public/status.js",
|
||||
"js:watch": "nodemon --watch app --exec \"npm run build\"",
|
||||
"start:watch": "nodemon --experimental-modules --watch api index.mjs | bunyan -o short",
|
||||
"start:watch": "nodemon --watch api --watch runner.mjs --watch index.mjs runner.mjs | bunyan",
|
||||
"start": "node --experimental-modules index.mjs | bunyan -o short",
|
||||
"start:win": "node --experimental-modules index.mjs | bunyan -o short",
|
||||
"dev": "run-p js:watch start:watch",
|
||||
"build": "run-p js:build:main js:build:client js:build:status",
|
||||
"docker": "docker run -it --rm --name my-running-script -p 3000:3000 -v \"%cd%\":/usr/src/app -w /usr/src/app node:alpine",
|
||||
"docker:install": "npm run docker -- npm install",
|
||||
"docker:dev": "npm run docker -- npm run dev"
|
||||
"build": "npm run js:build:main && npm run js:build:client && npm run js:build:status"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -34,21 +30,16 @@
|
|||
},
|
||||
"homepage": "https://github.com/nfp-projects/caspar-sup#readme",
|
||||
"dependencies": {
|
||||
"bunyan-lite": "^1.0.1",
|
||||
"casparcg-connection": "4.9.0",
|
||||
"lodash": "^4.5.0",
|
||||
"lowdb": "^1.0.0",
|
||||
"nconf": "^0.9.1",
|
||||
"node-static": "^0.7.11",
|
||||
"socket.io-serveronly": "^2.3.0",
|
||||
"tslib": "^1.11.1"
|
||||
"node-static-lib": "^1.0.0",
|
||||
"p3x-xml2json": "^2020.10.131",
|
||||
"socket.io-serveronly": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"asbundle": "^2.6.1",
|
||||
"dragula": "^3.7.2",
|
||||
"mithril": "^1.1.5",
|
||||
"nodemon": "^2.0.2",
|
||||
"mithril": "^2.0.4",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"run-p": "0.0.0"
|
||||
"service-core": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,6 +149,12 @@ nav .status {
|
|||
position: relative;
|
||||
margin-left: 1.8em;
|
||||
}
|
||||
nav .status-error {
|
||||
color: red;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
nav .status::after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
|
10
runner.mjs
Normal file
10
runner.mjs
Normal file
|
@ -0,0 +1,10 @@
|
|||
import ServiceCore from 'service-core'
|
||||
import * as server from './index.mjs'
|
||||
|
||||
const serviceCore = new ServiceCore('sc-manager', import.meta.url)
|
||||
|
||||
serviceCore.init(server)
|
||||
.then(function() {})
|
||||
.catch(function(err) {
|
||||
serviceCore.log.error(err, 'Unknown error starting server')
|
||||
})
|
Loading…
Reference in a new issue