【零基础学WebGL】播放视频

1,870 阅读3分钟

前言

如果已经看过【零基础学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);

结尾

完整代码,见这里