功能描述
本文主要介绍如何基于 canvas 实现推流添加水印功能。您可以在这个 demo 中体验开启水印功能。
前提条件
添加水印功能主要使用到了 canvas.captureStream 进行自定义渲染,兼容性情况可参考 canvas.captureStream 兼容性情况 及 TRTC Web SDK 兼容性情况。
实现流程
- 使用 TRTC.createStream 创建 LocalStream,并采集视频流。
- 创建 video 标签播视频流,用于将视频绘制到 canvas 画布中。
- 创建 image 实例,加载水印图片。
- 创建 canvas 标签,并使用 setInterval 将视频和水印绘制到 canvas 画布中。
- 使用 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 使用水印的已知问题: - 
                  iOS 15 以下的版本,canvas.captureStream 采集出的视频流,无法使用 video 标签播放。该问题是 iOS 的缺陷,参考:webkit bug。 
- 
                  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') } }
 
-