578 lines
15 KiB
TypeScript
578 lines
15 KiB
TypeScript
import 'dotenv/config'
|
|
import express from 'express';
|
|
import { Express, Request, Response, NextFunction } from 'express'
|
|
import fs from 'fs/promises';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
import { Database } from 'sqlite3';
|
|
import bodyParser from 'body-parser';
|
|
import multer from 'multer';
|
|
import { v4 as uuid } from 'uuid';
|
|
import getType from 'mime';
|
|
import type { Logger } from 'winston';
|
|
import * as Handlebars from 'handlebars';
|
|
import { Server } from 'ws';
|
|
import type { WebSocket } from 'ws';
|
|
|
|
import { createLog } from './log';
|
|
import { envString, envInt } from './env';
|
|
import { sendMail } from './mail';
|
|
import { Files } from './files';
|
|
import type { SequenceObject, VideoObject, ImageObject } from './files';
|
|
import { delay } from './delay';
|
|
import { TestImage } from './testimage';
|
|
import { FD, Mode, Action} from './fd';
|
|
import type { fdOutgoingPosition } from './fd';
|
|
import { Display, Dimensions } from './display';
|
|
import { FFMPEG } from './ffmpeg';
|
|
import { FFPROBE } from './ffprobe';
|
|
import { Camera } from './camera';
|
|
import { Sequence } from './sequence';
|
|
import { Image } from './image';
|
|
|
|
let mock : boolean = false;
|
|
const log : Logger = createLog('fm');
|
|
const app : Express = express();
|
|
let wss : Server;
|
|
let fd : FD;
|
|
let display : Display;
|
|
let ffmpeg : FFMPEG;
|
|
let ffprobe : FFPROBE;
|
|
let image : Image;
|
|
let camera : Camera;
|
|
let sequence : Sequence;
|
|
let index : HandlebarsTemplateDelegate<any>;
|
|
let focusImage : string = null;
|
|
let framingImage : string = null;
|
|
let blankImage : string = null;
|
|
let grayImage : string = null;
|
|
|
|
let port : number;
|
|
let wsPort : number;
|
|
let sequences : string;
|
|
let videos : string;
|
|
let width : number;
|
|
let height : number;
|
|
|
|
log.info('Starting filmout_manager...');
|
|
|
|
app.use(bodyParser.json());
|
|
app.use(bodyParser.urlencoded({ extended: true }));
|
|
|
|
app.use('/static', express.static('./static'));
|
|
|
|
interface WebSocketExtended extends WebSocket {
|
|
ip? : string,
|
|
session? : string
|
|
}
|
|
|
|
async function createTemplate (filePath : string) : Promise<HandlebarsTemplateDelegate<any>> {
|
|
let tmpl : string;
|
|
try {
|
|
tmpl = await fs.readFile(filePath, 'utf8');
|
|
} catch (err) {
|
|
log.error(err);
|
|
return null
|
|
}
|
|
return Handlebars.compile(tmpl);
|
|
}
|
|
|
|
async function settings () {
|
|
let sequencesExists : boolean = false;
|
|
let videosExists : boolean = false;
|
|
if (envString('FFMPEG', null) === null) {
|
|
log.error('Please include an FFMPEG value containing the path to your ffmpeg binary in .env');
|
|
process.exit(1);
|
|
} else {
|
|
log.info(`FFMPEG=${process.env['FFMPEG']}`);
|
|
}
|
|
if (envString('FD', null) === null) {
|
|
log.error('Please include an FD value containing the path to your fd binary in .env');
|
|
process.exit(1);
|
|
} else {
|
|
log.info(`FD=${process.env['FD']}`);
|
|
}
|
|
if (envString('FD_DISPLAY', null) !== null) {
|
|
log.info(`FD_DISPLAY=${envString('FD_DISPLAY', null)}`);
|
|
}
|
|
if (envString('FD_HOST', null) === null) {
|
|
log.error('Please include a FD_HOST value with the host that the fd socket server is hosted on in .env');
|
|
process.exit(4);
|
|
} else {
|
|
log.info(`FD_HOST=${process.env['FD_HOST']}`);
|
|
}
|
|
if (envInt('FD_PORT', null) === null) {
|
|
log.error('Please include a FD_PORT value with the port that the fd socket server is hosted on in .env')
|
|
process.exit(5);
|
|
} else {
|
|
log.info(`FD_PORT=${process.env['FD_PORT']}`);
|
|
}
|
|
if (envString('FI', null) === null) {
|
|
log.error('Please include an FI value containing the path to your fi binary in .env');
|
|
process.exit(1);
|
|
} else {
|
|
log.info(`FI=${process.env['FI']}`);
|
|
}
|
|
if (envInt('WIDTH', null) === null) {
|
|
log.error('Please include a WIDTH value containing the width of the screen you are using in .env');
|
|
process.exit(2);
|
|
} else {
|
|
width = envInt('WIDTH', 0);
|
|
log.info(`WIDTH=${width}`);
|
|
}
|
|
if (envInt('HEIGHT', null) === null) {
|
|
log.error('Please include a HEIGHT value containing the height of the screen you are using in .env');
|
|
process.exit(3);
|
|
} else {
|
|
height = envInt('HEIGHT', 0);
|
|
log.info(`HEIGHT=${height}`);
|
|
}
|
|
if (envInt('PORT', null) === null) {
|
|
log.error('Please include a PORT value with the port that the HTTP web process is hosted on in .env');
|
|
process.exit(6);
|
|
} else {
|
|
port = envInt('PORT', 8080);
|
|
log.info(`PORT=${port}`);
|
|
}
|
|
if (envInt('WS_PORT', null) === null) {
|
|
log.error('Please include a WSPORT value with the port that the WebSocket web process is hosted on in .env');
|
|
process.exit(6);
|
|
} else {
|
|
wsPort = envInt('WS_PORT', 8081);
|
|
log.info(`WS_PORT=${wsPort}`);
|
|
}
|
|
if (wsPort === port) {
|
|
log.error(`Websocket port (${wsPort}) should not be the same as HTTP port (${port})`);
|
|
process.exit(7);
|
|
}
|
|
|
|
if (envString('SEQUENCES', null) === null) {
|
|
log.error('Please include a SEQUENCES directory where the image sequences will be located in .env');
|
|
process.exit(7);
|
|
} else {
|
|
sequences = envString('SEQUENCES', null);
|
|
sequencesExists = await Files.init(sequences);
|
|
if (!sequencesExists) {
|
|
log.error(`The SEQUENCES directory in .env, ${sequences}, does not exist`);
|
|
process.exit(8);
|
|
}
|
|
log.info(`SEQUENCES=${sequences}`);
|
|
}
|
|
if (envString('VIDEOS', null) === null) {
|
|
log.error('Please include a VIDEOS directory where the videos will be located in .env');
|
|
process.exit(7);
|
|
} else {
|
|
videos = envString('VIDEOS', null);
|
|
videosExists = await Files.exists(videos);
|
|
if (!sequencesExists) {
|
|
log.error(`The VIDEOS directory in .env, ${videos}, does not exist`);
|
|
process.exit(8);
|
|
}
|
|
log.info(`VIDEOS=${videos}`);
|
|
}
|
|
if (envString('MOCK', null) !== null) {
|
|
if (envString('MOCK', '').trim().toLowerCase() === "true" || envString('MOCK', '').trim() === '1') {
|
|
mock = true;
|
|
log.info(`MOCK=true`);
|
|
} else {
|
|
mock = false;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function onWssConnection (ws : WebSocketExtended, req : Request) {
|
|
let ip : string = req.headers['x-forwarded-for'] as string || req.connection.remoteAddress;
|
|
if (ip.substr(0, 7) === "::ffff:") ip = ip.substr(7)
|
|
log.info(`Client ${ip} connected to WebSocket server`);
|
|
ws.ip = ip;
|
|
ws.session = uuid();
|
|
ws.on('message', function (data) { onClientMessage(data, ws) });
|
|
sequence.updateClientsOnLoad();
|
|
}
|
|
|
|
async function onClientMessage (data : any, ws : WebSocket) {
|
|
let msg : Message = null;
|
|
try {
|
|
msg = JSON.parse(data);
|
|
} catch (err) {
|
|
log.error('Error parsing message', err);
|
|
}
|
|
if (msg !== null && typeof msg.cmd !== 'undefined') {
|
|
await cmd(msg);
|
|
}
|
|
}
|
|
|
|
async function cmd (msg : Message) {
|
|
let success : boolean = false
|
|
switch(msg.cmd) {
|
|
case 'pong' :
|
|
//received keepalive
|
|
break;
|
|
case 'open' :
|
|
await cameraOpen();
|
|
break;
|
|
case 'close' :
|
|
await cameraClose();
|
|
break;
|
|
case 'select' :
|
|
await select(msg.state.sequence.hash);
|
|
break;
|
|
case 'start' :
|
|
await start();
|
|
break;
|
|
case 'stop' :
|
|
stop();
|
|
break;
|
|
case 'advance' :
|
|
await frameAdvance();
|
|
break;
|
|
case 'rewind' :
|
|
await frameRewind();
|
|
break;
|
|
case 'set' :
|
|
await frameSet(msg.state.sequence.current);
|
|
break;
|
|
case 'exposure' :
|
|
exposureSet(msg.state.exposure);
|
|
break;
|
|
case 'exposures' :
|
|
exposuresSet(msg.state.exposures);
|
|
break;
|
|
case 'focus' :
|
|
await focus();
|
|
break;
|
|
case 'framing' :
|
|
await framing();
|
|
break;
|
|
case 'blank' :
|
|
await blank();
|
|
break;
|
|
case 'offset' :
|
|
offset(msg);
|
|
break;
|
|
case 'size' :
|
|
size(msg);
|
|
break;
|
|
case 'scale' :
|
|
scale(msg);
|
|
break;
|
|
case 'mode' :
|
|
modeSet(msg);
|
|
break;
|
|
default :
|
|
log.warn(`No matching command: ${msg.cmd}`);
|
|
}
|
|
}
|
|
|
|
async function cameraOpen () {
|
|
await camera.open();
|
|
send({ cmd : 'open' });
|
|
}
|
|
|
|
async function cameraClose () {
|
|
await camera.close();
|
|
send({ cmd : 'close' });
|
|
}
|
|
|
|
async function frameAdvance () {
|
|
await reset('advance');
|
|
sequence.frameAdvance();
|
|
}
|
|
|
|
async function frameRewind () {
|
|
await reset('rewind');
|
|
sequence.frameRewind();
|
|
}
|
|
|
|
async function frameSet (frame : number) {
|
|
await reset('set');
|
|
sequence.frameSet(frame);
|
|
}
|
|
|
|
function exposureSet (exposure : number) {
|
|
if (exposure < 1) {
|
|
exposure = 1;
|
|
}
|
|
sequence.setExposure(exposure);
|
|
}
|
|
|
|
function exposuresSet (exposures : number[]) {
|
|
const r : number = exposures[0] > 0 ? exposures[0] : 1;
|
|
const g : number = exposures[1] > 0 ? exposures[1] : 1;
|
|
const b : number = exposures[2] > 0 ? exposures[2] : 1;
|
|
sequence.setExposures(r, g, b);
|
|
}
|
|
|
|
function modeSet (msg : Message) {
|
|
sequence.setMode(msg.mode);
|
|
}
|
|
|
|
async function select (id : string) : Promise<boolean> {
|
|
const sequencesArr : SequenceObject[] = await Files.enumerateSequences();
|
|
const seq : SequenceObject = sequencesArr.find(el => el.hash === id);
|
|
if (typeof seq == 'undefined' || seq == null) {
|
|
log.error('Sequence not found, maybe deleted?', new Error(`Cannot find sequence ${id}`));
|
|
return false;
|
|
}
|
|
await sequence.load(seq);
|
|
return true;
|
|
}
|
|
|
|
async function reset (target : string = 'none') {
|
|
if (target !== 'focus' && focusImage !== null) {
|
|
await stopFocus();
|
|
}
|
|
|
|
if (target !== 'framing' && framingImage !== null) {
|
|
await stopFraming();
|
|
}
|
|
|
|
if (target !== 'blank' && blank !== null) {
|
|
blankImage = null;
|
|
}
|
|
|
|
if (target !== 'gray' && blank !== null) {
|
|
grayImage = null;
|
|
}
|
|
}
|
|
|
|
async function start () {
|
|
await reset()
|
|
sequence.start();
|
|
}
|
|
|
|
function stop () {
|
|
sequence.stop();
|
|
}
|
|
|
|
async function blank () {
|
|
await reset('blank');
|
|
blankImage = "SET";
|
|
await send({ cmd : 'blank' });
|
|
}
|
|
|
|
async function send (msg : Message) {
|
|
const msgStr : string = JSON.stringify(msg);
|
|
wss.clients.forEach((client : WebSocket ) => {
|
|
client.send(msgStr);
|
|
});
|
|
}
|
|
|
|
async function keepAlive () {
|
|
await send({ cmd : 'ping' });
|
|
}
|
|
|
|
async function focus () {
|
|
let pos : fdOutgoingPosition;
|
|
let dims : Dimensions;
|
|
let state : State;
|
|
let filePath : string;
|
|
|
|
await reset('focus');
|
|
|
|
if (sequence.isLoaded()) {
|
|
state = sequence.getState();
|
|
pos = {
|
|
w : state.display.width,
|
|
h : state.display.height,
|
|
x : state.offset.x,
|
|
y : state.offset.y
|
|
}
|
|
} else {
|
|
dims = display.getScreen();
|
|
pos = {
|
|
w : dims.width,
|
|
h : dims.height,
|
|
x : 0,
|
|
y : 0
|
|
}
|
|
}
|
|
|
|
focusImage = await TestImage.Focus(pos.w, pos.h);
|
|
|
|
fd.setMode(Mode.RGB); // this never changes
|
|
await fd.load (focusImage, pos.x, pos.y, pos.w, pos.h);
|
|
await fd.display(focusImage);
|
|
send({ cmd : 'focus' });
|
|
}
|
|
|
|
async function stopFocus () {
|
|
focusImage = null;
|
|
await fd.stop(focusImage);
|
|
send({ cmd : 'unfocus' });
|
|
}
|
|
|
|
async function framing () {
|
|
let pos : fdOutgoingPosition;
|
|
let dims : Dimensions;
|
|
let state : State;
|
|
let filePath : string;
|
|
|
|
await reset('framing');
|
|
|
|
if (sequence.isLoaded()) {
|
|
state = sequence.getState();
|
|
pos = {
|
|
w : state.display.width,
|
|
h : state.display.height,
|
|
x : state.offset.x,
|
|
y : state.offset.y
|
|
}
|
|
} else {
|
|
dims = display.getScreen();
|
|
pos = {
|
|
w : dims.width,
|
|
h : dims.height,
|
|
x : 0,
|
|
y : 0
|
|
}
|
|
}
|
|
framingImage = await TestImage.Frame(pos.w, pos.h);
|
|
|
|
fd.setMode(Mode.RGB);
|
|
|
|
await fd.load (framingImage, pos.x, pos.y, pos.w, pos.h);
|
|
await fd.display(framingImage);
|
|
send({ cmd : 'framing' });
|
|
}
|
|
|
|
async function stopFraming () {
|
|
framingImage = null;
|
|
await fd.stop(framingImage);
|
|
send({ cmd : 'unframing' });
|
|
}
|
|
|
|
function offset (msg : Message) {
|
|
let current : ImageObject = sequence.getCurrent();
|
|
if (current !== null) {
|
|
sequence.updateOffset(msg.x, msg.y);
|
|
}
|
|
}
|
|
|
|
function size (msg : Message) {
|
|
let current : ImageObject = sequence.getCurrent();
|
|
if (current !== null) {
|
|
sequence.updateSize(msg.width, msg.height);
|
|
}
|
|
}
|
|
|
|
function scale (msg : Message) {
|
|
let current : ImageObject = sequence.getCurrent();
|
|
if (current !== null) {
|
|
sequence.updateScale(msg.scale);
|
|
}
|
|
}
|
|
|
|
app.get('/', async (req : Request, res : Response, next : NextFunction) => {
|
|
const sequencesArr : SequenceObject[] = await Files.enumerateSequences();
|
|
//const videosArr : VideoObject[] = await Files.enumerateVideos(videos);
|
|
const html : string = index({ sequences : sequencesArr, width, height, wsPort });
|
|
res.send(html);
|
|
});
|
|
|
|
app.get('/:width/:height/image.jpg', async (req : Request, res : Response, next : NextFunction) => {
|
|
let data : Buffer;
|
|
let current : ImageObject = sequence.getCurrent();
|
|
let width : number = parseInt(req.params.width);
|
|
let height : number = parseInt(req.params.height);
|
|
//log.info(`${width}x${height}`)
|
|
if (focusImage !== null) {
|
|
try {
|
|
data = await image.thumbnail(focusImage, width, height);
|
|
log.info(`Focus Image: ${focusImage} - ${width},${height}`);
|
|
} catch (err) {
|
|
log.error(`Error getting thumbnail of focusImage`, err);
|
|
return next(new Error('Error getting thumbnail'));
|
|
}
|
|
} else if (framingImage !== null) {
|
|
try {
|
|
data = await image.thumbnail(framingImage, width, height);
|
|
log.info(`Framing Image: ${framingImage} - ${width},${height}`);
|
|
} catch (err) {
|
|
log.error(`Error getting thumbnail of framingImage`, err);
|
|
return next(new Error('Error getting thumbnail'));
|
|
}
|
|
} else if (blankImage !== null) {
|
|
try {
|
|
data = await image.blank(width, height);
|
|
log.info(`Blank - ${width},${height}`);
|
|
} catch (err) {
|
|
log.error('Error generating blank image', err);
|
|
return next(new Error('Error generating blank image'));
|
|
}
|
|
} else if (grayImage !== null) {
|
|
try {
|
|
data = await image.gray(width, height);
|
|
log.info(`Gray - ${width},${height}`);
|
|
} catch (err) {
|
|
log.error('Error generating gray image', err);
|
|
return next(new Error('Error generating gray image'));
|
|
}
|
|
}else if (current !== null) {
|
|
try {
|
|
data = await image.thumbnail(current.path, width, height);
|
|
log.info(`Frame Image: ${current.path} - ${width},${height}`);
|
|
} catch (err) {
|
|
log.error(`Error getting thumbnail of frame ${current}`, err);
|
|
return next(new Error('Error getting thumbnail'));
|
|
}
|
|
}
|
|
res.contentType('image/jpeg');
|
|
res.send(data);
|
|
});
|
|
|
|
async function main () {
|
|
await settings();
|
|
index = await createTemplate('./views/index.hbs');
|
|
ffmpeg = new FFMPEG(envString('FFMPEG', 'ffmpeg'));
|
|
ffprobe = new FFPROBE();
|
|
image = new Image(envString('FI', 'fi'));
|
|
camera = new Camera(mock);
|
|
display = new Display(width, height);
|
|
fd = new FD(envString('FD', 'fd'), width, height, envString('FD_HOST', 'localhost'), envInt('FD_PORT', 8082), envString('FD_DISPLAY', null), mock);
|
|
|
|
app.listen(port, async () => {
|
|
log.info(`filmout_manager HTTP server running on port ${port}`);
|
|
});
|
|
|
|
wss = new Server({ port : wsPort, clientTracking : true });
|
|
wss.on('connection', onWssConnection);
|
|
log.info(`filmout_manager WebSocket server running on port ${wsPort}`);
|
|
|
|
//ffmpeg.listFormats();
|
|
//log.info(await TestImage.Focus(640, 480));
|
|
sequence = new Sequence(camera, fd, display, ffprobe, image, send);
|
|
setInterval(keepAlive, 30000);
|
|
}
|
|
|
|
|
|
main();
|
|
|
|
process.stdin.resume(); // so the program will not close instantly
|
|
|
|
async function exitHandler(options : any, exitCode : string) {
|
|
if (options.cleanup) {
|
|
log.info(`Cleaning up...`);
|
|
try {
|
|
await fd.exit();
|
|
} catch (err) {
|
|
log.error('Error cleanly exiting filmout_display (fd) executable', err);
|
|
}
|
|
}
|
|
exitCode == 'SIGINT' ? log.info(`exit: ${exitCode}`) : log.error(`exit: ${exitCode}`, new Error(`Exited with non-zero code: "${exitCode}"`));
|
|
if (options.exit) process.exit();
|
|
}
|
|
|
|
// do something when app is closing
|
|
process.on('exit', exitHandler.bind(null,{cleanup:true}));
|
|
|
|
// catches ctrl+c event
|
|
process.on('SIGINT', exitHandler.bind(null, {exit:true}));
|
|
|
|
// catches "kill pid" (for example: nodemon restart)
|
|
process.on('SIGUSR1', exitHandler.bind(null, {exit:true}));
|
|
process.on('SIGUSR2', exitHandler.bind(null, {exit:true}));
|
|
|
|
// catches uncaught exceptions
|
|
process.on('uncaughtException', exitHandler.bind(null, {exit:true})); |