rpi-cam-lib
Version:
A lightweight library to control CSI-2 camera modules via any raspberry pi can read camera module but is dependent to shell.
488 lines (487 loc) • 22.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { spawn, exec, execSync, } from "node:child_process";
import { EventEmitter } from "node:events";
import { _serveStill_params, _serveVideo_params } from "./utils/serveArgs";
import { Taskman } from "./common";
class LiveMode extends EventEmitter {
constructor() {
super();
}
}
export class RPICam extends Taskman {
/**
* @constructor constructor of `RPICam`.
* @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, options) {
super();
this.reserved = false;
/**
* @type {boolean}
* @prop `waiting` has two usage, first is forcing `wait` method to stop, seconds is checking process is waiting or not.
*/
this.waiting = false;
/** @type {LiveMode} */
/** @prop live streaming of camera comes to `live` event emitter of class.*/
this.live = new LiveMode();
this.camera = camera;
this.tasks = [];
this.options = options;
if ((options === null || options === void 0 ? void 0 : options.autoReserve) && this.isReadySync()) {
this.reserve();
}
}
/**
* Reserve the camera to prevent it from being used by other processes by opening an infinite and cancellable recording stream.
* @returns {Promise<IOutputException>} reservation status.
*/
reserve() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((res, rej) => __awaiter(this, void 0, void 0, function* () {
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 } = yield 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() {
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() {
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, width, height, options) {
var _a;
if (this.reserved)
this.unlockReserve();
const command = _serveStill_params(this.camera, filename, width, height, options);
try {
if (options === null || options === void 0 ? void 0 : 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 ((_a = this.options) === null || _a === void 0 ? void 0 : _a.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, height, options) {
var _a;
if (this.reserved)
this.unlockReserve();
const command = _serveStill_params(this.camera, (options === null || options === void 0 ? void 0 : options.output) || "", width, height, options);
if (options === null || options === void 0 ? void 0 : 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 ((_a = this.options) === null || _a === void 0 ? void 0 : _a.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`).
*/
serveStillCustom(width, height, id, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.reserved)
this.unlockReserve();
const command = _serveStill_params(this.camera, (options === null || options === void 0 ? void 0 : options.output) || "", width, height, options);
if (this.tasks.some((e) => e.id == id))
throw new Error("'serveStill' ,id must be unique!");
if (options === null || options === void 0 ? void 0 : options.stream) {
const proc = spawn(command);
return { output: proc, success: true };
}
return yield new Promise((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(() => { var _a; return ((_a = this.options) === null || _a === void 0 ? void 0 : _a.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, width, height, options) {
var _a;
if (this.reserved)
this.unlockReserve();
const command = _serveVideo_params(this.camera, (options === null || options === void 0 ? void 0 : options.output) || "", timeout, width, height, options);
try {
if (options === null || options === void 0 ? void 0 : 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 ((_a = this.options) === null || _a === void 0 ? void 0 : _a.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`).
*/
serveVideoCustom(timeout, width, height, id, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.reserved)
this.unlockReserve();
const command = _serveVideo_params(this.camera, (options === null || options === void 0 ? void 0 : options.output) || "", timeout, width, height, options);
if (this.tasks.some((e) => e.id == id))
throw new Error("'serveVideo' ,id must be unique!");
if (options === null || options === void 0 ? void 0 : options.stream) {
const proc = spawn(command);
return { output: proc, success: true };
}
return yield new Promise((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(() => { var _a; return ((_a = this.options) === null || _a === void 0 ? void 0 : _a.autoReserve) && this.reserve(); });
});
}
/**
* It takes a photo and saves it to 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`).
*/
serveStill(filename, width, height, id, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.reserved)
this.unlockReserve();
const command = _serveStill_params(this.camera, filename, width, height, options);
if (this.tasks.some((e) => e.id == id))
throw new Error("'serveStill', id must be unique!");
if (options === null || options === void 0 ? void 0 : options.stream) {
const proc = spawn(command);
return { output: proc, success: true };
}
return yield new Promise((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(() => { var _a; return ((_a = this.options) === null || _a === void 0 ? void 0 : _a.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, timeout, width, height, options) {
var _a;
if (this.reserved)
this.unlockReserve();
const command = _serveVideo_params(this.camera, filename, timeout, width, height, options);
try {
if (options === null || options === void 0 ? void 0 : 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 ((_a = this.options) === null || _a === void 0 ? void 0 : _a.autoReserve)
this.reserve();
}
}
/**
* Capture a video and save it to 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`).
*/
serveVideo(filename, timeout, width, height, id, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.reserved)
this.unlockReserve();
const command = _serveVideo_params(this.camera, filename, timeout, width, height, options);
if (this.tasks.some((e) => e.id == id))
throw new Error("'serveVideo', id must be unique!");
if (options === null || options === void 0 ? void 0 : options.stream) {
const proc = spawn(command);
return { output: proc, success: true };
}
return yield new Promise((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(() => { var _a; return ((_a = this.options) === null || _a === void 0 ? void 0 : _a.autoReserve) && this.reserve(); });
});
}
/**
* Start a live stream, load the stream to the `live` property or access it via the return value (this is similar to an event emitter and is 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, height, id, options) {
if (this.reserved)
this.unlockReserve();
const [cmd, ...args] = _serveVideo_params(this.camera, "-", 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", () => {
var _a;
this.live.emit("closed");
if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.autoReserve)
this.reserve();
});
return process;
}
catch (err) {
this.tasks.filter((e) => e.id != id);
throw err;
}
}
/**
* Gets list of available camera (connected cameras).
* @returns {ICameraDescriptor[]} list of cameras, must be empty if nothing connected.
*/
static getAvailCameras() {
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() {
try {
execSync(`rpicam-still --camera ${this.camera} -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`).
*/
isReady() {
return __awaiter(this, void 0, void 0, function* () {
return yield new Promise((res) => {
exec(`rpicam-still --camera ${this.camera} -t 10 -o -`, (err) => err ? res(false) : res(true));
});
});
}
/**
* Wait Asynchronously until camera being free.
* @param {number} timeout is waiting time for releasing camera, is optional and default values is `Infinity`.
* @returns {Promise<IOutputException>} when camera is free, always success of return value is `true` but output value means camera is free or not.
*/
wait() {
return __awaiter(this, arguments, void 0, function* (timeout = Infinity) {
const limit = Date.now() + timeout;
return new Promise((res) => __awaiter(this, void 0, void 0, function* () {
while (limit > Date.now() && this.waiting) {
if (yield this.isReady()) {
res({ success: true, output: true });
}
}
res({ success: true, output: false });
}));
});
}
/**
* is same to wait but is sync.
* @param {number} timeout is waiting time for releasing camera, is optional and default values is `Infinity`.
* @returns {IOutputException} when camera is free, always success of return value is `true` but output value means camera is free or not.
*/
waitSync(timeout = Infinity) {
const limit = Date.now() + timeout;
while (limit > Date.now()) {
if (this.isReadySync()) {
return { success: true, output: true };
}
}
return { success: true, output: false };
}
}