DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build a Node.js Camera Addon and Using It for Image Processing

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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, the checkGlobalDeps.js script checks and preinstalls the global dependencies. The postinstall.js script performs post-installation tasks, such as running install_name_tool on macOS to fix the dynamic library path.
  • src: Camera.h, CameraPreview.h, and stb_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. Run chmod +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
Enter fullscreen mode Exit fullscreen mode

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 the Camera and CameraWindow 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)
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. Create an empty directory and run npm init -y to create a new Node.js project.
  2. 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.

  3. Obtain a 30-day free trial license from the Dynamsoft website.

  4. 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.

  5. Run the application:

    node app.js
    

    Node.js Multi-Barcode Scanner

Source Code

https://github.com/yushulx/nodejs-lite-camera

Top comments (0)