透明视频
引言,又做了一个迭代和视频领域相关的内容,学习到了一些和视频领域有关的知识,了解了一下现在市面上通用的透明视频解决方案,简单的沉淀一下。
透明的图片我们都见过,哪透明的视频呢?
上篇文章有简单的说过,目前只有谷歌自研的 vp8/vp9
编码的 webm
格式的视频才支持真正的 alpha
通道。但是 webm
格式的视频又有 Safari
兼容性问题,有没有一种方案能让通用的 mp4
格式的视频支持透明呢?
MP4
主流的编码用的是 h.264
或者 h.265
,但是 它两 都不支持 alpha
通道。从根源上就没有办法让 mp4
支持透明。
那我们只能另辟蹊径,比如,抠图。
很快啊,我们就想到了 canvas
。但是要抠图的话,我们应该抠哪些区域呢?
于是就需要有一份文件告诉我们要抠哪些区域。
先说两个开源的库:腾讯开源的 vap,以及 YY
团队 参考 vap
开源的 yyeva
核心原理都一样。我们需要有一份如下的视频,一半是黑白,一般是正常的画面
原理就是,一般是正常的画面我们就不管,另一半黑白其实是 alpha
通道,alpha
通道的每一位 rgb
的值都是一样的,值就是对应的透明度
- 全透明,
opacity=0
,转成rgb
就是rgb(0, 0, 0)
,也就是黑色 - 半透明,
opacity=0.5
,转成rgb
就是rgb(128, 128, 128)
,也就是灰 - 不透明,
opacity=1
,转成rgb
就是rgb(255, 255, 255)
,也就是白
CanvasRender
在 canvas
里面拿到图片的像素的 api
是 getImageData
,拿到的值是 rgba 四位,因为没有 alpha 通道,所以第四位值就固定是 255 不透明
canvas 渲染一帧的基本原理如下
js
const draw = () => {
if (!ctx1Ref.current || !ctx2Ref.current || !videoRef.current) return;
ctx1Ref.current.drawImage(videoRef.current, 0, 0, videoInfo.width, videoInfo.height);
const alphaData = ctx1Ref.current.getImageData(0, 0, videoInfo.width / 2, videoInfo.height); // 左边像素信息
const imageData = ctx1Ref.current.getImageData(videoInfo.width / 2, 0, videoInfo.width / 2, videoInfo.height); // 右边像素信息
for (let y = 0; y < imageData.height; y++) {
for (let x = 0; x < imageData.width; x++) {
const index = (x + y * imageData.width) * 4;
imageData.data[index + 3] = alphaData.data[index]; // 根据 alpha 值设置透明度
}
}
ctx2Ref.current.putImageData(imageData, 0, 0);
};
不过性能实在是。。。
WebglRender
在 vap 内部使用到了 webgl
核心代码如下
tsx
import { useEffect, useRef } from 'react';
import { useUpdate } from 'ahooks';
import { createBuffer, createFragmentShader, createProgram, createTexture, createVertexShader } from './utils';
let videoInfo = { width: 0, height: 0 };
let gl: WebGLRenderingContext;
function WebglRender() {
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafId = useRef<number>();
const _update = useUpdate();
const draw = () => {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, videoRef.current!); // 配置纹理图像
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // 绘制
};
const initWebgl = () => {
if (!canvasRef.current) return;
canvasRef.current.width = videoInfo.width / 2;
canvasRef.current.height = videoInfo.height;
const op: WebGLContextAttributes = {
alpha: true, //指示Canvas是否含有透明通道,若设置为false不透明,如果Canvas下叠加了其他元素时,可以在绘制时提升一些性能
antialias: false, //绘制时是否开启抗锯齿功能
depth: true, //是否开启深度缓冲功能
failIfMajorPerformanceCaveat: false, //性能较低时,将不允许创建context。也就是是getContext()返回null [ios 会因此产生问题]
premultipliedAlpha: true, //将alpha通道预先乘入rgb通道内,以提高合成性能
stencil: false, //是否开启模板缓冲功能
preserveDrawingBuffer: false, //是否保留缓冲区数据,如果你需要读取像素,或者复用绘制到主屏幕上的图像
};
if (!gl) {
gl = canvasRef.current.getContext('webgl', op) || (canvasRef.current.getContext('experimental-webgl') as WebGLRenderingContext);
gl.disable(gl.BLEND);
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
}
gl.viewport(0, 0, canvasRef.current.width, canvasRef.current.height);
const vertexShader = createVertexShader(gl); // 创建顶点着色器
const fragmentShader = createFragmentShader(gl); // 创建片元着色器
const program = createProgram(gl, vertexShader, fragmentShader); // 创建着色器程序
const buffer = createBuffer(gl); // 创建缓冲
// 绑定缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, buffer.position);
const aPosition = gl.getAttribLocation(program, 'a_position'); // 允许属性读取,将缓冲区的值分配给特定的属性
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0); // 顶点着色器位置
gl.bindBuffer(gl.ARRAY_BUFFER, buffer.texture);
const aTexCoord = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(aTexCoord);
gl.vertexAttribPointer(aTexCoord, 2, gl.FLOAT, false, 0, 0);
// 绑定纹理
const texture = createTexture(gl);
gl.bindTexture(gl.TEXTURE_2D, texture);
const scaleLocation = gl.getUniformLocation(program, 'u_scale');
gl.uniform2fv(scaleLocation, [1, 1]); // 设置分辨率
};
const handelLoadeddata = () => {
if (!canvasRef.current || !videoRef.current) return;
videoInfo = { width: videoRef.current.videoWidth, height: videoRef.current.videoHeight };
_update();
initWebgl();
draw();
};
const handelPlay = () => {
const loop = () => {
draw();
rafId.current = requestAnimationFrame(loop);
};
loop();
};
const handelPause = () => {
rafId.current && cancelAnimationFrame(rafId.current);
};
useEffect(() => {
videoRef.current?.addEventListener('loadeddata', handelLoadeddata);
videoRef.current?.addEventListener('play', handelPlay);
videoRef.current?.addEventListener('pause', handelPause);
return () => {
videoRef.current?.removeEventListener('loadeddata', handelLoadeddata);
videoRef.current?.removeEventListener('play', handelPlay);
videoRef.current?.removeEventListener('pause', handelPause);
rafId.current && cancelAnimationFrame(rafId.current);
};
}, []);
return (
<div>
<video
style={{ width: videoInfo.width / 2, height: videoInfo.height / 2 }}
ref={videoRef}
src="./ip.mp4"
muted
preload="auto"
autoPlay={false}
crossOrigin="anonymous"
controls
loop
></video>
<div style={{ backgroundColor: 'green' }}>
<canvas style={{ width: videoInfo.width / 4, height: videoInfo.height / 2 }} ref={canvasRef}></canvas>
</div>
</div>
);
}
export default WebglRender;
ts
// utils.ts
// 片元着色器 glsl 代码
const vsSource = `
attribute vec2 a_position; // 接受顶点坐标
attribute vec2 a_texCoord; // 接受纹理坐标
varying vec2 v_texCoord; // 传递纹理坐标给片元着色器
uniform vec2 u_scale;
void main(void) {
gl_Position = vec4(a_position, 0.0, 1.0); // 设置坐标
v_texCoord = a_texCoord; // 设置纹理坐标
}
`;
// 片元着色器 glsl 代码
const fsSource = `
precision lowp float;
varying vec2 v_texCoord;
uniform sampler2D u_sampler;
void main(void) {
gl_FragColor = vec4(texture2D(u_sampler, v_texCoord).rgb, texture2D(u_sampler, v_texCoord+vec2(-0.5, 0)).r);
}
`;
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type)!; // 创建着色器
gl.shaderSource(shader, source); // 给着色器指定源码
gl.compileShader(shader); // 编译着色器
// if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
// console.error(gl.getShaderInfoLog(shader))
// }
return shader;
}
/** 创建顶点着色器 */
export function createVertexShader(gl: WebGLRenderingContext, source?: string) {
return createShader(gl, gl.VERTEX_SHADER, source || vsSource);
}
/** 创建片元着色器 */
export function createFragmentShader(gl: WebGLRenderingContext, source?: string) {
return createShader(gl, gl.FRAGMENT_SHADER, source || fsSource);
}
/** 创建着色器程序 */
export function createProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader) {
const program = gl.createProgram()!; // 创建程序对象
gl.attachShader(program, vertexShader); // 关联程序对象和着色器
gl.attachShader(program, fragmentShader);
gl.linkProgram(program); // 连接程序对象
gl.useProgram(program); // 使用程序对象
return program;
}
/** 创建缓冲区 */
export function createBuffer(gl: WebGLRenderingContext) {
const positionVertex = new Float32Array([-1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0]);
const positionBuffer = gl.createBuffer(); // 创建buffer
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 把缓冲区对象绑定到目标
gl.bufferData(gl.ARRAY_BUFFER, positionVertex, gl.STATIC_DRAW); // 向缓冲区对象写入刚定义的顶点数据
const textureBuffer = gl.createBuffer();
const textureVertex = new Float32Array([0.5, 1.0, 1.0, 1.0, 0.5, 0.0, 1.0, 0.0]); // 这里将纹理左半部分映射到整个画布上
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, textureVertex, gl.STATIC_DRAW);
return {
position: positionBuffer,
texture: textureBuffer,
};
}
/** 创建纹理 */
export function createTexture(gl: WebGLRenderingContext) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// 对纹理图像进行y轴反转,因为WebGL纹理坐标系统的t轴(分为t轴和s轴)的方向和图片的坐标系统Y轴方向相反。因此将Y轴进行反转。
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return texture;
}
GLSL(OpenGL Shading Language) 全称 OpenGL 着色语言,是用来在 OpenGL 中着色编程的语言。
canvas.getContext('webgl') 获取 canvas 的 WebGL 绘图上下文,然后来执行 GLSL 代码,调用 GPU,性能提升巨大
总结
vap 是只支持 webgl,yyeav 再次基础上还支持了 canvas 以及 webgl2,理论上 yyeva 会优于 vap,他两的核心逻辑没啥差别。
不过既然都提到了 webgl ,不知道这块的大哥 three.js 能不能做
因为公司业务,在小程序上使用 yyeva 遇到了严重的性能问题,好像是小程序底层 webgl 的原因,暂时无解。
参考链接
stackoverflow.com/questions/4...