写hello world,对于新手来说很有必要,但是对于希望能玩点更高级的来说,感觉就差那么点意思了,所以我们来写一个海康摄像头抓拍的插件吧(其实眼尖的人,看到我之前建的项目名称,大致能想到了)
这篇文章,会学到这么几个知识点:
1,设置第三方头文件、静态库文件
2,传参,javascript和c++代码之间如何相互传参
3,返回值
4,异常
按官方文档,异常有两种,一种是启用C++异常,一种是禁用C++异常。node-addon-api定义了一个 Napi:Error 类,用于处理异常。
- 启用C++异常
Napi:Error将从 std::exception 继承,这个时候 可以在代码里try-catch,举例如下:
// 抛出异常方式
throw Napi::Error::New(env, "Example exception");
// 捕获异常方式
Napi::Function jsFunctionThatThrows = someObj.As<Napi::Function>();
Napi::Value result;
try {
result = jsFunctionThatThrows({ arg1, arg2 });
} catch (const Error& e) {
cerr << "Caught JavaScript exception: " + e.what();
}
- 不启用C++异常
那么Napi:Error不会继承 std::exception,用法如下:
// 抛出异常(抛出后,可以在js代码里捕获)
Napi::Env env = ...
Napi::Error::New(env, "Example exception").ThrowAsJavaScriptException();
// 异常的传播
Napi::Env env = ...
Napi::Function jsFunctionThatThrows = someObj.As<Napi::Function>();
Napi::Value result = jsFunctionThatThrows({ arg1, arg2 });
if (env.IsExceptionPending()) {
Error e = env.GetAndClearPendingException();
return e.Value();
}
// 处理异常,但不传播
Napi::Env env = ...
Napi::Function jsFunctionThatThrows = someObj.As<Napi::Function>();
Napi::Value result = jsFunctionThatThrows({ arg1, arg2 });
if (env.IsExceptionPending()) {
Napi::Error e = env.GetAndClearPendingException();
cerr << "Caught JavaScript exception: " + e.Message();
}
node-gyp rebuild --debug --arch=$arch
注意以上命令,由于一些第三方c++ DLL,比如海康SDK,分x86和x64版本,所以如果大家需要编译为32位版本,除了引入海康32位版本的SDK以外,还需要指定gyp编译为32位版本,所以上述命令中的 "$arch"需要指定为 “ia32”。
但是如果大家安装的nodejs是64位的,而编译的目标平台指定的是32位的,那么在运行过程中,会报一个无效的32位应用程序的错误。
我这里打算编译64位的版本,所以我下载了64位的海康SDK,先看一下我的目录:
我把海康SDK相关文件,放到了红框所示的目录下,bin文件夹里存放的,是dll文件,include是头文件,lib是静态库文件。
然后配置binding.gyp,设置头文件和库文件:
{
"targets": [
{
# 链接目标
"target_name": "nodecamera",
"cflags!": [
"-fno-exceptions"
],
"cflags_cc!": [
"-fno-exceptions"
],
"sources": [
"src/index.cpp",
"src/KSHikCameraWrapper.cpp"
],
# 头文件搜索路径
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"sdk/hik/include"
],
# 预编译宏
'defines': [
# 启用C++异常
'NAPI_CPP_EXCEPTIONS'
# 禁用C++异常 NAPI_DISABLE_CPP_EXCEPTIONS
],
# 微软编译器附加设定
'msvs_settings': {
'VCCLCompilerTool': {
'ExceptionHandling': 1,
# 如果启用了C++异常,对于微软编译器来说,需要设定异常模型,下面两种方式效果是一样的
# 'AdditionalOptions': ['/EHsc']
}
},
# 静态库
'libraries': [
"../sdk/hik/lib/HCNetSDK.lib"
]
},
{
# 拷贝文件
"target_name": "action_after_build",
"type": "none",
"dependencies": [ "nodecamera" ],
"copies": [{
"destination": "<(module_root_dir)/build/debug",
"files": [
"<(module_root_dir)/sdk/hik/bin/**.*"
]
}]
}
]
}
注意,我的文章是windows下的nodejs c++插件开发,所以我并没有在gyp里设置一些条件来根据操作系统编译。这里有一个小问题,就是“拷贝文件”这块,我发现这样配置,并不能把包括文件夹及其子项的内容都拷贝走,哪位大神如果知道怎么搞的话,可以留言告诉我一下,先谢谢啦。
那么我们开始吧^_^
有过海康SDK开发经验的同学都知道,SDK需要全局一次性的初始化以及最后的清理工作,所以我打算将这部分代码放到全局函数中来实现:
#include <napi.h>
#include <string>
#include <sstream>
#include "KSHikCameraWrapper.h"
// 初始化海康SDK
Napi::Value InitHikSDK(const Napi::CallbackInfo &info)
{
try
{
if (!CKSHikCameraWrapper::g_bSDKInit)
{
if (NET_DVR_Init())
{
CKSHikCameraWrapper::g_bSDKInit = true;
}
else
{
DWORD dwErr = NET_DVR_GetLastError();
std::ostringstream os;
os << "return code: " << dwErr;
Napi::Error::New(info.Env(), os.str()).ThrowAsJavaScriptException();
}
}
return Napi::Boolean::New(info.Env(), true);
}
catch (const Napi::Error &e)
{
std::ostringstream os;
os << "init sdk failed, " << e.what();
Napi::TypeError::New(info.Env(), os.str()).ThrowAsJavaScriptException();
}
return Napi::Boolean::New(info.Env(), false);
}
// 注销海康SDK
void CleanupHikSDK(const Napi::CallbackInfo &info)
{
try
{
if (CKSHikCameraWrapper::g_bSDKInit)
{
if (NET_DVR_Cleanup())
{
CKSHikCameraWrapper::g_bSDKInit = false;
}
}
}
catch (const Napi::Error &e)
{
std::ostringstream os;
os << "cleanup sdk failed, " << e.what();
Napi::TypeError::New(info.Env(), os.str()).ThrowAsJavaScriptException();
}
}
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
exports.Set(
Napi::String::New(env, "initHikSDK"),
Napi::Function::New(env, InitHikSDK));
exports.Set(
Napi::String::New(env, "cleanupHikSDK"),
Napi::Function::New(env, CleanupHikSDK));
return CKSHikCameraWrapper::Init(env, exports);
}
// 最后调用这个宏,把上面定义的C++函数暴露给JAVASCRIPT
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
然后我们编写摄像头的打开、关闭和抓拍,首先是头文件定义:
#pragma once
#include <napi.h>
#include <windows.h>
#include <string>
#include <sstream>
#include "HCNetSDK.h"
class CKSHikCameraWrapper : public Napi::ObjectWrap<CKSHikCameraWrapper>
{
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
static Napi::Value CreateNewItem(const Napi::CallbackInfo &info);
static Napi::FunctionReference constructor;
CKSHikCameraWrapper(const Napi::CallbackInfo &info);
~CKSHikCameraWrapper();
public:
// 打开摄像头
// 需要传入的参数
// 摄像头IP,摄像头端口,账号,密码,通道号
Napi::Value Open(const Napi::CallbackInfo &info);
// 关闭摄像头
Napi::Value Close(const Napi::CallbackInfo &info);
// 抓拍
// 需要传入的参数
// 抓拍文件保存路径
Napi::Value Capture(const Napi::CallbackInfo &info);
public:
static BOOL g_bSDKInit;
private:
LONG m_nUserId;
};
在这里,我使用了 Napi::ObjectWrap 包装类,使用这个类,则在 javascript 代码里,需要用 new 的方式初始化一个对象,比较符合c++程序员的习惯,然后在javascript垃圾回收的时候,会自动调用这个类的析构函数。
接下来是实现代码:
#include "KSHikCameraWrapper.h"
BOOL CKSHikCameraWrapper::g_bSDKInit = FALSE;
Napi::Object CKSHikCameraWrapper::Init(Napi::Env env, Napi::Object exports)
{
Napi::Function func = DefineClass(
env, "KSHikCameraWrapper",
{InstanceMethod("open", &CKSHikCameraWrapper::Open),
InstanceMethod("close", &CKSHikCameraWrapper::Close),
InstanceMethod("capture", &CKSHikCameraWrapper::Capture),
StaticMethod<&CKSHikCameraWrapper::CreateNewItem>("CreateNewItem")});
Napi::FunctionReference *constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("KSHikCameraWrapper", func);
return exports;
}
Napi::Value CKSHikCameraWrapper::CreateNewItem(const Napi::CallbackInfo &info)
{
Napi::FunctionReference *constructor = info.Env().GetInstanceData<Napi::FunctionReference>();
return constructor->New({Napi::Number::New(info.Env(), 42)});
}
CKSHikCameraWrapper::CKSHikCameraWrapper(const Napi::CallbackInfo &info)
: Napi::ObjectWrap<CKSHikCameraWrapper>(info)
{
m_nUserId = -1;
}
CKSHikCameraWrapper::~CKSHikCameraWrapper()
{
}
Napi::Value CKSHikCameraWrapper::Open(const Napi::CallbackInfo &info)
{
if (!CKSHikCameraWrapper::g_bSDKInit)
{
Napi::TypeError::New(info.Env(), "sdk is not inited").ThrowAsJavaScriptException();
return Napi::Boolean::New(info.Env(), false);
}
if (info.Length() != 5)
{
Napi::TypeError::New(info.Env(), "less args").ThrowAsJavaScriptException();
return Napi::Boolean::New(info.Env(), false);
}
if (-1 != m_nUserId)
{
// 已经登录
return Napi::Boolean::New(info.Env(), true);
}
// IP
std::string strIPAddr = info[0].As<Napi::String>();
// 端口
int nPort = info[1].As<Napi::Number>().Int32Value();
// 账号
std::string strAcc = info[2].As<Napi::String>();
// 密码
std::string strPwd = info[3].As<Napi::String>();
NET_DVR_DEVICEINFO_V30 dvrinfo = {0};
m_nUserId = NET_DVR_Login_V30((char *)strIPAddr.c_str(), nPort,
(char *)strAcc.c_str(), (char *)strPwd.c_str(), &dvrinfo);
if (-1 == m_nUserId)
{
// 登录失败,查看一下错误代码
DWORD dwErr = NET_DVR_GetLastError();
std::ostringstream os;
os << "open camera failed, return code: " << dwErr;
return Napi::String::New(info.Env(), os.str());
}
return Napi::Boolean::New(info.Env(), true);
}
Napi::Value CKSHikCameraWrapper::Close(const Napi::CallbackInfo &info)
{
if (-1 != m_nUserId)
{
if (NET_DVR_Logout_V30(m_nUserId))
{
m_nUserId = -1;
return Napi::Boolean::New(info.Env(), true);
}
else
{
DWORD dwErr = NET_DVR_GetLastError();
std::ostringstream os;
os << "close camera failed, return code: " << dwErr;
return Napi::String::New(info.Env(), os.str());
}
}
return Napi::Boolean::New(info.Env(), true);
}
Napi::Value CKSHikCameraWrapper::Capture(const Napi::CallbackInfo &info)
{
if (-1 == m_nUserId)
{
return Napi::String::New(info.Env(), "please connect camera first");
}
if (info.Length() != 1)
{
Napi::TypeError::New(info.Env(), "less args").ThrowAsJavaScriptException();
return Napi::Boolean::New(info.Env(), false);
}
std::string strImageFile = info[0].As<Napi::String>();
NET_DVR_JPEGPARA struJpegPara = {0};
struJpegPara.wPicSize = 19;
struJpegPara.wPicQuality = 2;
if (!NET_DVR_CaptureJPEGPicture(m_nUserId, 0, &struJpegPara, (char *)strImageFile.c_str()))
{
DWORD dwErr = NET_DVR_GetLastError();
std::ostringstream os;
os << "capture failed, return code: " << dwErr;
return Napi::String::New(info.Env(), os.str());
}
return Napi::Boolean::New(info.Env(), true);
}
最后我们来写一个 js 调用看看效果:
// const addon = require('./build/Release/nodecamera.node');
const addon = require('bindings')('nodecamera.node');
var init = false;
try {
init = addon.initHikSDK();
if (true === init) {
var hik = new addon.KSHikCameraWrapper();
var camera = hik.open('192.168.20.121', 8000, 'admin', 'admin123456', 1);
if (true === camera) {
var result = hik.capture('d:\tmp\test.jpg');
if (true === result) {
console.log('抓拍成功');
} else {
console.log(result);
}
hik.close();
} else {
console.log(camera);
}
addon.cleanupHikSDK();
}
} catch (error) {
console.log(error);
}
很遗憾,我手头上用的一个海康摄像头,不支持 NET_DVR_CaptureJPEGPicture 函数抓拍:
再看看控制台输出:
最后我们查一下海康SDK:
好吧,兴致勃勃的心情一下不是很爽了,不过不要紧,我们至少证明了nodejs可以通过c++插件的方式实现很多扩展功能。
还有很多问题还没研究,不过这已经是一个好的开始了,不是吗?推荐大家去看看nodejs c++ addon 的git文档来继续学习:nodejs c/c++ addon