filmout_manager/src/index.ts

486 lines
13 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 { sendMail } from './mail';
import { Files } from './files';
import type { SequenceObject, VideoObject, ImageObject } from './files';
import { Shell } from './shell';
import { delay } from './delay';
import { TestImage } from './testimage';
import { FD } 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 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 (typeof process.env['FD'] === 'undefined') {
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 (typeof process.env['FFMPEG'] === 'undefined') {
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 (typeof process.env['WIDTH'] === 'undefined') {
log.error('Please include a WIDTH value containing the width of the screen you are using in .env');
process.exit(2);
} else {
width = parseInt(process.env['WIDTH']);
log.info(`WIDTH=${width}`);
}
if (typeof process.env['HEIGHT'] === 'undefined') {
log.error('Please include a HEIGHT value containing the height of the screen you are using in .env');
process.exit(3);
} else {
height = parseInt(process.env['HEIGHT'])
log.info(`HEIGHT=${height}`);
}
if (typeof process.env['FD_HOST'] === 'undefined') {
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 (typeof process.env['FD_PORT'] === 'undefined') {
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 (typeof process.env['PORT'] === 'undefined') {
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 = parseInt(process.env['PORT']);
log.info(`PORT=${port}`);
}
if (typeof process.env['WS_PORT'] === 'undefined') {
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 = parseInt(process.env['WS_PORT']);
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 (typeof process.env['SEQUENCES'] === 'undefined') {
log.error('Please include a SEQUENCES directory where the image sequences will be located in .env');
process.exit(7);
} else {
sequences = process.env['SEQUENCES'];
sequencesExists = await Files.exists(sequences);
if (!sequencesExists) {
log.error(`The SEQUENCES directory in .env, ${sequences}, does not exist`);
process.exit(8);
}
log.info(`SEQUENCES=${sequences}`);
}
if (typeof process.env['VIDEOS'] === 'undefined') {
log.error('Please include a VIDEOS directory where the videos will be located in .env');
process.exit(7);
} else {
videos = process.env['VIDEOS'];
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 (typeof process.env['MOCK'] !== 'undefined') {
if (process.env['MOCK'].trim().toLowerCase() === "true" || process.env['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' :
frameAdvance();
break;
case 'rewind' :
frameRewind();
break;
case 'set' :
frameSet(msg.state.sequence.current);
break;
case 'exposure' :
exposureSet(msg.state.exposure);
break;
case 'focus' :
await focus();
break;
case 'framing' :
await framing();
break;
case 'offset' :
offset(msg);
break;
case 'size' :
size(msg);
break;
case 'scale' :
scale(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' });
}
function frameAdvance () {
focusImage = null;
sequence.frameAdvance();
}
function frameRewind () {
focusImage = null;
sequence.frameRewind();
}
function frameSet (frame : number) {
focusImage = null;
sequence.frameSet(frame);
}
function exposureSet (exposure : number) {
if (exposure < 1) {
exposure = 1;
}
sequence.setExposure(exposure);
}
async function select (id : string) : Promise<boolean> {
const sequencesArr : SequenceObject[] = await Files.enumerateSequences(sequences);
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 start () {
if (focusImage !== null) {
await stopFocus();
}
if (framingImage !== null) {
await stopFraming();
}
sequence.start();
}
function stop () {
sequence.stop();
}
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;
if (focusImage !== null) {
await stopFocus();
return;
}
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);
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;
if (framingImage !== null) {
await stopFraming();
return;
}
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);
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(sequences);
//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);
if (focusImage !== null) {
try {
data = await image.thumbnail(focusImage, width, height);
log.info(`Image: ${current.path} - ${width},${height}`);
} catch (err) {
log.error(`Error getting thumbnail of ${current}`, err);
return next(new Error('Error getting thumbnail'));
}
} else if (framingImage !== null) {
try {
data = await image.thumbnail(framingImage, width, height);
log.info(`Image: ${current.path} - ${width},${height}`);
} catch (err) {
log.error(`Error getting thumbnail of ${current}`, err);
return next(new Error('Error getting thumbnail'));
}
} else if (current !== null) {
try {
data = await image.thumbnail(current.path, width, height);
log.info(`Image: ${current.path} - ${width},${height}`);
} catch (err) {
log.error(`Error getting thumbnail of ${current}`, err);
return next(new Error('Error getting thumbnail'));
}
} else {
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'));
}
}
res.contentType('image/jpeg');
res.send(data);
});
async function main () {
await settings();
index = await createTemplate('./views/index.hbs');
ffmpeg = new FFMPEG(process.env['FFMPEG']);
ffprobe = new FFPROBE();
image = new Image();
camera = new Camera(mock);
display = new Display(width, height);
fd = new FD(process.env['FD'], width, height, process.env['FD_HOST'], parseInt(process.env['FD_PORT']), 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, send);
setInterval(keepAlive, 30000);
}
main();