音视频开发时候,如果对方未发布视频,或者后面才发布视频,或停止发送流时,占位图的优势就有了;
总结一句话就是:比黑屏好;
效果图

调用API
typescript
/* 先new一个
@params: parentEle: HTMLElement; 说明:父元素;
@params: useName: String; 说明:用户姓名;
*/
const avatarSpace = new AvatarCanvasSpace(parentEle, useName);
// 单独更新名字
avatarSpace.updateUserName(useName);
// 占位方式1:绘制Canvas占位
avatarSpace.createCavas();
// 占位方式2:绘制Video占位
avatarSpace.createVideo();
// 清理掉之前父元素插入的占位
avatarSpace.clearRepeatCreate();
// 销毁
avatarSpace.destroy();
核心方法
typescript
// 头像占位
class AvatarCameraSpace {
video: HTMLVideoElement | any;
myCanvas: HTMLCanvasElement | any;
ctx: CanvasRenderingContext2D | any ;
streamCanvas: MediaStream | any;
parentDiv: HTMLElement | any;
useName: string | any;
constructor(parentDiv: HTMLElement, useName: string) {
this.initCanvasAndVideo();
this.parentDiv = parentDiv;
this.useName = useName;
}
initCanvasAndVideo() {
this.video = document.createElement('video');
this.myCanvas = document.createElement('canvas');
this.ctx = this.myCanvas.getContext('2d');
// 考虑占位1秒捕获1帧也足够用了
this.streamCanvas = this.myCanvas.captureStream(1);
this.addVideoAttributes();
}
// 添加属性的方法
addVideoAttributes() {
// 需要添加的属性列表(键值对形式)
const attributes = {
// 自动播放
autoplay: '',
// 控制视频在inline(内嵌)模式下播放,而非默认的全屏播放(尤其针对 iOS 设备)
playsinline: '',
'webkit-playsinline': 'true',
// 这是腾讯 X5 内核(微信、QQ、部分手机浏览器采用的内核)的私有属性,强制视频使用 H5 播放器,而非 X5 内核默认的全屏播放器。
'x5-video-player-type': 'h5',
// 腾讯 X5 内核的私有属性,进一步强化内嵌播放行为,确保视频在 X5 内核中不会自动全屏,与标准 playsinline 功能一致,但仅针对 X5 环境生效
'x5-playsinline': 'true',
};
// 循环添加属性
Object.entries(attributes).forEach(([key, value]) => {
this.video.setAttribute(key, value);
});
// 补充:现代浏览器自动播放通常需要静音(可选,根据需求添加)
this.video.muted = true; // 直接赋值(muted 是布尔属性,true 表示启用)
}
updateUserName(useName: string) {
this.useName = useName;
}
// 创建canvas占位
createCavas() {
// 防止重复创建
this.clearRepeatCreate();
const canvas = this.myCanvas;
const ctx = this.ctx;
if (!canvas || !ctx) {return;};
this.parentDiv.innerHTML = '';
this.parentDiv.appendChild(canvas);
this.drawCanvas();
}
// 绘制用户头像占位
drawCanvas() {
const canvas = this.myCanvas;
const ctx = this.ctx;
if (!canvas || !ctx) {return;};
const useName = this.useName;
canvas.width = 1280;
canvas.height = 720;
// 设置样式让canvas自适应父容器
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
// 绘制黑色背景
this.ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 计算蓝色方框的位置(居中)
const boxSize = 120;
const boxX = (canvas.width - boxSize) / 2;
const boxY = (canvas.height - boxSize) / 2 - 50; // 稍微向上移动一点,给下方名字留出空间
// 圆角半径(可根据需要调整,建议10-20之间)
const borderRadius = 15;
// 绘制蓝色方框
ctx.fillStyle = '#1689F4';
// 开始绘制路径
ctx.beginPath();
// 左上角圆角
ctx.moveTo(boxX + borderRadius, boxY);
// 上边缘
ctx.lineTo(boxX + boxSize - borderRadius, boxY);
// 右上角圆角
ctx.arcTo(boxX + boxSize, boxY, boxX + boxSize, boxY + borderRadius, borderRadius);
// 右边缘
ctx.lineTo(boxX + boxSize, boxY + boxSize - borderRadius);
// 右下角圆角
ctx.arcTo(boxX + boxSize, boxY + boxSize, boxX + boxSize - borderRadius, boxY + boxSize, borderRadius);
// 下边缘
ctx.lineTo(boxX + borderRadius, boxY + boxSize);
// 左下角圆角
ctx.arcTo(boxX, boxY + boxSize, boxX, boxY + boxSize - borderRadius, borderRadius);
// 左边缘
ctx.lineTo(boxX, boxY + borderRadius);
// 左上角收尾圆角
ctx.arcTo(boxX, boxY, boxX + borderRadius, boxY, borderRadius);
// 闭合路径
ctx.closePath();
// 填充路径(绘制出圆角矩形)
ctx.fill();
// 获取名字的第一个字
const firstChar = useName.charAt(0);
// 在蓝色方框中绘制第一个字(居中)
ctx.fillStyle = '#FFFFFF'; // 白色文字
ctx.font = '80px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(firstChar, boxX + boxSize / 2, boxY + boxSize / 2);
// 在蓝色方框下方绘制全名
ctx.font = '40px Arial';
ctx.fillText(`${useName.length > 12 ? (useName.substring(0, 12) + '...') : useName}`, canvas.width / 2, boxY + boxSize + 60);
console.log('drawCanvas', useName);
}
// 创建video占位
createVideo() {
// 防止重复创建
this.clearRepeatCreate();
const canvas = this.myCanvas;
const ctx = this.ctx;
const video = this.video;
if (!canvas || !ctx) {return;};
this.parentDiv.innerHTML = '';
this.parentDiv.appendChild(video);
// 设置样式让video自适应父容器
video.style.width = '100%';
video.style.height = '100%';
video.style.display = 'block';
/*
直接给流就行; canvas重绘后captureStream会自己捕捉更新
video 元素会显示 canvas 的当前状态; 如果 canvas 的内容不再更新,video 元素就会停留在最后一帧;
如果是WebRTC则会持续读取流,实时显示视频; 我们的需求是占位所以足够了
*/
video.srcObject = this.streamCanvas;
video.play();
/* 绘制用户头像占位
因为video会显示canvas的当前状态,所以我们需要在canvas上绘制用户头像占位; 否则video会显示一个黑色的方框;
*/
this.drawCanvas();
}
// 父元素是否还存在
parentDivIsExists() {
// this.parentDiv.isConnected(); 这个方法在ie11下不支持
return document.body.contains(this.parentDiv);
}
myCanvasIsExists() {
if (!this.myCanvas) { return false; }
return document.body.contains(this.myCanvas);
}
myVideoIsExists() {
if (!this.video) { return false; }
return document.body.contains(this.video);
}
clearRepeatCreate() {
try {
// 如果当前canvas还存在,则移除
if (this.parentDiv.contains(this.myCanvas)) {
this.parentDiv.removeChild(this.myCanvas);
}
// 如果当前video还存在,则移除
if (this.parentDiv.contains(this.video)) {
this.parentDiv.removeChild(this.video);
}
} catch(err) {
console.log('销毁重复创建失败', err);
}
}
// 销毁
destroy() {
if (!this.video) { return; }
this.clearRepeatCreate();
this.video = null;
this.myCanvas = null;
this.ctx = null;
this.streamCanvas = null;
this.useName = null;
}
}
export default AvatarCameraSpace;