功能描述
本文主要介绍如何基于 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') } }
-