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; 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> { 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 { 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();