音视频开发远端未发布视频占位图2
音视频开发时候,如果对方未发布视频,或者后面才发布视频,或停止发送流时,占位图的优势就有了;
考虑到大部分音视频SDK都会有play方法,但不一定有成员状态,如果我们自己创建canvas和视频有概率会有布局问题;最好还是给他弄成背景图片放后面,不管是对方流是空的(网络不好无法传流,或者有个别设备摄像头坏了无法编解码,或者本身就没摄像头给的空轨道);这个时候有个保底占位图就很舒服了;
总结一句话就是:比黑屏好;
效果图

调用API
/* 先new一个@params: curDrawDiv: HTMLElement; 说明:需要绘制的dev;@params: useName: String; 说明:用户姓名;
*/
const avatarSpace = new AvatarCanvasSpaceBj(curDrawDiv, useName);
// 开始绘制
avatarSpace.createCanvasBackground();// 单独更新名字_并绘制
avatarSpace.updateUserName(useName);// 销毁
avatarSpace.destroy();
核心方法
// 头像占位
class AvatarCameraSpace {myCanvas: HTMLCanvasElement | null;ctx: CanvasRenderingContext2D | null;curDrawDiv: HTMLElement;useName: string;constructor(curDrawDiv: HTMLElement, useName: string) {this.curDrawDiv = curDrawDiv;this.useName = useName;this.myCanvas = document.createElement('canvas');this.ctx = this.myCanvas.getContext('2d');}// 更新用户名(相同则不处理)updateUserName(useName: string) {// 检查用户名是否相同,相同则不处理if (useName === this.useName) return;this.useName = useName;this.drawCanvas();}// 创建canvas并设置为parentDiv的背景createCanvasBackground() {this.drawCanvas();}// 绘制用户头像占位并设置为背景drawCanvas() {// 先把原背景图给他置空掉this.clearResources();try {if (!this.myCanvas || !this.ctx || !this.parentDivIsExists()) return;const canvas = this.myCanvas;const ctx = this.ctx;const useName = this.useName;const canvasWidth = 1280;const canvasHeight = 720;canvas.width = 1280;canvas.height = 720;// 保存上下文初始状态(重要:确保每次绘制都是干净的状态)ctx.save();// 重置变换矩阵ctx.setTransform(1, 0, 0, 1, 0, 0);// 清空画布ctx.clearRect(0, 0, canvasWidth, canvasHeight);// 绘制黑色背景ctx.fillStyle = '#000000';ctx.fillRect(0, 0, canvasWidth, canvasHeight);// 计算蓝色方框的位置(居中)const boxSize = Math.min(120, canvasWidth * 0.2, canvasHeight * 0.2); // 自适应大小const boxX = (canvasWidth - boxSize) / 2;const boxY = (canvasHeight - boxSize) / 2 - 50; // 稍微向上移动一点,给下方名字留出空间// 圆角半径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 = `${Math.min(80, boxSize * 0.6)}px Arial`; // 自适应字体大小ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText(firstChar, boxX + boxSize / 2, boxY + boxSize / 2);// 在蓝色方框下方绘制全名const nameFontSize = Math.min(40, boxSize * 0.3);ctx.font = `${nameFontSize}px Arial`; // 自适应字体大小const displayName = useName.length > 12 ? `${useName.substring(0, 12)}...` : useName;ctx.fillText(displayName, canvasWidth / 2, boxY + boxSize + 60);// 恢复上下文初始状态(确保后续绘制不受当前状态影响)ctx.restore();// 绘制完成后设置为背景this.setCanvasAsBackground();} catch(error) {console.warn('绘制失败了', error);}}// 将Canvas内容设置为parentDiv的背景图片setCanvasAsBackground() {try {if (!this.myCanvas || !this.parentDivIsExists()) return;// 将Canvas转换为图片URLconst dataUrl = this.myCanvas.toDataURL('image/png');// 设置为parentDiv的背景图片this.curDrawDiv.style.backgroundImage = `url(${dataUrl})`;this.curDrawDiv.style.backgroundSize = 'cover';this.curDrawDiv.style.backgroundPosition = 'center';this.curDrawDiv.style.backgroundRepeat = 'no-repeat';} catch(error) {console.warn('设置背景失败了', error);}}// 父元素是否还存在于DOM中parentDivIsExists() {return document.body.contains(this.curDrawDiv);}// 清除背景和资源clearResources() {try {// 清除背景图this.curDrawDiv.style.backgroundImage = 'none';} catch(error) {}}// 销毁实例destroy() {this.clearResources();// 释放引用this.myCanvas = null;this.ctx = null;}
}export default AvatarCameraSpace;
