UNPKG

pistreamer

Version:

NodeJS module that allows streaming the raspberry pi camera module output over websocket to a webpage using a modified version of 131/h264-live-player.

247 lines (217 loc) 8.91 kB
import ws from 'ws'; import merge from 'lodash.merge'; import {spawn, ChildProcessWithoutNullStreams} from 'child_process'; import fs from 'fs'; import http from 'http'; import {join} from 'path'; import stream from 'stream'; import winston from 'winston'; import {logger} from './Notifications' import { StreamOptions } from './interfaces/StreamOptions'; import { ImageEffects } from './enums/ImageEffects'; import { VideoOptions } from './interfaces/VideoOptions'; const Splitter = require('stream-split'); export class PiStreamServer { /** * Static logger of PiStreamer. You can change the logger by giving * a winston.Logger object. * ```ts * PiStreamServer.log = winston.createLogger(...); * ``` */ static log: winston.Logger = logger; private buffer: Buffer = Buffer.from([0,0,0,1]); private streamer?: ChildProcessWithoutNullStreams | null; private readStream?: stream.Readable | null; private streamClients: Array<ws>; private wsServer: ws.Server; private options: StreamOptions; private readonly defaultOptions: StreamOptions = { videoOptions: { height: 240, width: 480, framerate: 12, imxfx: ImageEffects.none, brightness: 50, saturation: 50, sharpness: 50, contrast: 50 }, dynamic: true, limit: 0 } /** * PiStreamServer constructor. * @param wsServer - Instance of a websocket server. * @param options - Options of the stream. */ constructor(wsServer: ws.Server, options?: StreamOptions) { this.streamClients = []; this.options = merge(this.defaultOptions, options); this.wsServer = wsServer; this.wsServer.on('connection', this.newClient); this.setOptions.bind(this); } /** * Set the options of the stream. * @param options */ public setOptions(options: StreamOptions) { this.options = merge(this.defaultOptions, options); } /** * Stop the feed and kill the raspivid process. */ protected stopFeed = () => { process.kill(-this.streamer!.pid); this.streamer = null; this.readStream = null; } /** * Start the feed by creating one if there's none or by continuing the existant one. */ protected startFeed = () => { if(this.readStream == null || this.readStream == undefined) this.createFeed(); var rStream = this.streamer!.stdout; rStream = rStream.pipe(new Splitter(this.buffer)); rStream.on('data', this.broadcast); this.readStream! = rStream; } /** * Create a new feed by starting a new raspivid process. */ protected createFeed = () => { var opts: Array<any> = ['-t', '0', '-o', '-', '-pf', 'baseline']; var opt: keyof VideoOptions; for(opt in this.options.videoOptions) { var current = this.options.videoOptions![opt]; opts.push(`--${opt.toLowerCase()}`); if(opt == "imxfx") opts.push(ImageEffects[<number>current]) else if(opt != "vFlip" && opt != "hFlip") opts.push(current); } PiStreamServer.log.info(`Start of stream !`); this.streamer = spawn('raspivid', opts, {detached: true}); this.streamer.on('error', (error) => { PiStreamServer.log.error(error.message); }) this.streamer!.on('exit', (code) => { var msg = (code === null)? 'Stream Exit' : `Failure code ${code}`; PiStreamServer.log.log((code === null)? 'info': 'error', msg); }); } /** * Broadcast the feed to all the websocket client connected. * @param data - Video stream. */ protected broadcast = (data: any) => { this.streamClients.forEach((socket: any) => { if(socket.buzy) return; socket.buzy = true; socket.buzy = false; socket.send(Buffer.concat([this.buffer, data]), { binary: true}, function ack(error: any) { socket.buzy = false; }); }); } /** * Actions done when a new client is connected. * @param socket - Client socket. */ protected newClient = (socket: ws) => { var userLimit = (this.options.limit! > 0)? this.options.limit! : 0; var condition = (userLimit == 0)? true : (this.wsServer.clients.size <= userLimit); var self = this; if(condition) { this.streamClients.push(socket); PiStreamServer.log.info(`Someone just connected ! (${self.wsServer.clients.size} user(s) online.)`); socket.send(JSON.stringify({ action: "init", width: self.options.videoOptions!.width, height: self.options.videoOptions!.height })); socket.on('close', () => { PiStreamServer.log.info(`Someone just left. (${self.wsServer.clients.size} user(s) online.)`); if(self.streamer != null) self.readStream!.destroy(); if(self.options.dynamic) { if(self.wsServer.clients.size == 0 && self.streamer != null) self.stopFeed(); } }); socket.on("message", (data: any) => { var cmd = "" + data, action = data.split(' ')[0]; var validAction: boolean = true; //All of these actions are executed for all the connected users ! try { switch(action){ //Start the stream case "REQUESTSTREAM": if(self.streamer != null && self.streamer != undefined) throw "A stream already exists."; self.startFeed(); break; //Stop the stream case "STOPSTREAM": if(self.streamer == null || self.streamer == undefined) throw "There's no stream to stop."; self.stopFeed(); break; //Toggle pause the stream case "clientPAUSESTREAM": if(self.streamClients.includes(socket)) { var id = self.streamClients.indexOf(socket); self.streamClients.splice(id-1,1); } else self.streamClients.push(socket); break; case "globalPAUSESTREAM": if(self.streamer == null || self.streamer != undefined) throw "There's no stream to pause."; if(self.readStream!.isPaused()) self.readStream!.read() else self.readStream!.pause() break; default: validAction = false; break; } if(validAction) PiStreamServer.log.info(`Action incoming: ${action}`); } catch (error) { PiStreamServer.log.error(error); } }); } } } /** * Creates an instance of PiStreamServer and returns the Http server linked to it. * @param requestListner - Request listener. * @param video - Options of the stream. */ export const createServer = (requestListener: http.RequestListener, video?: StreamOptions): http.Server => { var server = http.createServer(requestListener); var stream = new PiStreamServer(new ws.Server({server}), video); return server; } /** * Copy the client file "http-live-player.js" to the given path. * @param path - Path of the target folder. */ export const createClient = (path='.') => { try { var file = 'http-live-player.js'; if(fs.existsSync(path)) fs.createReadStream(join(__dirname, '../../vendor/'+file)).pipe(fs.createWriteStream(join(path, file))); } catch (error) { PiStreamServer.log.error(error); } }