VideoBackground.vue 3.61 KB
<template>
  <div class="video-background">
    <!-- Loading 状态 -->
    <div v-if="isLoading" class="video-loading">
      <van-loading size="24px" color="#fff">加载中...</van-loading>
    </div>

    <!-- 视频元素 -->
    <video
      ref="videoRef"
      class="video-element"
      :src="videoSrc"
      :poster="posterUrl"
      :autoplay="autoplay"
      :loop="loop"
      :muted="muted"
      :webkit-playsinline="true"
      :playsinline="true"
      x5-video-player-type="h5"
      x5-video-player-fullscreen="true"
      @canplay="onCanPlay"
      @error="onError"
    ></video>

    <!-- 降级背景图 -->
    <div
      v-if="showFallback"
      class="video-fallback"
      :style="{ backgroundImage: `url(${posterUrl})` }"
    ></div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const props = defineProps({
  /** 视频源 URL */
  videoSrc: {
    type: String,
    required: true
  },
  /** 封面图 URL (可选,不传则自动从视频生成) */
  poster: {
    type: String,
    default: ''
  },
  /** 是否自动播放 */
  autoplay: {
    type: Boolean,
    default: true
  },
  /** 是否循环播放 */
  loop: {
    type: Boolean,
    default: true
  },
  /** 是否静音 */
  muted: {
    type: Boolean,
    default: true
  },
  /** 视频填充模式 */
  objectFit: {
    type: String,
    default: 'cover' // cover, contain, fill
  }
})

const videoRef = ref(null)
const isLoading = ref(true)
const showFallback = ref(false)

/**
 * 自动生成封面图 URL
 * 如果没有传入 poster,使用七牛云视频处理参数从视频中提取第一帧
 * 处理参数: ?vframe/jpg/offset/0.001 表示从视频第 0.001 秒截取一帧作为 JPG
 */
const posterUrl = computed(() => {
  if (props.poster) {
    return props.poster
  }
  // 从视频 URL 自动生成封面图
  // 七牛云视频处理: https://developer.qiniu.com/dora/1316/video-frame-operation
  return `${props.videoSrc}?vframe/jpg/offset/0.001`
})

// 视频可以播放时
const onCanPlay = () => {
  isLoading.value = false

  // 尝试自动播放
  if (props.autoplay && videoRef.value) {
    videoRef.value.play().catch(err => {
      console.warn('[VideoBackground] 自动播放失败:', err)
      // iOS Safari 可能需要用户交互才能播放
      showFallback.value = true
    })
  }
}

// 视频加载错误
const onError = (e) => {
  console.error('[VideoBackground] 视频加载失败:', e)
  isLoading.value = false
  showFallback.value = true
}

// 手动播放(用于处理需要用户交互的情况)
const play = () => {
  if (videoRef.value) {
    videoRef.value.play().catch(err => {
      console.warn('[VideoBackground] 播放失败:', err)
    })
  }
}

// 暂停播放
const pause = () => {
  if (videoRef.value) {
    videoRef.value.pause()
  }
}

onMounted(() => {
  // 预加载视频
  if (videoRef.value) {
    videoRef.value.load()
  }
})

onUnmounted(() => {
  // 清理资源
  if (videoRef.value) {
    videoRef.value.pause()
    videoRef.value.src = ''
  }
})

defineExpose({
  play,
  pause
})
</script>

<style scoped>
.video-background {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: -1;
  overflow: hidden;
}

.video-element {
  width: 100%;
  height: 100%;
  object-fit: v-bind(objectFit);
  background-color: #000;
}

.video-loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
}

.video-fallback {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  z-index: -1;
}
</style>