跳到主要内容

水印处理

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

  • 方案一:基于 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。

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

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

步骤二:播放音视频资源​

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

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

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

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

步骤三:加载水印图片​

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

typescript
const loadImage = (imageUrl: string) => {
return new Promise<HTMLImageElement>((resolve) => {
const image = new Image();
image.src = imageUrl;
image.onload = () => resolve(image);
});
}

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

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

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

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

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

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

循环绘制。setInterval 和 requestAnimationFrame 都可以进行循环绘制。

typescript
setInterval(loop, 1000/30);
提示

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

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

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

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

typescript
const canvasStream = canvasEl.captureStream();

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

typescript
/**
* 转化带水印的视频 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 及媒体类型重复,后者将覆盖前者进行发布。

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