笔者碰到这样的需求,即如何将C++代码产生的图像数据高速的渲染到由Nodejs编写的WEB页面上,而且图像数据以每秒几十帧以上的速度产生,WEB界面显示要平滑连续不卡顿。
针对这样的需求,笔者做了下调研,确定了需要解决如下的关键技术点:
- 如何在C++代码和nodejs代码之间高速交换数据。
- 如何在浏览器前端和Nodejs后端传输和渲染图像数据
如何在C++代码和nodejs代码之间高速交换数据
采用window File Mapping来进行内存共享,实现进程间通信,nodejs可以通过编写addon(插件)的方式来访问window File Mapping 的API。编写nodejs addon插件不是一件容易的事情,好在目前有一种node-addon-api技术可大大简化插件的开发,只要具备一定的C++知识即可编写,具体样例可参考:
GitHub - nodejs/node-addon-api: Module for using Node-API from C++Module for using Node-API from C++. Contribute to nodejs/node-addon-api development by creating an account on GitHub.https://github.com/nodejs/node-addon-api#examples 关于window File Mapping的API使用则很简单,微软官方提供了简单的示例,只需要将这个示例按照node-addon-api的接口要求进行改造一下即可。关于window File Mapping的使用可参考
Creating Named Shared Memory - Win32 apps | Microsoft LearnTo share data, multiple processes can use memory-mapped files that the system paging file stores.https://learn.microsoft.com/en-us/windows/win32/memory/creating-named-shared-memory 根据上面所述,笔者编写了一个demo,生成addon, 在nodejs代码里可以方便调用,例如进程生产数据的代码类似如下:
var addon = require('../build/Release/addon')
var env = require('./env')
var mapName = env.file_mapping_name;
var bufSize = env.image_width*env.image_height;
var fileMap = new addon.FileMap();
//直接返回封装好的Buffer对象
var buf = fileMap.create(mapName,bufSize);
buf[0] = 1;
buf[1] = 2;
这个插件的create方法创建了FileMapping对象并返回Buffer对象,这个方法没有进行内存拷贝操作,是直接引用并封装到javascript里的Buffer对象里,可以利用Buffer对象的方法直接进行数据操作。
而消费者进程则也可以用该addon获取共享内存,消费数据,代码类似如下
var addon = require('../build/Release/addon')
var env = require('./env')
var mapName = env.file_mapping_name;
var bufSize = env.image_width*env.image_height;
var fileMap = new addon.FileMap();
var buf = fileMap.open(mapName,bufSize,0);
if(buf)
{
console.log(`buf size=${buf.length}`);
var value = buf[0];
}
如何在浏览器前端和Nodejs后端传输和渲染图像数据
笔者使用了koa2作为Nodejs的web框架,使用上面提到的插件来获取图像数据,并通过web api返回二进制流数据给前端,代码示例如下:
router.get('/image',async (ctx, next) =>{
const fileMap = new addon.FileMap();
var buf = fileMap.open(env.file_mapping_name, env.image_height*env.image_width,0);
if(!buf)
{
console.log('file mapping is not opened');
ctx.throw(500);
}
ctx.set("Content-Type", "application/octet-stream");
ctx.response.body = buf;
})
而前端页面则采用canvas的双缓冲来绘制图像,并使用requestAninationFrame方法来起到平滑播放动画的效果,代码示例如下:
const downloadData = async () => {
const resp = await axios.get('/image',{responseType: 'arraybuffer'});
return resp.data;
}
const sleep = (interval) => {
return new Promise(resolve => {
setTimeout(resolve, interval);
})
}
var canvas = document.querySelector('#my-canvas');
var context = canvas.getContext('2d');
// to increase performance createImageData method
// should be executed once e.g. before drawing
var image = context.createImageData(canvas.width, canvas.height);
var data = image.data;
var dataView = new Uint32Array(data.buffer);
const height = canvas.height;
const width = canvas.width;
const byteLength = height*width;
function drawPixel(i, value) {
dataView[i] =
(255 << 24) | // alpha
(value << 16) | // blue
(value << 8) | // green
value; // red
}
function swapBuffer() {
context.putImageData(image, 0, 0);
}
const showData = async () => {
var buf = await downloadData();
if(buf.byteLength < byteLength)
{
console.log("error:downloaded buffer's size is smaller than canvas's size");
return;
}
var bufView = new Uint8Array(buf);
var t1 = new Date();
for(var y = 0; y < height; ++y) {
for(var x = 0; x < width; ++x) {
var index = y*height + x;
var gray = bufView[index];
//var color = { r : gray, g: gray, b:gray, a:255 };
drawPixel(index, gray);
}
}
swapBuffer();
var t2 = new Date();
var dt = t2 - t1;
console.log('elapsed time = ' + dt + ' ms');
}
const animate = () => {
showData().then( () =>
window.requestAnimationFrame(animate)
).catch(err => console.log(err));
}
window.requestAnimationFrame(animate)
整个demo完整代码示例可访问码云