水印处理
融云支持在视频流上添加水印。添加水印有多种控制方式,本文仅介绍方案一:
- 方案一:基于 canvas 实现添加水印,并发布带水印的视频流。客户端发布的视频流即带有图片水印,因此订阅分流或合流的直播观众均会看到带水印的视频流。
- 方案二:使用客户端 SDK 提供构建 MCU 配置的方法
addPictureWaterMark
,为合流中为单道视频流(子视图)添加图片水印。仅订阅合流的直播观众可看到水印。详见合流布局。 - 方案三:使用服务端 API 的
/rtc/mcu/config
接口,在服务端处理,添加时间戳水印、文字水印或图片水印。这种方式支持为单人视频流或合流视频添加水印,但只有订阅合流的观众可看到带水印的视频流。本文不介绍服务端的处理方案,如有需要,请参见服务端文档直播合流。
方案一、二适用于实现 App 客户端用户自主添加个性化水印,例如每个主播可自行设置贴纸、挂件等;方案三更适用于由 App 添加统一风格的水印。
客户端与服务端添加的水印相互独立。如果同时使用,则订阅合流的观众可能会看到水印叠加。
前提条件
添加水印功能主要使用到了 canvas.captureStream 捕获 canvas 中绘制出的流对象。兼容性情况可参考 canvas.captureStream 兼容性情况。
实现流程
- 采集音视频资源。
- 创建 video 标签播放视频流,用于将视频绘制到 canvas 画布中。
- 创建 image 实例,加载水印图片。
- 创建 canvas 标签,将视频和水印绘制到 canvas 画布中。
- 使用 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);
}
循环绘制。setInterval
和 requestAnimationFrame
都可以进行循环绘制。
setInterval(loop, 1000/30);
提示:原始视频渲染和解码不占 JS 线程,但循环绘制 canvas 流程跟其他 JS 执行代码处于同一线程,会因渲染进程阻塞而造成卡顿。
- setInterval:存在执行间隔不精确的问题。
delay
参数只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间,如果队列前面已经加入了其他任务,动画就必须等前面的任务执行完才可以执行。 - requestAnimationFrame: 限制必须停留在绘制页 面。可保证执行步伐与系统的绘制频率一致,即保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。但是
requestAnimationFrame
运行在后台标签页时,会被暂停调用以提升性能和电池寿命,使用requestAnimationFrame
时,需保持停留在绘制页面。
步骤五:发布带水印的视频流
从 canvas 元素中抓取带水印的视频流。参见 captureStream。
const canvasStream = canvasEl.captureStream();
将带水印的视频 track 和之前获取的音频 track 转为融云 publish 接口接受的入参类型。这一步需要调用融云 SDK 的 createLocalVideoTrack 与 createLocalAudioTrack 方法。
/**
* 转化带水印的视频 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]);