前言
如果已经看过【零基础学WebGL】绘制图片,或已经了解WebGL绘制基础。
H5播放视频,通常可以使用HTML Video实现。但如果考虑HTML Video在移动端的兼容性,或者需要进行视频处理,比如添加滤镜效果,那么WebGL是一种可选的手段。
本文定义playVideoInWebGL方法,包含了三个环节:
- 环境准备:负责初始化webgl上下文、着色器程序。和之前的文章是一致的,本文不再赘述。
- 纹理数据准备:负责初始化纹理对象、视频数据、视频封面数据;
- 动画播放:负责循环播放视频
const playVideoInWebGL = async () => {
/* *** 环境准备 *** */
// 获得webgl上下文
const gl = createContext("container", 300, 300);
if (!gl) return;
gl.clearColor(0, 0, 0, 0);
// 分别创建顶点着色器和片段着色器
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertextSource);
if (!vertexShader) return;
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
if (!fragmentShader) return;
// 创建应用程序
const program = createProgram(gl, [vertexShader, fragmentShader]);
if (!program) return;
/* *** 准备纹理数据 *** */
...
/* *** 播放动画 *** */
...
};
本文主要讲解纹理数据准备和动画播放两个环节。
纹理数据准备
这个环节,会启动视频的下载,然后使用一张图片作为纹理对象的初始数据。
const playVideoInWebGL = async () => {
...
/* *** 准备纹理数据 *** */
// 下载视频,并确保视频有可用数据
const video = setupVideo('demo.mp4'); // 不阻塞绘制
// 下载图片,作为初始纹理
const image = await createImage('cover.png'); // 阻塞绘制
const texture = imageTexture(gl, image);
/* *** 播放动画 *** */
...
};
创建一个video对象,设置静音和自动播放,然后,监听 video的播放事件playing和播放位置发生变化事件timeupdate,当两个事件都触发之后,才表示视频对象可用。
var copyVideo = false;
const setupVideo = (url: string) => {
const video = document.createElement('video');
var playing = false;
var timeupdate = false;
video.autoplay = true;
video.muted = true;
video.loop = true;
// Waiting for these 2 events ensures, there is data in the video
video.addEventListener('playing', function() {
playing = true;
checkReady();
}, true);
video.addEventListener('timeupdate', function() {
timeupdate = true;
checkReady();
}, true);
video.src = url;
video.play();
function checkReady() {
if (playing && timeupdate) {
copyVideo = true;
}
}
return video;
}
由于等待视频可用(playing和timeupdate都触发)的时长,往往比下载一个图片需要更多的时间。从更好的用户体验角度出发,我们先使用一张图片作为初始纹理,然后等视频对象可用之后,再更新纹理。
图片纹理的初始化过程,和前文是一致的:
- 下载图片;
- 创建纹理对象,并绑定到目标点 gl.TEXTURE_2D;
- 调用 gl.texParameteri 设置纹理参数;
- 调用 gl.texImage2D 指定图片对象填充纹理对象;
const createImage = (src: string) => {
return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve(img);
}
img.onerror = (e) => {
reject(e);
}
img.crossOrigin = 'crossOrigin';
img.src = src;
});
}
const imageTexture = (gl: WebGLRenderingContext, image: HTMLImageElement) => {
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
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);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
return texture!;
}
动画播放
经过上一个环节,我们得到一个已经初始化完成的纹理对象。接下来,我们需要使用WebGL绘制。
定义drawTexture方法。首先清空canvas画布,然后给顶点着色器设置顶点数据和纹理坐标,给片元着色器设置纹理,最后调用drawArrays执行绘制。
const drawTexture = (gl: WebGLRenderingContext, program: WebGLProgram, texture: WebGLTexture) => {
// 绘制前,清理canvas
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 指定颜色缓冲区的清除值
gl.clearDepth(1.0); // 指定深度缓冲区的清除值
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 设置顶点坐标属性
const vertexPostion = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0];
setAttribute(gl, program, vertexPostion, "a_position");
// 设置纹理坐标属性
const txtCoordData = [0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0];
setAttribute(gl, program, txtCoordData, "a_texCoord");
/* ***** 纹理设置 ***** */
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
const sampler = gl.getUniformLocation(program, "u_image");
gl.uniform1i(sampler, 0);
// gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
每次执行drawTexture,都接受一个texture参数,如果我们随着视频播放,不断地更新texture变量,就可以实现WebGL播放视频的效果。
const playVideoInWebGL = async () => {
...
/* *** 播放动画 *** */
const animation = () => {
if (copyVideo) { // 拿到视频数据后,更新纹理图片
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
}
drawTexture(gl, program, texture); // 重新绘制纹理
requestAnimationFrame(animation);
}
animation();
};
这里,我们使用requestAnimationFrame构建一个动画循环,循环主体会调用 gl.texImage2D 设置视频对象video填充纹理对象texture。自此我们会发现gl.texImage2D不仅可以接受一个HTMLImageElement对象作为纹理,也可以是HTMLVideoElement。
void gl.texImage2D(target, level, internalformat, format, type, HTMLImageElement);
void gl.texImage2D(target, level, internalformat, format, type, HTMLVideoElement);
结尾
完整代码,见这里。