Electron 与 Native NodeJS 整合

251

简介

在开发中有时会遇到前端需要调用 DLL 或 C++ 代码的情况,本篇就将通过实例介绍如何使用 Native NodeJS 封装 DLL 以供 Electron 进行调用。本例用 OpenCV 实现一个简单的人脸识别功能,并将该功能封装成 Native Node,最后利用 Electron 展现结果。

OpenCV实现人脸识别

让我们先来实现采集摄像头图像,并对采集的图像做人脸识别。为了与Electron前端进行交互,我们将这个功能写成Native NodeJS。下面是全部的代码,我将在后面逐行解释。

$ \underline{StreamPlayer.cpp} $

#include <node.h>
#include <uv.h>
#include <opencv2/opencv.hpp>
#include <Windows.h>
#include <memory>

namespace sp
{

// 日志回调函数
static v8::Persistent<v8::Function> g_logCallback;

// 获取到 Frame
static v8::Persistent<v8::Function> g_frameDataCallback;

// 是否退出摄像头抓取线程
static bool g_quit = false;

// 摄像头抓取线程
static uv_thread_t g_captureThreadId;

// 异步对象,用来在摄像头抓取线程和主线程之间传递数据
static uv_async_t g_async = { 0 };

// 要传递的数据
struct AsyncData
{
    std::wstring type;  // 类型
    std::wstring message;  // 消息
};
static AsyncData g_asyncData;

// Frame 缓存,通过该缓存与 JS 交互
static const unsigned int BUFFER_SIZE = 640 * 480 * 4;
static unsigned char* g_frameBuffer = new unsigned char[BUFFER_SIZE]();

std::string toUtf8String(const wchar_t * const str)
{
    auto length = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
    std::unique_ptr<char[]> buffer{ new char[length]() };
    WideCharToMultiByte(CP_UTF8, 0, str, -1, buffer.get(), length, NULL, NULL);
    return std::string(buffer.get(), length);
}

void log(v8::Isolate* isolate, const wchar_t * const message)
{
    std::string messageUtf8 = toUtf8String(message);
    v8::Handle<v8::Value> argv = v8::String::NewFromUtf8(isolate, messageUtf8.c_str());
    auto logCallback = v8::Local<v8::Function>::New(isolate, g_logCallback);
    logCallback->Call(v8::Null(isolate), 1, &argv);
}

void setLogCallback(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    auto isolate = args.GetIsolate();

    // 如果第一个参数不是函数,则抛出错误
    if (!args[0]->IsFunction())
    {
        isolate->ThrowException(v8::Exception::TypeError(
            v8::String::NewFromUtf8(isolate, toUtf8String(L"The first argument must be a function!").c_str())));
        return;
    }

    // 获取回调函数,并保留到全局变量
    auto logCallback = v8::Handle<v8::Function>::Cast(args[0]);
    g_logCallback.Reset(isolate, logCallback);

    log(isolate, L"Set log callback success!");
}

void setFrameDataCallback(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    auto isolate = args.GetIsolate();

    // 如果第一个参数不是函数,则抛出错误
    if (!args[0]->IsFunction())
    {
        isolate->ThrowException(v8::Exception::TypeError(
            v8::String::NewFromUtf8(isolate, toUtf8String(L"The first argument must be a function!").c_str())));
        return;
    }

    // 获取回调函数,并保留到全局变量
    auto frameDataCallback = v8::Handle<v8::Function>::Cast(args[0]);
    g_frameDataCallback.Reset(isolate, frameDataCallback);
}

void capture(void *arg)
{
    // 打开摄像头
    cv::VideoCapture cap(0);

    // 如果打开失败,返回错误
    if (!cap.isOpened())
    {
        g_asyncData.type = L"Error";
        g_asyncData.message = L"Open camera failed!";
        g_async.data = &g_asyncData;
        uv_async_send(&g_async);
        return;
    }

    // 人脸识别分类器
    cv::CascadeClassifier faceCascadeClassifier("F:\\opencv\\lib\\opencv\\data\\haarcascades_cuda\\haarcascade_frontalface_alt2.xml");

    // 读取 Frame ,直到退出系统
    while (!g_quit)
    {
        cv::Mat frame;
        if (!cap.read(frame))
        {
            // 读取失败,返回错误
            g_asyncData.type = L"Error";
            g_asyncData.message = L"Read frame failed!";
            g_async.data = &g_asyncData;
            uv_async_send(&g_async);
            break;
        }

        // 进行人脸识别
        std::vector<cv::Rect> faces;
        faceCascadeClassifier.detectMultiScale(frame, faces);
        // 将人脸识别结果绘制到图片上
        for (const auto& face : faces) 
        {
            cv::rectangle(frame,
                cv::Point(face.x, face.y), 
                cv::Point(face.x + face.width, face.y + face.height), 
                CV_RGB(255, 0, 0), 
                2);
        }

        // 因为 <canvas> 显示的数据是 RGBA 的图像,
        // 因此需要将采集的图片(BGR格式)转换为 RGBA 格式
        cv::Mat converted;
        cv::cvtColor(frame, converted, cv::COLOR_BGR2RGBA);

        // 将转换好的图片赋值给缓存
        memcpy(g_frameBuffer, converted.data, 640 * 480 * 4);

        // 读取成功,返回读到的 Frame
        g_asyncData.type = L"Frame";
        g_asyncData.message = L"Frame captured!";
        g_async.data = &g_asyncData;
        uv_async_send(&g_async);
    }
}

// 创建缓存,创建的缓存用于与前端交互
void createFrameBuffer(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    auto isolate = args.GetIsolate();
    auto buffer = v8::SharedArrayBuffer::New(isolate, g_frameBuffer, BUFFER_SIZE);
    auto array = v8::Uint8ClampedArray::New(buffer, 0, BUFFER_SIZE);
    args.GetReturnValue().Set(array);

    log(isolate, L"Create frame buffer succeess!");
}

void start(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    g_quit = false;
    // 创建线程,在线程内处理采集,识别,并将最终结果分发到前端展示
    uv_thread_create(&g_captureThreadId, capture, nullptr);
}

void stop(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    g_quit = true;
    uv_thread_join(&g_captureThreadId);

    log(args.GetIsolate(), L"Capture thread stopped.");
}

// 处理摄像头线程分发的消息
void eventCallback(uv_async_t* handle)
{
    auto isolate = v8::Isolate::GetCurrent();
    AsyncData *asyncData = reinterpret_cast<AsyncData*>(handle->data);
    if (asyncData->type == L"Error")
    {
        log(isolate, asyncData->message.c_str());
    }
    else if (asyncData->type == L"Frame")
    {
        auto cb = v8::Local<v8::Function>::New(isolate, g_frameDataCallback);
        cb->Call(v8::Null(isolate), 0, NULL);
    }
}

void init(v8::Local<v8::Object> exports)
{
    NODE_SET_METHOD(exports, "setLogCallback", setLogCallback);
    NODE_SET_METHOD(exports, "setFrameDataCallback", setFrameDataCallback);
    NODE_SET_METHOD(exports, "createFrameBuffer", createFrameBuffer);
    NODE_SET_METHOD(exports, "start", start);
    NODE_SET_METHOD(exports, "stop", stop);

    // 将事件回调函数注册到V8线程
    uv_async_init(uv_default_loop(), &g_async, eventCallback);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, init)

}

首先是包含文件:

#include <node.h>
#include <uv.h>
#include <opencv2/opencv.hpp>
#include <Windows.h>
#include <memory>

#include <node.h> 是实现 Native NodeJS 所必需的。我们使用了 libuv 的线程和事件的相关API,因此也同样包含了 #include <uv.h>。采集和人脸识别我们用的都是 OpenCV,因此包含 #include <opencv2/opencv.hpp>#include <Windows.h>这个大家应该很熟悉了,使用 Windows API 所必须包含的头文件。另外为了使用一些智能指针,这里包含了 C++ 的标准库文件 #include <memory>

// 日志回调函数
static v8::Persistent g_logCallback;

// 获取到 Frame
static v8::Persistent g_frameDataCallback;

// 是否退出摄像头抓取线程
static bool g_quit = false;

// 摄像头抓取线程
static uv_thread_t g_captureThreadId;

// 异步对象,用来在摄像头抓取线程和主线程之间传递数据
static uv_async_t g_async = { 0 };

// 要传递的数据
struct AsyncData
{
    std::wstring type;  // 类型
    std::wstring message;  // 消息
};
static AsyncData g_asyncData;

// Frame 缓存,通过该缓存与 JS 交互
static const unsigned int BUFFER_SIZE = 640 * 480 * 4;
static unsigned char* g_frameBuffer = new unsigned char[BUFFER_SIZE]();

上面的程序片段中,在第 2 行和第 5 行声明了两个全局变量 g_logCallbackg_frameDataCallback 用来保存日志回调函数和采集回调函数。日志回调函数用来打印一些日志给前端,而采集回调函数会在我们处理好每一帧图片的时候调用并在前端显示这张处理好的图片,通过不断调用该回调函数就实现了播放的效果。
第 8 行的全局变量 g_quit 用来标识是否退出采集线程。
第 10 行到 22 行是关于 libuv 的一些定义。11 行的全局变量 g_captureThreadId 是采集线程的 ID。14 行定义的 g_async 用来在采集线程和主线程(V8线程)之间传递数据,它是一个结构体类型,其中有一个 void* data的成员变量,用来在线程间传递自定义数据。17 行的结构体 struct AsyncData 定义了两个线程间传递的数据的具体类型,其中 type 表示消息的类型,而 message 表示具体的消息。第 22 行定义了一个全局的 AsyncData 变量用以实际地传递数据,这样的好处是不用在传递数据时反复的分配(new 或者 malloc)和销毁(delete 或者 free)堆内存了。
第 25 行定义了帧缓存的大小。注意这里把大小写死了,实际情况中应该根据需要进行静态或者动态配置。
第 26 行定义了一个全局变量 g_frameBuffer 作为帧缓存,用来在 NodeJS 和前端传递数据,定义为全局变量的好处是可以不用反复地在 NodeJS 和前端 JS 之间分配、销毁内存了。

std::string toUtf8String(const wchar_t * const str)
{
    auto length = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
    std::unique_ptr buffer{ new char[length]() };
    WideCharToMultiByte(CP_UTF8, 0, str, -1, buffer.get(), length, NULL, NULL);
    return std::string(buffer.get(), length);
}

这个函数是一个辅助函数,用来将宽字符串转换为UTF8编码。关于 WideCharToMultiByte() 的用法请见官方文档:MSDN: WideCharToMultiByte function.aspx)。另外,这里使用了 std::unique_ptr 管理内存。

void log(v8::Isolate* isolate, const wchar_t * const message)
{
    std::string messageUtf8 = toUtf8String(message);
    v8::Handle argv = v8::String::NewFromUtf8(isolate, messageUtf8.c_str());
    auto logCallback = v8::Local::New(isolate, g_logCallback);
    logCallback->Call(v8::Null(isolate), 1, &argv);
}

这个函数用来调用JS端的日志回调函数,注意这里需要将字符串编码为UTF8再传递给V8。

void setLogCallback(const v8::FunctionCallbackInfo& args)
{
    auto isolate = args.GetIsolate();

    // 如果第一个参数不是函数,则抛出错误
    if (!args[0]->IsFunction())
    {
        isolate->ThrowException(v8::Exception::TypeError(
            v8::String::NewFromUtf8(isolate, toUtf8String(L"The first argument must be a function!").c_str())));
        return;
    }

    // 获取回调函数,并保留到全局变量
    auto logCallback = v8::Handle::Cast(args[0]);
    g_logCallback.Reset(isolate, logCallback);

    log(isolate, L"Set log callback success!");
}

这段代码用来设置日志回调函数。注册为Native NodeJS的函数原型为 void yourFunction(const v8::FunctionCallbackInfo<v8::Value>& args);,所以每一个被JS调用的函数都需要以这种方式声明。
args.GetIsolate()用来获取 isolate 对象,几乎每个 V8 API 都会以这个对象为第一个参数。接着该函数判断第一个参数是否是函数,如果不是则抛出异常,该异常会在JS端被接收。
这个函数的最后,保存该JS回调函数到全局变量 g_logCallback 中。

void setFrameDataCallback(const v8::FunctionCallbackInfo& args)
{
    auto isolate = args.GetIsolate();

    // 如果第一个参数不是函数,则抛出错误
    if (!args[0]->IsFunction())
    {
        isolate->ThrowException(v8::Exception::TypeError(
            v8::String::NewFromUtf8(isolate, toUtf8String(L"The first argument must be a function!").c_str())));
        return;
    }

    // 获取回调函数,并保留到全局变量
    auto frameDataCallback = v8::Handle::Cast(args[0]);
    g_frameDataCallback.Reset(isolate, frameDataCallback);
}

这段代码和设置日志回调函数相似,将JS函数注册为回调函数。该函数用来在获取一帧后被调用。

void capture(void *arg)
{
    // 打开摄像头
    cv::VideoCapture cap(0);

    // 如果打开失败,返回错误
    if (!cap.isOpened())
    {
        g_asyncData.type = L"Error";
        g_asyncData.message = L"Open camera failed!";
        g_async.data = &g_asyncData;
        uv_async_send(&g_async);
        return;
    }

    // 人脸识别分类器
    cv::CascadeClassifier faceCascadeClassifier("F:\\opencv\\lib\\opencv\\data\\haarcascades_cuda\\haarcascade_frontalface_alt2.xml");

    // 读取 Frame ,直到退出系统
    while (!g_quit)
    {
        cv::Mat frame;
        if (!cap.read(frame))
        {
            // 读取失败,返回错误
            g_asyncData.type = L"Error";
            g_asyncData.message = L"Read frame failed!";
            g_async.data = &g_asyncData;
            uv_async_send(&g_async);
            break;
        }

        // 进行人脸识别
        std::vector faces;
        faceCascadeClassifier.detectMultiScale(frame, faces);
        // 将人脸识别结果绘制到图片上
        for (const auto& face : faces) 
        {
            cv::rectangle(frame,
                cv::Point(face.x, face.y), 
                cv::Point(face.x + face.width, face.y + face.height), 
                CV_RGB(255, 0, 0), 
                2);
        }

        // 因为 <canvas> 显示的数据是 RGBA 的图像,
        // 因此需要将采集的图片(BGR格式)转换为 RGBA 格式
        cv::Mat converted;
        cv::cvtColor(frame, converted, cv::COLOR_BGR2RGBA);

        // 将转换好的图片赋值给缓存
        memcpy(g_frameBuffer, converted.data, 640 * 480 * 4);

        // 读取成功,返回读到的 Frame
        g_asyncData.type = L"Frame";
        g_asyncData.message = L"Frame captured!";
        g_async.data = &g_asyncData;
        uv_async_send(&g_async);
    }
}

该函数用来采集摄像头数据,并做人脸识别,将识别的结果发送给JS端。
第 4 行我们定义了一个 cv::VideoCapture 对象用于采集摄像头数据。传递 0 给构造函数表示我们使用默认的摄像头。关于更多关于 cv::VideoCapture 的信息,请见官方文档
第 6 ~ 14 行判断摄像头是否被打开,如果没有被打开则发送消息给V8线程。
第 17 行定义了一个 OpenCVcv::CascadeClassifier 对象,在这里该对象用于做人脸识别,注意这里的路径应该根据实际路径做修改。
从第 20 行开始,进入采集、处理循环。第 23 行读取摄像头采集的一帧。第 34 行调用 cv::CascadeClassifierdetectMultiScale()方法获取人脸识别的矩形框。
第 37 行的循环将识别到的矩形框绘制到采集的图像上。由于我们在前端是用 <canvas> 来渲染的,而 <canvas> 支持的图片数据格式是 RGBA 的,因此第 49 行将采集的图片数据格式(BGR)转换为 RGBA
第 52 行我们将读取到的数据拷贝到帧缓存并在第 58 行发送消息给主线程。

// 创建缓存,创建的缓存用于与前端交互
void createFrameBuffer(const v8::FunctionCallbackInfo& args)
{
    auto isolate = args.GetIsolate();
    auto buffer = v8::SharedArrayBuffer::New(isolate, g_frameBuffer, BUFFER_SIZE);
    auto array = v8::Uint8ClampedArray::New(buffer, 0, BUFFER_SIZE);
    args.GetReturnValue().Set(array);

    log(isolate, L"Create frame buffer succeess!");
}

该函数用于创建帧缓存,这里我们将创建的缓存通过返回值传递给JS端,从而实现JS端直接读取帧数据。

void start(const v8::FunctionCallbackInfo& args)
{
    g_quit = false;
    // 创建线程,在线程内处理采集,识别,并将最终结果分发到前端展示
    uv_thread_create(&g_captureThreadId, capture, nullptr);
}

void stop(const v8::FunctionCallbackInfo& args)
{
    g_quit = true;
    uv_thread_join(&g_captureThreadId);

    log(args.GetIsolate(), L"Capture thread stopped.");
}

这两个函数用于开始和结束采集。这两个函数仅仅是调用 uv_thread_create() 函数以开启采集线程,并调用 uv_thread_join() 函数等待线程结束。

// 处理摄像头线程分发的消息
void eventCallback(uv_async_t* handle)
{
    auto isolate = v8::Isolate::GetCurrent();
    AsyncData *asyncData = reinterpret_cast(handle->data);
    if (asyncData->type == L"Error")
    {
        log(isolate, asyncData->message.c_str());
    }
    else if (asyncData->type == L"Frame")
    {
        auto cb = v8::Local::New(isolate, g_frameDataCallback);
        cb->Call(v8::Null(isolate), 0, NULL);
    }
}

该函数用于处理采集线程发过来的消息。这里根据数据的类型做不同的处理。当数据类型为 L"Error" 时,调用日志回调函数打印日志。
当数据类型为 L"Frame" 时,则调用帧回调函数通知JS端我们获取了一帧。

void init(v8::Local exports)
{
    NODE_SET_METHOD(exports, "setLogCallback", setLogCallback);
    NODE_SET_METHOD(exports, "setFrameDataCallback", setFrameDataCallback);
    NODE_SET_METHOD(exports, "createFrameBuffer", createFrameBuffer);
    NODE_SET_METHOD(exports, "start", start);
    NODE_SET_METHOD(exports, "stop", stop);

    // 将事件回调函数注册到V8线程
    uv_async_init(uv_default_loop(), &g_async, eventCallback);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, init)

这里调用 NODE_SET_METHODNODE_MODULE 宏注册我们的接口函数。
注意在 init() 的最后我们调用了 uv_async_init() 来注册事件回调函数。

为了将上面的代码编译为 Native NodeJS 模块,我们需要编写下面的 bingding.gyp 用来通过 node-gyp 进行编译。
注意这里的头文件包含路径和库路径需要根据实际情况设置。

$\underline{binding.gyp}$

{
    "targets": [
        {
            "target_name": "StreamPlayer",
            "sources": [
                "StreamPlayer.cpp"
            ],
            "include_dirs": [
                "G:/libs/opencv/vc14_x64_release/include"
            ],
            "libraries": [
                "G:/libs/opencv/vc14_x64_release/lib/opencv_calib3d340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_core340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_dnn340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_features2d340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_flann340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_highgui340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_imgcodecs340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_imgproc340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_ml340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_objdetect340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_photo340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_shape340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_stitching340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_superres340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_video340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_videoio340.lib",
                "G:/libs/opencv/vc14_x64_release/lib/opencv_videostab340.lib"
            ]
        }
    ]
}

通过Electron展示

首先我们需要建立我们的工程,执行下列命令创建 package.json,选项值默认即可。

npm init

创建后的 package.json 文件如下:

{
  "name": "facedetection",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "install": "node-gyp rebuild"
  },
  "author": "",
  "license": "ISC",
  "gypfile": true
}

由于我们需要用 Electron 来展现识别结果,因此我们需要引入依赖。另外我们还需要 electron-rebuild 来编译我们的 Native NodeJS 代码。
加入依赖后的 package.json 如下:

{
  "name": "facedetection",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "install": "node-gyp rebuild"
  },
  "author": "",
  "license": "ISC",
  "gypfile": true,
  "dependencies": {
    "electron": "1.7.10",
    "electron-rebuild": "1.7.3"
  }
}

现在我们可以安装依赖了:

npm install

安装好 electrong-rebuild 后,我们需要重新编译我们的 Native NodeJS 模块:

.\node_modules\.bin\electron-rebuild.cmd .

接下来我们可以编写主线程 JS 代码了,注意这里必须加入 JS Flag --harmony-sharedarraybuffer 才能使用我们的帧缓存。

$\underline{index.js}$

const {BrowserWindow, globalShortcut, app} = require('electron')
const path = require('path');
const url = require('url');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

function createWindow() {
    // Create the browser window.
    mainWindow = new BrowserWindow({width: 800, height: 600});

    // and load the index.html of the app.
    mainWindow.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }));

    //
    // // Open the DevTools.
    mainWindow.webContents.openDevTools();
    //
    // // Emitted when the window is closed.
    mainWindow.on('closed', function () {
        // Dereference the window object, usually you would store windows
        // in an array if your app supports multi windows, this is the time
        // when you should delete the corresponding element.
        mainWindow = null
    })
}

app.commandLine.appendSwitch('js-flags', '--harmony-sharedarraybuffer');

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', function () {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
});

app.on('activate', function () {
    // On OS X it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (mainWindow === null) {
        createWindow()
    }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

渲染进程的 HTML 文件如下:

$\underline{index.html}$

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script>
        var context;
        var imageDataBuffer = new Uint8ClampedArray(640 * 480 * 4);
        var imageData = new ImageData(imageDataBuffer, 640, 480);

        const streamPlayer = require('./build/Release/StreamPlayer');
        streamPlayer.setLogCallback(function(message){
            console.log(message);
        });
        streamPlayer.setFrameDataCallback(function(){
            imageDataBuffer.set(frameBuffer);
            context.putImageData(imageData, 0, 0);
        });
        var frameBuffer = streamPlayer.createFrameBuffer();

        window.onload = function() {
            var canvas = document.getElementById("canvas");
            context = canvas.getContext('2d');
        }
    </script>
</head>
<body>
    <button onclick="streamPlayer.start();">开始</button>
    <button onclick="streamPlayer.stop();">结束</button><br>
    <canvas width="640" height="480" id="canvas">
        Your browser does not support canvas!
    </canvas>
</body>
</html>

最后,把 OpenCV 所需的 DLL 拷贝到 StreamPlayer.node 所在目录下,通过下面的命令就可以运行我们的程序了:

.\node_modules\electron\dist\electron.exe .

运行效果:

版权声明:本文为原创文章,转载请注明出处。http://cynhard.com/2018/06/02/electron-node-opencv-for-face-detection/

推荐文章