<script setup> import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue' const props = defineProps({ id: { type: String, }, showOperateBtns: { type: Boolean, default: false, }, preloadUrl: { type: String, default: '', }, }) const jessibuca = ref(null) // 播放器实例引用 const container = ref(null) // 播放器容器引用 const isInitialized = ref(false) // 播放器初始化状态 const isFirstPlay = ref(true) // 是否首次播放标记 /** * 创建Jessibuca播放器实例并初始化配置 */ function create() { console.log('执行jessibuca初始化', props.id) // 等待DOM渲染完成后再初始化播放器,确保容器已存在 nextTick(() => { container.value = document.getElementById(props.id) jessibuca.value = new Jessibuca({ container: container.value, videoBuffer: 0.1, // 减少初始缓冲时间,加快首帧显示 isResize: false, loadingText: '视频加载中', useMSE: true, // 优先使用MSE硬解码(适用于大多数现代浏览器) useWCS: true, // 同时启用Webcodecs硬解码(Chrome 94+支持) debug: false, showBandwidth: props.showOperateBtns, loadingTimeout: 15, // 缩短超时时间,避免长时间等待无响应的请求 operateBtns: { fullscreen: props.showOperateBtns, screenshot: props.showOperateBtns, play: props.showOperateBtns, audio: false, recorder: false, }, forceNoOffscreen: true, // 不使用离屏模式,提升渲染性能 isNotMute: false, hotKey: false, keepScreenOn: false, supportDblclickFullscreen: true, hasAudio: false, // 不需要音频,禁用音频解码提升性能 autoWasm: true, // H265解码失败时自动降级到WASM模式 controlAutoHide: false, wasmDecodeErrorReplay: true, wcsUseVideoRender: true, // 使用video标签渲染,提升渲染性能 // 新增优化首屏加载的参数 firstFrameTimeout: 5, // 首帧超时时间,超时后可尝试重新加载 preloadTime: 1, // 预加载时间,提前缓冲一小段视频 maxBufferLength: 1, // 限制最大缓冲长度,避免占用过多内存 useWorker: true, // 使用Web Worker处理解码,避免阻塞主线程 }) // 播放器事件监听 jessibuca.value.on('videoInfo', (data) => { console.log('videoInfo:', 'width:', data.width, 'height:', data.height) }) jessibuca.value.on('playFailedAndPaused', (error) => { console.log('playFailedAndPaused事件:监听流断掉', error) // 首次播放失败时,自动尝试重连 if (isFirstPlay.value) { setTimeout(() => { jessibuca.value && jessibuca.value.replay() }, 1000) } }) jessibuca.value.on('timeout', (error) => { console.log('timeout当设定的超时时间内无数据返回,则回调:', error) // 首次播放超时,尝试重连 if (isFirstPlay.value) { setTimeout(() => { jessibuca.value && jessibuca.value.replay() }, 1000) } }) jessibuca.value.on('loadingTimeout', () => { console.log('loadingTimeout当play()的时候,如果没有数据返回,则回调: timeout') // 首次加载超时,尝试重连 if (isFirstPlay.value) { setTimeout(() => { jessibuca.value && jessibuca.value.replay() }, 1000) } }) jessibuca.value.on('firstFrame', () => { console.log('首帧渲染完成') isFirstPlay.value = false // 标记首帧已渲染,后续不再触发重连逻辑 // 首帧渲染后可以隐藏加载指示器 container.value.classList.remove('loading') }) jessibuca.value.on('playToRenderTimes', (times) => { console.log('监听调用play方法 经过 初始化-> 网络请求-> 解封装 -> 解码 -> 渲染 一系列过程的时间消耗:', times) }) isInitialized.value = true // 如果有预加载URL,尝试预加载视频资源 if (props.preloadUrl) { jessibuca.value.preload(props.preloadUrl) } }) } /** * 预连接视频服务器,减少首次请求延迟 * @param {string} url - 视频URL */ async function preconnect(url) { if (!url) { return } try { // 解析URL获取主机名和协议 const parsedUrl = new URL(url) const protocol = parsedUrl.protocol === 'https:' ? 'https' : 'http' const host = parsedUrl.host // 创建预连接标记并添加到文档头部 // 提前进行DNS解析、TCP握手和TLS协商 const link = document.createElement('link') link.rel = 'preconnect' link.href = `${protocol}://${host}` document.head.appendChild(link) console.log(`已预连接到: ${host}`) } catch (error) { console.error('预连接失败:', error) } } /** * 播放视频 * @param {string} playUrl - 视频播放地址 */ async function play(playUrl = '') { if (!playUrl) { return } console.log('播放视频路径: ', playUrl) // 显示加载指示器 if (container.value) { container.value.classList.add('loading') } // 首次播放前先预连接服务器,优化网络连接时间 if (isFirstPlay.value) { await preconnect(playUrl) } // 如果播放器还未初始化完成,等待一段时间 if (!isInitialized.value) { await new Promise(resolve => setTimeout(resolve, 500)) } // 播放视频 if (jessibuca.value) { jessibuca.value.play(playUrl) } else { console.error('播放器未初始化') // 隐藏加载指示器 if (container.value) { container.value.classList.remove('loading') } } } // 暂停播放 function pause() { jessibuca.value && jessibuca.value.pause().then(() => { console.log('pause success') }) } // 恢复播放 function restore() { jessibuca.value && jessibuca.value.play().then(() => { console.log('restore success') }) } // 销毁播放器实例 function destroy() { jessibuca.value && jessibuca.value.destroy() jessibuca.value = null isInitialized.value = false } // 组件卸载前清理资源 onBeforeUnmount(() => { destroy() }) // 组件挂载后初始化播放器 onMounted(() => { create() }) // 暴露公共方法供父组件调用 defineExpose({ play, destroy, pause, restore }) </script> <template> <div :id="props.id" class="container" /> </template> <style scoped> .container { background: rgba(13, 14, 27, 0.7); height: 100%; width: 100%; position: relative; } /* 加载指示器样式 */ .container::before { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 40px; height: 40px; border: 3px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: #fff; animation: spin 1s linear infinite; z-index: 10; display: none; } /* 显示加载指示器的状态 */ .container.loading::before { display: block; } /* 加载动画 */ @keyframes spin { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } } </style>