如何通过 canvas 实现视频水印

融云支持在视频流上添加水印。添加水印有多种控制方式,本文仅介绍方案一:

  • 方案一:基于 canvas 实现添加水印,并发布带水印的视频流。客户端发布的视频流即带有图片水印,因此订阅分流或合流的直播观众均会看到带水印的视频流。
  • 方案二:使用客户端 SDK 提供构建 MCU 配置的方法 addPictureWaterMark,为合流中为单道视频流(子视图)添加图片水印。仅订阅合流的直播观众可看到水印。详见合流布局
  • 方案三:使用服务端 API 的 /rtc/mcu/config 接口,在服务端处理,添加时间戳水印、文字水印或图片水印。这种方式支持为单人视频流或合流视频添加水印,但只有订阅合流的观众可看到带水印的视频流。本文不介绍服务端的处理方案,如有需要,请参见服务端文档直播合流

方案一、二适用于实现 App 客户端用户自主添加个性化水印,例如每个主播可自行设置设置贴纸、挂件等;方案三更适用于由 App 添加统一风格的水印。

客户端与服务端添加的水印相互独立。如果同时使用,则订阅合流的观众可能会看到水印叠加。

前提条件

添加水印功能主要使用到了 canvas.captureStream 捕获 canvas 中绘制出的流对象。兼容性情况可参考 canvas.captureStream 兼容性情况

实现流程

  1. 采集音视频资源。
  2. 创建 video 标签播放视频流,用于将视频绘制到 canvas 画布中。
  3. 创建 image 实例,加载水印图片。
  4. 创建 canvas 标签,将视频和水印绘制到 canvas 画布中。
  5. 使用 canvas.captureStream 从画布中采集视频轨道,转为融云 RTC 视频轨道,通过 RTC SDK 的 publish 接口发布到房间中。

步骤一:采集音视频资源

以获取摄像头、麦克风的资源为例,使用浏览器原生 API MediaDevices.getUserMedia() 获取 mediaStream

 const mediaStream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: {
    width: 640,
    height: 480
  }
})

如需为屏幕共享需要添加水印,您也可以从屏幕分享源采集获得资源。如果播放文件,可以从文件获取资源。

步骤二:播放音视频资源

创建播放视频的 video 元素。元素宽高需与采集的视频资源的分辨率宽高一致,避免拉伸。本端不需要播放自己的声音,因此示例中已将音轨作静音处理。

 // 使用 video 标签播放摄像头视频;不播放自己的声音,避免回声
<video muted id="videoEl" style="display:none; width:640px; height:480px"></video>

播放资源。以下示例使用了 srcObject

/**
* 播放音、视频
*/
const videoEl = document.getElementById('videoEl');
videoEl.srcObject = mediaStream;

步骤三:加载水印图片

创建 image 实例,加载水印图片。注意,加载线上图片时,需允许跨域访问。参见允许图片和 canvas 跨源使用

 const loadImage = (imageUrl) => {
  return new Promise((resolve) => {
    const image = new Image();
    image.src = imageUrl;
    image.onload = () => resolve(image);
  });
}

const image = await loadImage('/assets/image/watermark.jpg')

步骤四:将视频和水印绘制到 canvas 画布中

准备 canvas 画布元素,元素大小也需与采集的视频分辨率一致。

 // 绘制视频和水印
<canvas id="canvasEl" style="width:640px; height:480px"></canvas>

在 canvas 画布上绘制视频和水印。

 const canvasEl = document.getElementById('canvasEl');
const loop = () => {
  const ctx = canvasEl.getContext('2d');
  // 将视频绘制到画布中
  ctx.drawImage(videoEl, 0, 0, 640, 480);
  // 将水印图片绘制到画布中,可以控制水印的位置和大小
  ctx.drawImage(image, 0, 0, image.width, image.height);
}

循环绘制。setIntervalrequestAnimationFrame 都可以进行循环绘制。

setInterval(loop, 1000/30);

提示:原始视频渲染和解码不占 JS 线程,但循环绘制 canvas 流程跟其他 JS 执行代码处于同一线程,会因渲染进程阻塞而造成卡顿。

  • setInterval:存在执行间隔不精确的问题。delay 参数只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间,如果队列前面已经加入了其他任务,动画就必须等前面的任务执行完才可以执行。
  • requestAnimationFrame: 限制必须停留在绘制页面。可保证执行步伐与系统的绘制频率一致,即保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。但是 requestAnimationFrame 运行在后台标签页时,会被暂停调用以提升性能和电池寿命,使用 requestAnimationFrame 时,需保持停留在绘制页面。

步骤五:发布带水印的视频流

从 canvas 元素中抓取带水印的视频流。参见 captureStream

const canvasStream = canvasEl.captureStream();

将带水印的视频 track 和之前获取的音频 track 转为融云 publish 接口接受的入参类型。这一步需要调用融云 SDK 的 createLocalVideoTrackcreateLocalAudioTrack 方法。

/**
* 转化带水印的视频 track 为 融云 publish 接口参数类型
*/
const captureVideoTrack = canvasStream.getVideoTracks()[0];
const audioTrack = mediaStream.getAudioTracks()[0];
const { track: newVideoTrack } = await rtcClient.createLocalVideoTrack('RongCloudRTC', captureVideoTrack)
const { track: newAudioTrack } = await rtcClient.createLocalAudioTrack('RongCloudRTC', audioTrack);

在房间中发布音频和带水印的视频。注意,您需要在加入房间时获取 RCLivingRoom 实例。若发布的资源 tag 及媒体类型重复,后者将覆盖前者进行发布。

/**
* 发布音频和带水印的视频
* crtRoom 为加房间返回的 room 对象
*/
crtRoom.publish([newVideoTrack, newAudioTrack]);