音视频开发时候,如果对方未发布视频,或者后面才发布视频,或停止发送流时,占位图的优势就有了;
考虑到大部分音视频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转换为图片URL
            const 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;
Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐