音视频开发远端未发布视频占位图

音视频开发时候,如果对方未发布视频,或者后面才发布视频,或停止发送流时,占位图的优势就有了;

总结一句话就是:比黑屏好;

效果图
调用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;
相关推荐
YWamy4 小时前
如何降低RTC SDK在复杂网络环境下的运行成本
网络·音视频·实时音视频
FlightYe6 小时前
视界原理之3D视频(四):裸眼、眼镜和头显
android·linux·c++·3d·ffmpeg·音视频
小豆庄7 小时前
本地视频打码:多模型检测、帧间追踪与18种模糊的选型
opencv·目标检测·ai·自然语言处理·音视频·视频
TST-魚爷12 小时前
小型视频会议室音频踩坑:一体化拾音扩音方案实测分享(易视讯)
stm32·电脑·音视频·钉钉·腾讯会议·zoom会议
Sayai13 小时前
让动漫角色复刻真人动作!MiniMax H3 动作控制实战:一段视频驱动角色动画(12G 显存,附模型下载)
人工智能·aigc·音视频·comfyui·动作捕捉·minimax
美狐美颜SDK开放平台17 小时前
开发直播APP、电商短视频与视频社交平台时,能否共用一套美颜SDK?
android·图像处理·人工智能·音视频·直播美颜sdk
AI日报派送佬17 小时前
2026年11月02日 AI 行业日报|商汤开源 Loop-9B、Pika 发布 H3 Flex 音视频模型、Meta× 傅利叶推出人形机器人 Max
人工智能·微软·开源·音视频·openai·1024程序员节·ai日报
揽秀亭长18 小时前
音频、五线谱、MIDI 如何转成简谱?
人工智能·音视频
Likeadust18 小时前
说开就开:临时发起的私有化音视频实时通信EasyDSS视频会议,让跨城协作不再“等排期”
网络·音视频·easydss
AI创界者18 小时前
【AI生视频本地部署】最新 MiniMax-H3 全模态生成模型本地部署与极致优化:N卡 6G 显存流畅跑、无审查加速完整工作流实战
人工智能·aigc·音视频