透明视频

透明视频

引言,又做了一个迭代和视频领域相关的内容,学习到了一些和视频领域有关的知识,了解了一下现在市面上通用的透明视频解决方案,简单的沉淀一下。

透明的图片我们都见过,哪透明的视频呢?

上篇文章有简单的说过,目前只有谷歌自研的 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 里面拿到图片的像素的 apigetImageData,拿到的值是 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 的原因,暂时无解。

参考链接

juejin.cn/post/734381...

juejin.cn/post/732542...

stackoverflow.com/questions/4...

blog.gskinner.com/archives/20...

juejin.cn/post/688567...

rotato.app/blog/transp...

相关推荐
鱼樱前端8 分钟前
全前端需要的工程化能力之 Vue3 + TypeScript + Vite 工程化项目搭建最佳实践
前端·vue.js
明远湖之鱼9 分钟前
手把手带你实现 Vite+React 的简易 SSR 改造【含部分原理讲解】
前端·react.js·vite
野生的程序媛28 分钟前
重生之我在学Vue--第10天 Vue 3 项目收尾与部署
前端·javascript·vue.js
烟锁池塘柳01 小时前
技术栈的概念及其组成部分的介绍
前端·后端·web
加减法原则1 小时前
面试题之虚拟DOM
前端
故事与他6452 小时前
Tomato靶机攻略
android·linux·服务器·前端·网络·web安全·postcss
jtymyxmz2 小时前
mac 苍穹外卖 前端环境配置
前端
烛阴2 小时前
JavaScript Rest 参数:新手也能轻松掌握的进阶技巧!
前端·javascript
chenchihwen2 小时前
ITSM统计分析:提升IT服务管理效能 实施步骤与操作说明
java·前端·数据库
陌上烟雨寒2 小时前
es6 尚硅谷 学习
前端·学习·es6