Tutorial: 开启水印

开启水印

功能描述

本文主要介绍如何基于 canvas 实现推流添加水印功能。您可以在这个 demo 中体验开启水印功能。

前提条件

添加水印功能主要使用到了 canvas.captureStream 进行自定义渲染,兼容性情况可参考 canvas.captureStream 兼容性情况TRTC Web SDK 兼容性情况

实现流程

  1. 使用 TRTC.createStream 创建 LocalStream,并采集视频流。
  2. 创建 video 标签播视频流,用于将视频绘制到 canvas 画布中。
  3. 创建 image 实例,加载水印图片。
  4. 创建 canvas 标签,并使用 setInterval 将视频和水印绘制到 canvas 画布中。
  5. 使用 canvas.captureStream 从画布中采集视频流,使用 LocalStream.replaceTrack 替换视频流。

代码示例

创建 LocalStream 采集视频流

// 1. 创建 LocalStream 采集视频流
const localStream = TRTC.createStream({ audio: true, video: true });
await localStream.initialize();
let sourceVideoTrack = null;
let intervalId = -1;
let video = null;

封装开启水印函数

// 用于加载水印图片
function loadImage(imageUrl) {
  return new Promise((resolve) => {
    const image = new Image();
    image.src = imageUrl;
    image.onload = () => resolve(image);
  });
}
async function startWaterMark({ localStream, x, y, width, height, imageUrl }) {
  // 2. 创建 video 标签播放视频流
  const video = document.createElement('video');
  sourceVideoTrack = localStream.getVideoTrack();
  const mediaStream = new MediaStream();
  mediaStream.addTrack(sourceVideoTrack);
  video.srcObject = mediaStream;
  await video.play();
  // 3. 加载水印图片
  const image = await loadImage(imageUrl); 
  // 4. 创建 canvas 标签,并使用 setInterval 将视频和水印绘制到 canvas 画布中
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const settings = sourceVideoTrack.getSettings();
  canvas.width = settings.width;
  canvas.height = settings.height;
  intervalId = setInterval(() => {
    // 将视频绘制到画布中
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
    // 将水印图片绘制到画布中,可以控制水印的位置和大小
    ctx.drawImage(image, x, y, width || image.width, height || image.height);
  }, Math.floor(1000 / settings.frameRate)); // 根据帧率计算每次绘制的时间间隔
  // 5. 使用 canvas.captureStream 从画布中采集视频流,使用 LocalStream.replaceTrack 替换视频流
  const canvasStream = canvas.captureStream();
  await localStream.replaceTrack(canvasStream.getVideoTracks()[0]);
}
// 开启水印
await startWaterMark({ localStream, x: 100, y: 100, imageUrl: './xxx.png'}); // 需传入图片 url
localStream.play('elementId'); // 传入 elementId 播放 LocalStream
// 6. 推流
await client.publish(localStream);

封装关闭水印函数

// 关闭水印
async function stopWaterMark() {
  if (intervalId) {
    clearInterval(intervalId);
    intervalId = -1;
    await localStream.replaceTrack(sourceVideoTrack);
    if (video) {
      video.srcObject = null;
      video = null;
    }
  }
}

注意事项

  • 上述代码示例使用了 setInterval 来绘制 canvas 画布,若 js 线程出现拥挤时,绘制可能会出现卡顿。可以考虑使用 requestAnimationFrame 来替代 setInterval,以提升渲染性能。

  • 上述代码只绘制了一个水印,可能您的业务场景需要让水印铺满整个视频,此时有两种方案,一种是制作一张宽高大于视频窗口的水印图片,绘制一次让水印图片铺满画布即可;另一种方案是多次调用 CanvasRenderingContext2D.drawImage,多次绘制同一张水印文件,使水印铺满整个画布。

  • 您可以使用 CanvasRenderingContext2D.rotate 接口让水印旋转一定的角度。需要注意的是:在绘制完水印后,需要再次调用该接口旋转回来,以避免视频绘制也出现旋转。

    setInterval(() => {
      // 将视频绘制到画布中
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
    	// 1. 让水印旋转30度
    	ctx.rotate((30 * Math.PI) / 180);
      ctx.drawImage(image, x, y, width || image.width, height || image.height);
    	// 2. 水印绘制完后,需旋转回来,以避免下一次绘制时,视频也出现旋转30度的情况发生。
    	ctx.rotate((-30 * Math.PI) / 180);
    }, Math.floor(1000 / frameRate))
    
  • 在 iOS/Mac Safari 使用水印的已知问题:

    1. iOS 15 以下的版本,canvas.captureStream 采集出的视频流,无法使用 video 标签播放。该问题是 iOS 的缺陷,参考:webkit bug

    2. Mac Safari 15.0+ 的版本,canvas.captureStream 采集出的视频流,使用 video 标签播放会出现红屏现象。该问题是 Mac Safari 的缺陷,参考:webkit bug

      规避方案:

      在 iOS 15 以下、Mac Safari 15 以上的版本,不使用 video 标签播放 canvas.captureStream 采集出的视频流,而是直接在页面中使用 canvas 渲染。

      // 1. 在 startWaterMark 中增加如下代码,将 canvas 放置 dom 中渲染渲染
      async function startWaterMark() {
        // ... 
        // 可使用第三方的 userAgent 解析库,判断 iOS Mac 的版本
        if (IOS_VERSION < 15 || MAC_SAFARI_VERSION >= 15) {
          // 停止播放
          localStream.stop();
          // 将 canvas 放置到 DOM 中渲染。
          canvas.style.width = '100%';
          canvas.style.height = '100%';
          canvas.style.objectFit = 'cover';
          canvas.style.transform = 'rotateY(180deg)'; // 本地视频是镜像显示的,此处对齐
          // 'local_stream' 为 localStream.play(elementId) 传入的 elementId
          document.querySelector('#local_stream').appendChild(canvas);
        }
      }
      // 2. 在 stopWaterMark 中增加如下代码,在关闭水印时,移除 canvas,恢复使用 video 标签播放。
      async function stopWaterMark() {
        // ...
        if (IOS_VERSION < 15 || MAC_SAFARI_VERSION >= 15) {
          // 'local_stream' 为 localStream.play(elementId) 传入的 elementId
          document.querySelector('#local_stream').removeChild(this.canvas);
          this.canvas = null;
          localStream.play('local_stream')
        }
      }