笔者碰到这样的需求,即如何将C++代码产生的图像数据高速的渲染到由Nodejs编写的WEB页面上,而且图像数据以每秒几十帧以上的速度产生,WEB界面显示要平滑连续不卡顿。
针对这样的需求,笔者做了下调研,确定了需要解决如下的关键技术点:
采用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];
- }
笔者使用了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完整代码示例可访问码云