Newer
Older
safe_production_front / src / views / bigScreen / jessibuca-OptimizeInitialFrame.vue
<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>