写在前面
本文用wasm和js原生分别对视频增加视频蒙层,并通过最终的视频帧数和具体的渲染函数运行时间对比这两者对数据密集逻辑的处理速度。
该文章大部分代码来自于航老师课程:time.geekbang.org/column/intr…
文章中所出现的代码存于这里
关键代码说明
视频播放逻辑
// html
<canvas class="canvas"></canvas>
<video
class="video"
type="video/mp4"
muted="muted"
loop="true"
autoplay="true"
src="./media/video.mp4"
>
// js
let video = document.querySelector('.video');
let canvas = document.querySelector('.canvas');
let promise = video.play();
if (promise !== undefined) {
promise.catch(error => {
console.error("The video can not autoplay!")
});
}
// <video> 视频资源加载完毕后执行;
video.addEventListener("loadeddata", () => {
canvas.setAttribute('height', video.videoHeight);
canvas.setAttribute('width', video.videoWidth);
clientX = canvas.clientWidth;
clientY = canvas.clientHeight;
draw();
});
function draw() {
context.drawImage(video, 0, 0);
// 获得 <canvas> 上当前帧对应画面的像素数组;
const pixels = context.getImageData(0, 0, video.videoWidth, video.videoHeight);
// append image onto the canvas.
context.putImageData(pixels, 0, 0);
requestAnimationFrame(draw);
}
- CanvasRenderingContext2D.drawImage 接收的第一个参数MDN说明如下:
可以传入HTMLVideoElement元素,将当前帧渲染在canvas上。
通过上述流程,便可以去除视频的各个像素,并进行处理,从而达到增加视频蒙层的效果。
js原生处理逻辑
function jsConvFilter(data, width, height, kernel) {
const divisor = 4; // 分量调节参数;
const h = kernel.length, w = h; // 保存卷积核数组的宽和高;
const half = Math.floor(h / 2);
// 根据卷积核的大小来忽略对边缘像素的处理;
for (let y = half; y < height - half; ++y) {
for (let x = half; x < width - half; ++x) {
// 每个像素点在像素分量数组中的起始位置;
const px = (y * width + x) * 4;
let r = 0, g = 0, b = 0;
// 与卷积核矩阵数组进行运算;
for (let cy = 0; cy < h; ++cy) {
for (let cx = 0; cx < w; ++cx) {
// 获取卷积核矩阵所覆盖位置的每一个像素的起始偏移位置;
const cpx = ((y + (cy - half)) * width + (x + (cx - half))) * 4;
// 对卷积核中心像素点的 RGB 各分量进行卷积计算(累加);
r += data[cpx + 0] * kernel[cy][cx];
g += data[cpx + 1] * kernel[cy][cx];
b += data[cpx + 2] * kernel[cy][cx];
}
}
// 处理 RGB 三个分量的卷积结果;
data[px + 0] = ((r / divisor) > 255) ? 255 : ((r / divisor) < 0) ? 0 : r / divisor;
data[px + 1] = ((g / divisor) > 255) ? 255 : ((g / divisor) < 0) ? 0 : g / divisor;
data[px + 2] = ((b / divisor) > 255) ? 255 : ((b / divisor) < 0) ? 0 : b / divisor;
}
}
return data;
}
视频逻辑逻辑其实就是大量的矩阵卷积计算,从而对视频所有的像素点位的rgb色值进行调整。
其实并不需要了解具体计算逻辑,只需要知道这是一个非常庞大的数据计算就行
wasm处理逻辑
使用c++代码生成wasm文件
#include <emscripten.h>
#include <cmath>
#define KH 3
#define KW 3
char kernel[KH][KW];
unsigned char data[921600];
extern "C" {
// 获取卷积核数组的首地址;
EMSCRIPTEN_KEEPALIVE auto* cppGetkernelPtr() { return kernel; }
// 获取帧像素数组的首地址;
EMSCRIPTEN_KEEPALIVE auto* cppGetDataPtr() { return data; }
// 滤镜函数;
EMSCRIPTEN_KEEPALIVE void cppConvFilter(
int width,
int height,
int divisor) {
const int half = std::floor(KH / 2);
for (int y = half; y < height - half; ++y) {
for (int x = half; x < width - half; ++x) {
int px = (y * width + x) * 4;
int r = 0, g = 0, b = 0;
for (int cy = 0; cy < KH; ++cy) {
for (int cx = 0; cx < KW; ++cx) {
const int cpx = ((y + (cy - half)) * width + (x + (cx - half))) * 4;
r += data[cpx + 0] * kernel[cy][cx];
g += data[cpx + 1] * kernel[cy][cx];
b += data[cpx + 2] * kernel[cy][cx];
}
}
data[px + 0] = ((r / divisor) > 255) ? 255 : ((r / divisor) < 0) ? 0 : r / divisor;
data[px + 1] = ((g / divisor) > 255) ? 255 : ((g / divisor) < 0) ? 0 : g / divisor;
data[px + 2] = ((b / divisor) > 255) ? 255 : ((b / divisor) < 0) ? 0 : b / divisor;
}
}
}
}
其实可以看出来,c++代码和上面的js代码结构非常类似,也就是用来做卷积运算的。
然后可以参考这篇文章将c++代码转化成wasm文件
生成的wasm已将需要的处理方法导出
然后在js代码中使用WebAssembly.instantiateStreaming获取,再转成Uint8Array处理即可
时间帧计算
只需要取20次draw函数的执行时间,算平均值就可以得到大概的帧时间,从而得到fps
const currentTimestamp = performance.now();
fps.innerHTML = calcFPS(currentTimestamp - timestamp);
timestamp = currentTimestamp;
查看结果
(图片有20M的限制,gif图放不上去,就只能整静图了)
在不开启渲染的情况下,视频的fps还是能稳定到60的,在火焰图中也能看到draw函数只需要drawImage就行,16,7左右可以执行完
使用js原生渲染(加了个灰白效果),每次处理时间大概50ms,fps也降低到16.7左右
使用wasm的渲染,灰白效果肉眼观察是与js一致的,所读提升了将近60%,提升确实是很大的。
查看了js heap发现两种方法的内容占比也没什么差异。
结论
所以wasm在这种大数据计算的场景下还是可以有显著提升的。
平时常见的应该有视频解码(easyPlayer就是用的wasm),文件上传时的hash编码,后续有机会希望在项目中进行尝试。