In the previous article, we demonstrated how to build a Python camera SDK based on the litecam C++ SDK. In this article, we will integrate the C++ camera library into a Node.js addon, enabling developers to capture webcam frames and perform further image processing using server-side JavaScript.
Node.js Multi-Barcode Scanner Demo Video
Preparing the Development Environment
Install the following tools and libraries:
npm install -g node-gyp
npm install -g node-addon-api
-
node-gyp
is a tool used to compile native addon modules for Node.js. It enables you to compile C++ or C code into a native Node.js addon, allowing integration of low-level C or C++ code with Node.js applications. -
node-addon-api
is a header-only C++ API that provides a modern C++ interface for developing native Node.js addons. It wraps the N-API (Node.js C API) to make it easier and safer to write native addons.
Scaffolding a Node.js Camera Addon Project
Create a Node.js addon project with the following structure:
python-lite-camera
│
│── examples
│ ├── barcode/
│── platforms
│ ├── linux
│ │ ├── liblitecam.so
│ ├── macos
│ │ ├── liblitecam.dylib
│ ├── windows
│ │ ├── litecam.dll
│ │ ├── litecam.lib
│── scripts
│ ├── checkGlobalDeps.js
│ ├── postinstall.js
├── src
│ ├── Camera.h
│ ├── CameraPreview.h
│ ├── litecam.cc
│ ├── nodecam.h
│ ├── stb_image_write.h
│── binding.gyp
│── index.d.ts
│── index.js
│── macos_build.sh
│── package.json
Explanation:
-
examples
: Contains image processing applications. We will create a multi-barcode scanner application in the barcode directory later. -
platforms
: Contains the precompiled C++ camera library for Windows, Linux, and macOS. -
scripts
: Since the addon will be built with the source code when installing the package, thecheckGlobalDeps.js
script checks and preinstalls the global dependencies. Thepostinstall.js
script performs post-installation tasks, such as runninginstall_name_tool
on macOS to fix the dynamic library path. -
src
:Camera.h
,CameraPreview.h
, andstb_image_write.h
are header files for the camera library.litecam.cc
is the main source file for the camera addon.nodecam.h
is the header file for the Node.js addon. -
binding.gyp
: The build configuration file for the Node.js addon. -
index.d.ts
: Exports the TypeScript type definitions for the addon. -
index.js
: The main entry file for the addon. -
macos_build.sh
: A script to build the addon on macOS. Runchmod +x macos_build.sh
to make it executable. -
package.json
: The package configuration file.
Defining the Native Class and Methods for the Node Camera Addon
Based on the header files Camera.h
and CameraPreview.h
, we define the native class and methods for the camera addon in nodecam.h
:
#ifndef NodeCamera_H
#define NodeCamera_H
#include <napi.h>
#include <string>
#include <uv.h>
#include <vector>
#include "Camera.h"
#include "CameraPreview.h"
using namespace std;
Napi::String ConvertWCharToJSString(Napi::Env env, const wchar_t *wideStr)
{
std::wstring wstr(wideStr);
std::string utf8Str(wstr.begin(), wstr.end());
return Napi::String::New(env, utf8Str);
}
class NodeCam : public Napi::ObjectWrap<NodeCam>
{
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
NodeCam(const Napi::CallbackInfo &info);
~NodeCam();
// Camera
Napi::Value open(const Napi::CallbackInfo &info);
Napi::Value listMediaTypes(const Napi::CallbackInfo &info);
Napi::Value release(const Napi::CallbackInfo &info);
Napi::Value setResolution(const Napi::CallbackInfo &info);
Napi::Value captureFrame(const Napi::CallbackInfo &info);
Napi::Value getWidth(const Napi::CallbackInfo &info);
Napi::Value getHeight(const Napi::CallbackInfo &info);
// Window
Napi::Value createWindow(const Napi::CallbackInfo &info);
Napi::Value waitKey(const Napi::CallbackInfo &info);
Napi::Value showPreview(const Napi::CallbackInfo &info);
Napi::Value showFrame(const Napi::CallbackInfo &info);
Napi::Value drawContour(const Napi::CallbackInfo &info);
Napi::Value drawText(const Napi::CallbackInfo &info);
private:
Camera *pCamera;
CameraWindow *pCameraWindow;
static Napi::FunctionReference constructor;
};
#endif
Explanation
-
ConvertWCharToJSString
: Converts a wide character string (used in the Windows environment) to a UTF-8 string that JavaScript can handle. -
NodeCam
: The native class for the camera addon. It wraps theCamera
andCameraWindow
classes, allowing JavaScript to interact with the camera and window functionalities.
Implementing the Node Camera Addon
In the litecam.cc
file, we implement all the methods defined in nodecam.h
.
Node Module Initialization
The NODE_API_MODULE
macro is used to initialize the Node module. It exports the getDeviceList
and saveJpeg
functions, as well as the NodeCam
class.
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
exports.Set("getDeviceList", Napi::Function::New(env, getDeviceList));
exports.Set("saveJpeg", Napi::Function::New(env, saveJpeg));
NodeCam::Init(env, exports);
return exports;
}
NODE_API_MODULE(litecam, Init)
The getDeviceList() function returns the available camera devices.
Napi::Value getDeviceList(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
Napi::Array deviceList = Napi::Array::New(env);
std::vector<CaptureDeviceInfo> devices = ListCaptureDevices();
for (size_t i = 0; i < devices.size(); i++)
{
CaptureDeviceInfo &device = devices[i];
#ifdef _WIN32
Napi::String jsFriendlyName = ConvertWCharToJSString(env, device.friendlyName);
#else
Napi::String jsFriendlyName = Napi::String::New(env, device.friendlyName);
#endif
deviceList.Set(i, jsFriendlyName);
}
return deviceList;
}
The saveJpeg() function saves RGB data as a JPEG image.
Napi::Value saveJpeg(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
std::string filename = info[0].As<Napi::String>();
int width = info[1].As<Napi::Number>().Int32Value();
int height = info[2].As<Napi::Number>().Int32Value();
Napi::Buffer<unsigned char> buffer = info[3].As<Napi::Buffer<unsigned char>>();
unsigned char *data = buffer.Data();
saveFrameAsJPEG(data, width, height, filename.c_str());
return env.Undefined();
}
The NodeCam
class registers the methods for the camera and window operations.
Napi::Object NodeCam::Init(Napi::Env env, Napi::Object exports)
{
Napi::Function camerafunc = DefineClass(env, "NodeCam", {InstanceMethod("open", &NodeCam::open), InstanceMethod("listMediaTypes", &NodeCam::listMediaTypes), InstanceMethod("release", &NodeCam::release), InstanceMethod("setResolution", &NodeCam::setResolution), InstanceMethod("captureFrame", &NodeCam::captureFrame), InstanceMethod("getWidth", &NodeCam::getWidth), InstanceMethod("getHeight", &NodeCam::getHeight), InstanceMethod("createWindow", &NodeCam::createWindow), InstanceMethod("waitKey", &NodeCam::waitKey), InstanceMethod("showFrame", &NodeCam::showFrame), InstanceMethod("drawContour", &NodeCam::drawContour), InstanceMethod("drawText", &NodeCam::drawText), InstanceMethod("showPreview", &NodeCam::showPreview)});
NodeCam::constructor = Napi::Persistent(camerafunc);
NodeCam::constructor.SuppressDestruct();
exports.Set("NodeCam", camerafunc);
return exports;
}
Camera and Window Methods
Camera:
-
open(index): Opens the camera with the specified index.
Napi::Value NodeCam::open(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); int index = info[0].As<Napi::Number>().Int32Value(); bool ret = false; if (pCamera) { ret = pCamera->Open(index); } return Napi::Boolean::New(env, ret); }
-
listMediaTypes(): Lists supported media types.
Napi::Value NodeCam::listMediaTypes(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); Napi::Array list = Napi::Array::New(env); std::vector<MediaTypeInfo> mediaTypes = pCamera->ListSupportedMediaTypes(); for (size_t i = 0; i < mediaTypes.size(); i++) { MediaTypeInfo &mediaType = mediaTypes[i]; int width = mediaType.width; int height = mediaType.height; #ifdef _WIN32 Napi::String jsMediaType = ConvertWCharToJSString(env, mediaType.subtypeName); #else Napi::String jsMediaType = Napi::String::New(env, mediaType.subtypeName); #endif Napi::Object obj = Napi::Object::New(env); obj.Set("width", Napi::Number::New(env, width)); obj.Set("height", Napi::Number::New(env, height)); obj.Set("mediaType", jsMediaType); list.Set(i, obj); } return list; }
-
setResolution(int width, int height): Sets the resolution for the camera.
Napi::Value NodeCam::setResolution(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); int width = info[0].As<Napi::Number>().Int32Value(); int height = info[1].As<Napi::Number>().Int32Value(); bool ret = false; if (pCamera) { ret = pCamera->SetResolution(width, height); } return Napi::Boolean::New(env, ret); }
-
captureFrame(): Captures a single RGB frame.
Napi::Value NodeCam::captureFrame(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); if (pCamera) { FrameData frame = pCamera->CaptureFrame(); if (frame.rgbData) { Napi::Object res = Napi::Object::New(env); int width = frame.width; int height = frame.height; int size = frame.size; unsigned char *rgbData = frame.rgbData; Napi::Buffer<unsigned char> buffer = Napi::Buffer<unsigned char>::New(env, size); memcpy(buffer.Data(), rgbData, size); res.Set("width", Napi::Number::New(env, width)); res.Set("height", Napi::Number::New(env, height)); res.Set("data", buffer); ReleaseFrame(frame); return res; } } return env.Undefined(); }
-
release(): Closes the camera and releases resources.
Napi::Value NodeCam::release(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); if (pCamera) { pCamera->Release(); } return env.Undefined(); }
-
getWidth(): Returns the width of the frame.
Napi::Value NodeCam::getWidth(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); if (pCamera) { int width = pCamera->frameWidth; return Napi::Number::New(env, width); } return env.Undefined(); }
-
getHeight(): Returns the height of the frame.
Napi::Value NodeCam::getHeight(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); if (pCamera) { int height = pCamera->frameHeight; return Napi::Number::New(env, height); } return env.Undefined(); }
Window:
-
createWindow(width, height, title): Creates a window with the specified dimensions and title.
Napi::Value NodeCam::createWindow(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); int width = info[0].As<Napi::Number>().Int32Value(); int height = info[1].As<Napi::Number>().Int32Value(); string title = info[2].As<Napi::String>().Utf8Value(); pCameraWindow = new CameraWindow(width, height, title.c_str()); if (pCameraWindow->Create()) { pCameraWindow->Show(); } return env.Undefined(); }
-
waitKey(key): Waits for user input; returns
false
if the specified key is pressed or the window is closed.
Napi::Value NodeCam::waitKey(const Napi::CallbackInfo &info) { if (!pCameraWindow) return info.Env().Undefined(); Napi::Env env = info.Env(); std::string key = info[0].As<Napi::String>(); bool ret = pCameraWindow->WaitKey(key[0]); return Napi::Boolean::New(env, ret); }
-
showFrame(width, height, rgbdata): Displays a frame in the window.
Napi::Value NodeCam::showFrame(const Napi::CallbackInfo &info) { if (!pCameraWindow) return info.Env().Undefined(); Napi::Env env = info.Env(); int width = info[0].As<Napi::Number>().Int32Value(); int height = info[1].As<Napi::Number>().Int32Value(); Napi::Buffer<unsigned char> buffer = info[2].As<Napi::Buffer<unsigned char>>(); unsigned char *data = buffer.Data(); pCameraWindow->ShowFrame(data, width, height); return env.Undefined(); }
-
drawContour(points): Draws contours on the preview window.
Napi::Value NodeCam::drawContour(const Napi::CallbackInfo &info) { if (!pCameraWindow) return info.Env().Undefined(); Napi::Env env = info.Env(); std::vector<std::pair<int, int>> points = {}; Napi::Array arr = info[0].As<Napi::Array>(); for (size_t i = 0; i < arr.Length(); i++) { Napi::Value tupleValue = arr.Get(i); Napi::Array tupleArray = tupleValue.As<Napi::Array>(); int x = tupleArray.Get(uint32_t(0)).As<Napi::Number>().Int32Value(); int y = tupleArray.Get(uint32_t(1)).As<Napi::Number>().Int32Value(); points.push_back(std::make_pair(x, y)); } pCameraWindow->DrawContour(points); return env.Undefined(); }
-
drawText(text, x, y, fontSize, color): Draws text on the preview window.
Napi::Value NodeCam::drawText(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); string text = info[0].As<Napi::String>().Utf8Value(); int x = info[1].As<Napi::Number>().Int32Value(); int y = info[2].As<Napi::Number>().Int32Value(); int fontSize = info[3].As<Napi::Number>().Int32Value(); Napi::Array colorArr = info[4].As<Napi::Array>(); CameraWindow::Color color; color.r = colorArr.Get(uint32_t(0)).As<Napi::Number>().Int32Value(); color.g = colorArr.Get(uint32_t(1)).As<Napi::Number>().Int32Value(); color.b = colorArr.Get(uint32_t(2)).As<Napi::Number>().Int32Value(); pCameraWindow->DrawText(text, x, y, fontSize, color); return env.Undefined(); }
Building the Node.js Camera Addon
node-gyp configure
node-gyp build
Creating a Multi-Barcode Scanner Application using Node.js
To test the camera addon, we will create an image processing application that captures frames from the camera and decodes barcodes in real-time.
- Create an empty directory and run
npm init -y
to create a new Node.js project. -
Install lite-camera and barcode4nodejs
npm install barcode4nodejs litecam
barcode4nodejs
is a Node.js wrapper for the Dynamsoft Barcode Reader SDK. It supports various barcode formats, including QR Code, DataMatrix, PDF417, and Aztec Code. Obtain a 30-day free trial license from the Dynamsoft website.
-
Create a new file named
app.js
and add the following code:
const dbr = require('barcode4nodejs'); dbr.initLicense("LICENSE-KEY"); var litecam = require('litecam'); const nodecamera = new litecam.NodeCam(); console.log(litecam.getDeviceList()); var isWorking = false; let results = null; async function decode(buffer, width, height) { if (isWorking) { return; } isWorking = true; results = await dbr.decodeBufferAsync(buffer, width, height, width * 3, dbr.formats.ALL, ""); isWorking = false; } function show() { if (nodecamera.waitKey('q')) { let frame = nodecamera.captureFrame(); if (frame) { nodecamera.showFrame(frame['width'], frame['height'], frame['data']); decode(frame['data'], frame['width'], frame['height']); if (results) { for (let i = 0; i < results.length; i++) { result = results[i]; let contour_points = [[result['x1'], result['y1']], [result['x2'], result['y2']], [result['x3'], result['y3']], [result['x4'], result['y4']]]; nodecamera.drawContour(contour_points) nodecamera.drawText(result['value'], result['x1'], result['y1'], 24, [255, 0, 0]) } } } setTimeout(show, 30); } else { nodecamera.release(); } } if (nodecamera.open(0)) { let mediaTypes = nodecamera.listMediaTypes(); console.log(mediaTypes); nodecamera.createWindow(nodecamera.getWidth(), nodecamera.getHeight(), "Camera Stream"); show(); }
Replace
LICENSE-KEY
with your own license key. -
Run the application:
node app.js
Top comments (0)