UNPKG

@abpd2001/rpicam

Version:

A lightweight library to control CSI-2 camera modules via any raspberry pi can read camera module but is dependent to shell.

813 lines (755 loc) 27.1 kB
import type { ICameraDescriptor, ICameraOptions, ICameraStillOptions, ICameraVideoOptions, IOutputException, } from "./interfaces/index"; import { spawn, exec, execSync, ChildProcessWithoutNullStreams, } from "node:child_process"; import { EventEmitter } from "node:events"; import { TASKMAN } from "./common"; class LiveMode extends EventEmitter { constructor() { super(); } } export class RPICam extends TASKMAN { private camera: number; private options?: ICameraOptions; private reserved: boolean = false; /** @type {{ id: string; pid: number }[]} */ /**@prop `tasks` is current async opreations running on camera with pid and id.*/ public tasks: { id: string; pid: number }[]; /** @type {LiveMode} */ /** @prop live streaming of camera comes to `live` event emitter of class.*/ public live: LiveMode = new LiveMode(); /** * @constructor class of `RPICam`, also this constructor can wait until camera get ready. * @param {number} camera is number of camera to get in use. * @param {ICameraOptions} options is avail options and formatting and setting camera by manual and method of capturing. */ constructor(camera: number, options?: ICameraOptions) { super(); this.camera = camera; this.tasks = []; this.options = options; if (options?.autoReserve && this.isReadySync()) { this.reserve(); } } private _serveStill_params( filename: string, width: number, height: number, options?: ICameraStillOptions ) { const { contrast, brightness, quality, exposureCompensation, saturation, sharpness, autoFocusModeRange, effect, autoFocusOnCapture, awb, awbgains, burst, datetime, exposure, flipHorizontal, flipVertical, iso, rotation, zoom, timeout, noPreview, denoise, mode, metadata, finalCommand, initialCommand, timelapse, signal, keypress, format, } = options || {}; const zoomValues: { name: ICameraStillOptions["zoom"]; x: number; y: number; w: number; h: number; }[] = [ { name: "1x", x: 0, y: 0, w: 1, h: 1 }, { name: "2x", x: 0.25, y: 0.25, w: 0.5, h: 0.5 }, { name: "3x", w: 0.33, h: 0.33, x: 0.385, y: 0.385 }, { name: "4x", w: 0.25, h: 0.25, x: 0.375, y: 0.375 }, { name: "5x", w: 0.2, h: 0.2, x: 0.4, y: 0.4 }, { name: "6x", w: 0.16, h: 0.16, x: 0.42, y: 0.42 }, { name: "7x", w: 0.14, h: 0.14, x: 0.43, y: 0.43 }, { name: "8x", w: 0.125, h: 0.125, x: 0.437, y: 0.437 }, { name: "9x", w: 0.11, h: 0.11, x: 0.445, y: 0.445 }, { name: "10x", w: 0.1, h: 0.1, x: 0.45, y: 0.45 }, ]; const zoomValue = zoomValues.find((e) => e.name == (zoom || "1x")); const flags = [ { key: "--roi", value: `${zoomValue!.x},${zoomValue!.y},${zoomValue!.w},${ zoomValue!.h }`, cond: zoom, }, { key: "--format", value: format, cond: format }, { key: "--iso", value: iso, cond: iso }, { key: "--effect", value: effect, cond: effect }, { key: "--exposure", value: exposure, cond: exposure }, { key: "--ev", value: exposureCompensation, cond: (exposureCompensation || -1) > -1, }, { key: "-n", cond: noPreview, value: undefined }, { key: "-t", cond: (timeout || -1) > -1, value: timeout }, { key: "--quality", cond: (quality || -1) > -1 }, { key: "--contrast", cond: (contrast || -1) > -1, value: contrast }, { key: "--sharpness", cond: (sharpness || -1) > -1, value: sharpness }, { key: "--brightness", cond: (brightness || -1) > -1, value: brightness }, { key: "--hflip", cond: flipHorizontal, value: undefined }, { key: "--vflip", cond: flipVertical, value: undefined }, { key: "--denoise", cond: typeof denoise == "boolean", value: denoise ? "on" : "off", }, { key: "--saturation", cond: (saturation || -1) > -1, value: saturation }, { key: "--awb", cond: awb, value: awb }, { key: "--autofocus-on-capture", cond: autoFocusOnCapture, value: undefined, }, { key: "--autofocus-range", cond: autoFocusModeRange, value: undefined }, { key: "--mode", cond: mode, value: mode }, { key: "--rotation", cond: (rotation || -1) > -1, value: rotation }, { key: "--datatime", cond: datetime, value: undefined }, { key: "--burst", cond: burst, value: undefined }, { key: "--awbgains", cond: awbgains, value: awbgains }, { key: "--metadata", cond: metadata, value: metadata ? "on" : "off" }, { key: "--final", cond: finalCommand, value: finalCommand }, { key: "--initial", cond: initialCommand, value: initialCommand }, { key: "--timelapse", cond: (timelapse || -1) > -1, value: timelapse }, { key: "--signal", cond: signal, value: signal }, { key: "--keypress", cond: keypress, value: keypress }, ]; const command = `rpicam-still --camera ${this.camera} ${flags .filter((e) => e.cond) .map((e) => `${e.key}${e.value ? e.value : ""}`) .join(" ")} -w ${width} -h ${height} ${filename ? `-o ${filename}` : ""}`; return command; } private _serveVideo_params( filename: string, timeout: number, width: number, height: number, options?: Omit<ICameraVideoOptions, "timeout"> ) { const { contrast, brightness, exposureCompensation, saturation, sharpness, autoFocusModeRange, effect, autoFocusOnCapture, awb, awbgains, datetime, exposure, flipHorizontal, flipVertical, iso, rotation, zoom, noPreview, denoise, mode, fps, instra, maxFileSize, segment, codec, codecLevel, codecProfile, bitrate, saveParts, circularMode, finalCommand, initialCommand, metadata, keypress, signal, format, } = options || {}; const zoomValues: { name: ICameraStillOptions["zoom"]; x: number; y: number; w: number; h: number; }[] = [ { name: "1x", x: 0, y: 0, w: 1, h: 1 }, { name: "2x", x: 0.25, y: 0.25, w: 0.5, h: 0.5 }, { name: "3x", w: 0.33, h: 0.33, x: 0.385, y: 0.385 }, { name: "4x", w: 0.25, h: 0.25, x: 0.375, y: 0.375 }, { name: "5x", w: 0.2, h: 0.2, x: 0.4, y: 0.4 }, { name: "6x", w: 0.16, h: 0.16, x: 0.42, y: 0.42 }, { name: "7x", w: 0.14, h: 0.14, x: 0.43, y: 0.43 }, { name: "8x", w: 0.125, h: 0.125, x: 0.437, y: 0.437 }, { name: "9x", w: 0.11, h: 0.11, x: 0.445, y: 0.445 }, { name: "10x", w: 0.1, h: 0.1, x: 0.45, y: 0.45 }, ]; const zoomValue = zoomValues.find((e) => e.name == (zoom || "1x")); const flags = [ { key: "--roi", value: `${zoomValue!.x},${zoomValue!.y},${zoomValue!.w},${ zoomValue!.h }`, cond: zoom, }, { key: "--format", value: format, cond: format }, { key: "--iso", value: iso, cond: iso }, { key: "--effect", value: effect, cond: effect }, { key: "--exposure", value: exposure, cond: exposure }, { key: "--ev", value: exposureCompensation, cond: (exposureCompensation || -1) > -1, }, { key: "-n", cond: noPreview, value: undefined }, { key: "-t", cond: (timeout || -1) > -1, value: timeout }, { key: "--contrast", cond: (contrast || -1) > -1, value: contrast }, { key: "--sharpness", cond: (sharpness || -1) > -1, value: sharpness }, { key: "--brightness", cond: (brightness || -1) > -1, value: brightness }, { key: "--hflip", cond: flipHorizontal, value: undefined }, { key: "--vflip", cond: flipVertical, value: undefined }, { key: "--denoise", cond: typeof denoise == "boolean", value: denoise ? "on" : "off", }, { key: "--saturation", cond: (saturation || -1) > -1, value: saturation }, { key: "--awb", cond: awb, value: awb }, { key: "--autofocus-on-capture", cond: autoFocusOnCapture, value: undefined, }, { key: "--autofocus-range", cond: autoFocusModeRange, value: undefined }, { key: "--mode", cond: mode, value: mode }, { key: "--rotation", cond: (rotation || -1) > -1, value: rotation }, { key: "--datatime", cond: datetime, value: undefined }, { key: "--awbgains", cond: awbgains, value: awbgains }, { key: "--fps", cond: (fps || -1) > -1, value: fps }, { key: "--instra", cond: (instra || -1) > -1, value: instra }, { key: "--metadata", cond: metadata, value: metadata ? "on" : "off" }, { key: "--final", cond: finalCommand, value: finalCommand }, { key: "--initial", cond: initialCommand, value: initialCommand }, { key: "--signal", cond: signal, value: signal }, { key: "--keypress", cond: keypress, value: keypress }, { key: "--codec", cond: codec, value: codec }, { key: "--segment", cond: segment, value: segment }, { key: "--level", cond: codecLevel, value: codecLevel }, { key: "--profile", cond: codecProfile, value: codecProfile }, { key: "--bitrate", cond: bitrate, value: bitrate }, { key: "--save-pts", cond: saveParts, value: undefined }, { key: "--max-length", cond: maxFileSize, value: maxFileSize }, { key: "--circular", cond: circularMode, value: undefined }, ]; const command = `rpicam-still --camera ${this.camera} ${flags .filter((e) => e.cond) .map((e) => `${e.key}${e.value ? e.value : ""}`) .join(" ")} -w ${width} -h ${height} ${filename ? `-o ${filename}` : ""}`; return command; } /** * Reserve camera to not get in use of other process by opening a infinite and cancellable stream of recording. * @returns {Promise<IOutputException>} reservation status. */ async reserve(): Promise<IOutputException> { return new Promise<IOutputException>(async (res, rej) => { if (!this.isReadySync()) rej({ success: false, error: { name: "CAMERA_BUSY_ALREADY", readable: "Can not reserve camera because is busy (in use of other process).", }, }); const { success } = await this.serveVideoCustom(0, 50, 50, "Reserve", { fps: 5, stream: true, output: "-", }); this.reserved = success; res({ success }); }); } /** * kills process of reservation of camera and make it free. * @returns {IOutputException} status of unlocking camera. */ unlockReserve(): IOutputException { if (!this.reserved) return { success: false, error: { readable: "Camera should be reserved to unlock!", name: "NOT_RESERVED", }, }; return this.killTask("Reserve"); } /** * Check reservation of camera by this process. * @returns {boolean} is reserved or not. */ isReserved(): boolean { return this.reserved; } /** * Same as `serveStill` but is sync. * @param {string} filename is path of saving image, also is okay set it to empty string for streaming options and everything like stream should not be outputed on a file or use `serveStillCutomSync` to set everything manually. * @param {number} width (no description needed) * @param {number} height (no description needed) * @param { ICameraStillOptions & { stream?: boolean }} options of serving (full settings and options of camera and rpicam). * @returns {IOutputException} capturing status or stream event emitter output. */ serveStillSync( filename: string, width: number, height: number, options?: ICameraStillOptions & { stream?: boolean } ): IOutputException { if (this.reserved) this.unlockReserve(); const command = this._serveStill_params(filename, width, height, options); try { if (options?.stream) { const proc = spawn(command); return { output: proc, success: true }; } const execute = execSync(command); return { output: execute, error: undefined, success: true, }; } catch (err) { throw err; } finally { if (this.options?.autoReserve) this.reserve(); } } /** * Same as `serveStillCustom` but is sync. * @param {number} width (no description needed) * @param {number} height (no description needed) * @param { ICameraStillOptions & {stream?: boolean,output?: string,format?: string}} options of serving (full settings and options of camera and rpicam). * @returns {IOutputException} capturing status or stream event emitter output. */ serveStillCustomSync( width: number, height: number, options?: ICameraStillOptions & { stream?: boolean; output?: string; format?: string; } ): IOutputException { if (this.reserved) this.unlockReserve(); const command = this._serveStill_params( options?.output || "", width, height, options ); if (options?.stream) { const proc = spawn(command); return { output: proc, success: true }; } try { const execute = execSync(command); return { output: execute, error: undefined, success: true, }; } catch (err) { throw err; } finally { if (this.options?.autoReserve) this.reserve(); } } /** * Same as `serveStill` But everything except some required things must setted manually. * @param {number} width (no description needed) * @param {number} height (no description needed) * @param {string} id of task for managing. * @param { ICameraStillOptions & {stream?: boolean,output?: string,format?: string}} options of serving (full settings and options of camera and rpicam). * @returns { Promise<IOutputException>} capturing status or stream event emitter output (as a `Promise`). */ async serveStillCustom( width: number, height: number, id: string, options?: ICameraStillOptions & { stream?: boolean; output?: string; format?: string; } ): Promise<IOutputException> { if (this.reserved) this.unlockReserve(); const command = this._serveStill_params( options?.output || "", width, height, options ); if (this.tasks.some((e) => e.id == id)) throw new Error("'serveStill' ,id must be unique!"); if (options?.stream) { const proc = spawn(command); return { output: proc, success: true }; } return await new Promise<IOutputException>((res, rej) => { this.tasks.push({ pid: exec(command, (error, output) => { if (error) rej({ error: { readable: `An error in capturing still on camera!\nerr: ${error}`, name: "CAMERA_STILLING_INTERNAL_ERROR", }, success: false, }); else if (output) { res({ output, success: true }); this.tasks = this.tasks.filter((e) => e.id != id); } }).pid || -1, id, }); }).finally(() => this.options?.autoReserve && this.reserve()); } /** * Same as `serveVideoCustom` but is sync. * @param {number} timeout of capturing or time argument of streams and other stuff needs time. * @param {number} width (no description needed) * @param {number} height (no description needed) * @param {ICameraVideoOptions & { stream?: boolean, output?: string }} options of serving (full settings and options of camera and rpicam). * @returns {IOutputException} capturing status or stream event emitter output. */ serveVideoCustomSync( timeout: number, width: number, height: number, options?: ICameraVideoOptions & { stream?: boolean; output?: string } ): IOutputException { if (this.reserved) this.unlockReserve(); const command = this._serveVideo_params( options?.output || "", timeout, width, height, options ); try { if (options?.stream) { const proc = spawn(command); return { output: proc, success: true }; } const execute = execSync(command); return { output: execute, error: undefined, success: true, }; } catch (err) { throw err; } finally { if (this.options?.autoReserve) this.reserve(); } } /** * Same as `serveVideo` But everything except some required things must setted manually. * @param {number} timeout of capturing or time argument of streams and other stuff needs time. * @param {number} width (no description needed) * @param {number} height (no description needed) * @param {string} id of task for managing. * @param {ICameraVideoOptions & { stream?: boolean, output?: string }} options of serving (full settings and options of camera and rpicam). * @returns {Promise<IOutputException>} capturing status or stream event emitter output (as a `Promise`). */ async serveVideoCustom( timeout: number, width: number, height: number, id: string, options?: ICameraVideoOptions & { stream?: boolean; output?: string } ): Promise<IOutputException> { if (this.reserved) this.unlockReserve(); const command = this._serveVideo_params( options?.output || "", timeout, width, height, options ); if (this.tasks.some((e) => e.id == id)) throw new Error("'serveVideo' ,id must be unique!"); if (options?.stream) { const proc = spawn(command); return { output: proc, success: true }; } return await new Promise<IOutputException>((res, rej) => { this.tasks.push({ pid: exec(command, (error, output) => { if (error) rej({ error, success: false }); else if (output) { res({ output, success: true }); this.tasks = this.tasks.filter((e) => e.id != id); } }).pid || -1, id, }); }).finally(() => this.options?.autoReserve && this.reserve()); } /** * Takes a photo and save it into a file (powered by `rpicam-still`). * @param {string} filename is path of saving image, also is okay set it to empty string for streaming options and everything like stream should not be outputed on a file or use `serveStillCutomSync` to set everything manually. * @param {number} width of resolution. * @param {number} height of resolution. * @param {string} id of task for managing. * @param { ICameraStillOptions & { stream?: boolean }} options of serving (full settings and options of camera and rpicam). * @returns {Promise<IOutputException>} capturing status or stream event emitter output (as a `Promise`). */ async serveStill( filename: string, width: number, height: number, id: string, options?: ICameraStillOptions & { stream?: boolean } ): Promise<IOutputException> { if (this.reserved) this.unlockReserve(); const command = this._serveStill_params(filename, width, height, options); if (this.tasks.some((e) => e.id == id)) throw new Error("'serveStill', id must be unique!"); if (options?.stream) { const proc = spawn(command); return { output: proc, success: true }; } return await new Promise<IOutputException>((res, rej) => { this.tasks.push({ pid: exec(command, (error, output) => { if (error) rej({ error: { readable: `An error in capturing still on camera!\nerr: ${error}`, name: "CAMERA_STILLING_INTERNAL_ERROR", }, success: false, }); else if (output) { res({ output, success: true }); this.tasks = this.tasks.filter((e) => e.id != id); } }).pid || -1, id, }); }).finally(() => this.options?.autoReserve && this.reserve()); } /** * Same as `serveVideo` but is sync. * @param {string} filename is path of saving video, also is okay set it to empty string for streaming options and everything like stream should not be outputed on a file or use `serveVideoCutomSync` to set everything manually. * @param {number} width of resolution. * @param {number} height of resolution. * @param { ICameraStillOptions & { stream?: boolean }} options of serving (full settings and options of camera and rpicam). * @returns {Promise<IOutputException>} capturing status or stream event emitter output. */ serveVideoSync( filename: string, timeout: number, width: number, height: number, options?: ICameraVideoOptions & { stream?: boolean } ): IOutputException { if (this.reserved) this.unlockReserve(); const command = this._serveVideo_params( filename, timeout, width, height, options ); try { if (options?.stream) { const proc = spawn(command); return { output: proc, success: true }; } const execute = execSync(command); return { output: execute, error: undefined, success: true, }; } catch (err) { throw err; } finally { if (this.options?.autoReserve) this.reserve(); } } /** * Capture a video and save it into a file (powered by `rpicam-vid`). * @param {string} filename is path of saving video, also is okay set it to empty string for streaming options and everything like stream should not be outputed on a file or use `serveVideoCutomSync` to set everything manually. * @param {number} width of resolution. * @param {number} height of resolution. * @param {string} id of task for managing. * @param { ICameraStillOptions & { stream?: boolean }} options of serving (full settings and options of camera and rpicam). * @returns {Promise<IOutputException>} capturing status or stream event emitter output (as a `Promise`). */ async serveVideo( filename: string, timeout: number, width: number, height: number, id: string, options?: ICameraVideoOptions & { stream?: boolean } ): Promise<IOutputException> { if (this.reserved) this.unlockReserve(); const command = this._serveVideo_params( filename, timeout, width, height, options ); if (this.tasks.some((e) => e.id == id)) throw new Error("'serveVideo' ,id must be unique!"); if (options?.stream) { const proc = spawn(command); return { output: proc, success: true }; } return await new Promise<IOutputException>((res, rej) => { this.tasks.push({ pid: exec(command, (error, output) => { if (error) rej({ error, success: false }); else if (output) { res({ output, success: true }); this.tasks = this.tasks.filter((e) => e.id != id); } }).pid || -1, id, }); }).finally(() => this.options?.autoReserve && this.reserve()); } /** * Start a live stream, load stream to `live` property or access it via return value (is similar to event emitter, and powered by `serveVideo`). * @param width (no description needed) * @param height (no description needed) * @param id of task for managing. * @param options of serving (full settings and options of camera and rpicam). * @returns {ChildProcessWithoutNullStreams } spawn stream of node:child_process. */ serveLive( width: number, height: number, id: string, options: ICameraVideoOptions ): ChildProcessWithoutNullStreams { if (this.reserved) this.unlockReserve(); const [cmd, ...args] = this._serveVideo_params( "-", 0, width, height, options ).split(" "); try { const process = spawn(cmd, args); this.tasks.push({ pid: process.pid || -1, id }); this.live.emit("started"); process.stdout.on("data", (frame) => { this.live.emit("frame", frame); }); process.on("close", () => { this.live.emit("closed"); if (this.options?.autoReserve) this.reserve(); }); return process; } catch (err) { this.tasks.filter((e) => e.id != id); throw err; } } /** * Gets list of avail camera (connected cameras). * @returns {ICameraDescriptor[]} list of cameras, must be empty if nothing connected. */ static getAvailCameras(): ICameraDescriptor[] { const rawData = execSync("rpicam-hello --list-cameras").toString(); if (rawData.includes("No cameras available!")) return []; const lists = rawData .split("-----------------") .slice(1) .map((e) => { const [idx, data] = e.split(":").map((a) => a.trim()); const [name, resolution, path] = data.split(" "); const [width, height] = resolution .replace("[", "") .replace("]", "") .split("x"); return { index: Number(idx), name, resolution: { width: Number(width), height: Number(height) }, path: path.replace("(", "").replace(")", ""), }; }); return lists; } /** * Same as `isReady` but is sync. * @returns {Promise<boolean>} ready status by boolean. */ isReadySync(): boolean { try { execSync("rpicam-still -t 10 -o -"); return true; } catch (err) { return false; } } /** * Checks is Camera ready or not by testing a simple rpicam-still on camera (purpose of testing is to check is connected okay or is free or not). * @returns {Promise<boolean>} ready status by boolean (as a`Promise`). */ async isReady(): Promise<boolean> { return await new Promise((res) => { exec("rpicam-still -t 10 -o -", (err) => (err ? res(false) : res(true))); }); } }