目录
前言
本人是在vue3中使用flv.js处理推流时,遇到的一些问题,以及处理办法,归纳总结为一个组件,仅限于推流使用。
目前只贴出部分关键代码,若需要完整的代码,请往github下载
1、构建
/** * @description: 构建播放器 * @return {*} * @Author: liuxin */ function flvCreated() { try { const videoElement = flvPlayerVideo.value; if (flvjs.isSupported() && videoElement) { addLog(`flv created,address:${prop.url}`); const flvOption = { url: prop.url, // 播放地址 hasAudio: false, // 是否有音频 hasVideo: true, //是否有视频 isLive: true, // 是否是直播流,默认 true type: "flv", // 是否是直播流,默认 true stashInitialSize: 128, // 减少首帧显示等待时长 ...prop.option, }; state.flvPlayer = flvjs.createPlayer(flvOption, { enableWorker: false, // 不启用分离的线程进行转换,之前为true enableStashBuffer: false, // 关闭IO隐藏缓冲区 stashInitialSize: 128, // 减少首帧显示等待时长 autoCleanupSourceBuffer: true, // 打开自动清除缓存 fixAudioTimestampGap: false, //false才会音视频同步,新增 lazyLoad: false, // 去掉懒加载,新增 }); state.flvPlayer.attachMediaElement(videoElement); state.flvPlayer.load(); state.flvPlayer.play(); state.endedReloadFlag = true; // 重置画面停滞的播放状态,下次停滞了会再次打开 videoElementEvent(); // 手动跳帧,防止延时 flvPlayerEvent(); // 断流、卡顿处理 } } catch (error) { console.error("构建错误", error); } }
2、销毁
/** * @description: 销毁播放器 * @return {*} * @Author: liuxin */ function flvDestory() { if (state.delayTimer) { clearTimeout(state.delayTimer); // 清除推迟打开播放器定时器 } if (state.flvPlayer == null) return; // 空对象,不执行销毁 /* ----- 销毁开始 ----- */ addLog(`flv destory,address:${prop.url}`); try { state.flvPlayer.off(flvjs.Events.ERROR, errorHandle); if (state.flvPlayer._hasPendingLoad) { state.flvPlayer.pause(); state.flvPlayer.unload(); } state.flvPlayer.detachMediaElement(); state.flvPlayer.destroy(); state.flvPlayer = null; } catch (error) { console.error("销毁错误"); } }
3、断流、卡顿重连
state.flvPlayer.on(flvjs.Events.STATISTICS_INFO, statisticsInfoHanle); // 断流重连 /** * @description: 视频卡顿,销毁后重建 * @param {*} errorType * @param {*} errorDetail * @param {*} errorInfo * @return {*} * @Author: liuxin */ function statisticsInfoHanle(res) { // 初始化播放 if (state.lastDecodedFrame == 0) { state.lastDecodedFrame = res.decodedFrames; return; } // 正常播放 if (state.lastDecodedFrame != res.decodedFrames) { state.lastDecodedFrame = res.decodedFrames; state.loading = false; // 去掉loading动画 state.errorCount = 0; // 错误连接次数归0 } // 播放异常 else { if (state.player) { addLog(`Reconnect after video freezes, address:${prop.url}`); // 添加日志 state.errorCount = 0; // 错误连接次数归0 state.lastDecodedFrame = 0; // 最后播放编码号 flvDestory(); // 销毁对象 flvCreated("statistics_info"); // 创建对象 } } }
4、报错、停滞重连
state.flvPlayer.on(flvjs.Events.ERROR, errorHandle); // 监听出错消息后,销毁后重连 state.flvPlayer.on(flvjs.Events.LOADING_COMPLETE, errorHandle); // ctrl+f5刷新,会莫名因为停止end不播放 /** * @description: 错误回调事件 * @param {*} errorType * @param {*} errorDetail * @param {*} errorInfo * @return {*} * @Author: liuxin */ function errorHandle() { //视频出错后销毁重新创建 网络错误 if (state.flvPlayer && state.errorCount <= state.maxReconnectCount) { addLog(`Video error ${state.errorCount} reconnection, address:${prop.url}`); // 视频报错N重连 state.loading = true; // 添加loading动画 state.errorCount++; //错误重连次数+1 flvDestory(); flvCreated("ERROR"); } if (state.errorCount > state.maxReconnectCount) { state.loading = false; // 去掉loading } }
5、累计延时处理
/** * @description: 浏览器下载流事件,手动跳帧,防止累计延时 * @return {*} * @Author: liuxin */ videoElement.onprogress = (e) => { // 不需要跳帧,如:异常视频 或者没有数据流,则不进行跳帧 if (!prop.isBufferedEnd || state.flvPlayer.buffered.length <= 0) { return; } state.loading = false; /* ----- 跳帧操作 ----- */ let end = state.flvPlayer.buffered.end(0); //获取当前时间值 let diff = end - state.flvPlayer.currentTime; //获取相差差值 // 延迟过大或帧率不正常,通过跳帧的方式更新视频 if (diff > 20 || diff < 0) { // addLog(`Manual frame skipping,address:${prop.url}`); // 添加日志 state.flvPlayer.currentTime = state.flvPlayer.buffered.end(0) - 0.5; // 手动跳帧到最后 return; } // 正常帧率,正常播放 if (diff <= 1) { videoElement.playbackRate = 1; } // 10秒内的延时,1.1倍速播放 else if (diff <= 10) { // addLog(`Chase frames manually 1.1,address:${prop.url}`); // 手动追帧 videoElement.playbackRate = 1.1; } // 20秒内的延时,1.2倍速播放 else if (diff <= 20) { // addLog(`Chase frames manually 1.2,address:${prop.url}`); // 手动追帧 videoElement.playbackRate = 1.2; } };
6、手动全屏
/** * @description: 全屏 / 退出全屏 * @return {*} * @Author: liuxin */ function fullscreenHandle() { state.isFlullscreen = !state.isFlullscreen; document.addEventListener("keydown", function (e) { //此处填写你的业务逻辑即可 if (e.key == "Escape") { e.stopPropagation(); state.isFlullscreen = false; } });