Vue 高级视频播放器实现指南

Vue 高级视频播放器实现指南

下面我将实现一个功能完整的 Vue 视频播放器组件,包含所有你要求的功能,并深入底层实现原理。

一、组件实现

1. 组件代码 (VideoPlayer.vue)

vue 复制代码
<template>
  <div class="video-player-container" ref="container">
    <video
      ref="videoElement"
      class="video-element"
      @play="onPlay"
      @pause="onPause"
      @timeupdate="onTimeUpdate"
      @volumechange="onVolumeChange"
      @loadedmetadata="onLoadedMetadata"
      @progress="onProgress"
      @ended="onEnded"
      @error="onError"
    >
      <template v-for="(source, index) in currentSources" :key="index">
        <source :src="source.src" :type="source.type" :data-quality="source.quality">
      </template>
      <track v-for="(track, index) in tracks" :key="index" :src="track.src" :kind="track.kind" :srclang="track.srclang" :label="track.label">
      您的浏览器不支持HTML5视频
    </video>

    <!-- 加载指示器 -->
    <div v-if="loading" class="loading-indicator">
      <div class="spinner"></div>
    </div>

    <!-- 错误提示 -->
    <div v-if="error" class="error-message">
      {{ errorMessage }}
    </div>

    <!-- 控制栏 -->
    <div class="controls-container" @mouseenter="showControls = true" @mouseleave="showControls = false">
      <div class="progress-container" ref="progressContainer" @click="handleProgressClick">
        <input
          type="range"
          class="progress-bar"
          min="0"
          max="100"
          step="0.1"
          v-model="progress"
          @mousedown="isDragging = true"
          @mouseup="handleProgressDragEnd"
          @input="handleProgressDrag"
        >
        <div class="buffer-bar" :style="{ width: bufferProgress + '%' }"></div>
      </div>

      <div class="controls" :class="{ 'controls-hidden': !showControls && !isHoveringControls }">
        <button class="control-btn" @click="togglePlay" :title="isPlaying ? '暂停' : '播放'">
          <svg-icon :icon="isPlaying ? 'pause' : 'play'" />
        </button>

        <button v-if="hasPlaylist" class="control-btn" @click="playNext" title="下一个">
          <svg-icon icon="next" />
        </button>

        <div class="time-display">
          {{ formatTime(currentTime) }} / {{ formatTime(duration) }}
        </div>

        <button class="control-btn" @click="toggleMute" :title="isMuted ? '取消静音' : '静音'">
          <svg-icon :icon="isMuted ? 'volume-mute' : volume > 0.5 ? 'volume-high' : 'volume-low'" />
        </button>

        <div class="volume-control">
          <input
            type="range"
            class="volume-slider"
            min="0"
            max="1"
            step="0.01"
            v-model="volume"
            @input="handleVolumeChange"
          >
        </div>

        <div class="quality-selector" v-if="qualities.length > 1">
          <select v-model="currentQuality" @change="changeQuality">
            <option v-for="q in qualities" :key="q" :value="q">
              {{ q === 'auto' ? '自动' : q }}
            </option>
          </select>
        </div>

        <div class="speed-selector">
          <select v-model="playbackRate" @change="changePlaybackRate">
            <option v-for="rate in playbackRates" :key="rate" :value="rate">
              {{ rate }}x
            </option>
          </select>
        </div>

        <button class="control-btn" @click="togglePictureInPicture" title="画中画">
          <svg-icon icon="pip" />
        </button>

        <button class="control-btn" @click="toggleMirror" title="镜像">
          <svg-icon :icon="isMirrored ? 'mirror-on' : 'mirror-off'" />
        </button>

        <button class="control-btn" @click="toggleTheaterMode" title="宽屏">
          <svg-icon :icon="isTheaterMode ? 'theater-off' : 'theater-on'" />
        </button>

        <button class="control-btn" @click="toggleWebFullscreen" title="网页全屏">
          <svg-icon :icon="isWebFullscreen ? 'web-fullscreen-off' : 'web-fullscreen-on'" />
        </button>

        <button class="control-btn" @click="toggleFullscreen" title="全屏">
          <svg-icon :icon="isFullscreen ? 'fullscreen-off' : 'fullscreen-on'" />
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import SvgIcon from './SvgIcon.vue' // 假设有一个SVG图标组件

export default {
  name: 'VideoPlayer',
  components: { SvgIcon },
  props: {
    sources: {
      type: [Array, String],
      required: true,
      validator: value => {
        if (typeof value === 'string') return true
        return Array.isArray(value) && value.every(item => item.src && item.type)
      }
    },
    tracks: {
      type: Array,
      default: () => []
    },
    poster: {
      type: String,
      default: ''
    },
    autoplay: {
      type: Boolean,
      default: false
    },
    initialQuality: {
      type: String,
      default: 'auto'
    }
  },
  data() {
    return {
      isPlaying: false,
      currentTime: 0,
      duration: 0,
      volume: 1,
      isMuted: false,
      progress: 0,
      bufferProgress: 0,
      playbackRate: 1,
      playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2],
      currentQuality: 'auto',
      qualities: ['auto'],
      currentSources: [],
      loading: false,
      error: false,
      errorMessage: '',
      isDragging: false,
      showControls: true,
      isHoveringControls: false,
      isTheaterMode: false,
      isWebFullscreen: false,
      isFullscreen: false,
      isMirrored: false,
      supportsPictureInPicture: false,
      isInPictureInPicture: false,
      playlistIndex: 0,
      videoQualityMap: new Map() // 用于存储不同清晰度的视频源
    }
  },
  computed: {
    hasPlaylist() {
      return Array.isArray(this.sources) && this.sources.length > 1
    },
    videoElement() {
      return this.$refs.videoElement
    }
  },
  watch: {
    sources: {
      immediate: true,
      handler(newVal) {
        this.initSources(newVal)
      }
    },
    volume(newVal) {
      this.videoElement.volume = newVal
      this.isMuted = newVal === 0
    },
    isMuted(newVal) {
      this.videoElement.muted = newVal
    }
  },
  mounted() {
    this.initPlayer()
    this.detectFeatures()
    document.addEventListener('fullscreenchange', this.handleFullscreenChange)
    document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange)
    document.addEventListener('msfullscreenchange', this.handleFullscreenChange)
    this.videoElement.addEventListener('enterpictureinpicture', () => {
      this.isInPictureInPicture = true
    })
    this.videoElement.addEventListener('leavepictureinpicture', () => {
      this.isInPictureInPicture = false
    })
  },
  beforeDestroy() {
    document.removeEventListener('fullscreenchange', this.handleFullscreenChange)
    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange)
    document.removeEventListener('msfullscreenchange', this.handleFullscreenChange)
    if (this.pipInterval) {
      clearInterval(this.pipInterval)
    }
  },
  methods: {
    // 初始化播放器
    initPlayer() {
      this.videoElement.volume = this.volume
      this.videoElement.muted = this.isMuted
      this.videoElement.playbackRate = this.playbackRate
      
      if (this.autoplay) {
        this.play()
      }
    },
    
    // 初始化视频源
    initSources(sources) {
      if (typeof sources === 'string') {
        this.currentSources = [{ src: sources, type: this.guessMimeType(sources) }]
        this.qualities = ['auto']
      } else {
        // 构建清晰度映射表
        this.videoQualityMap.clear()
        const qualities = new Set()
        
        sources.forEach(source => {
          const quality = source.quality || 'auto'
          if (!this.videoQualityMap.has(quality)) {
            this.videoQualityMap.set(quality, [])
          }
          this.videoQualityMap.get(quality).push(source)
          qualities.add(quality)
        })
        
        this.qualities = Array.from(qualities)
        this.currentQuality = this.initialQuality
        this.setCurrentQuality(this.currentQuality)
      }
    },
    
    // 设置当前清晰度
    setCurrentQuality(quality) {
      if (quality === 'auto') {
        // 自动选择最佳清晰度 - 这里简化处理,实际可以根据网络状况选择
        this.currentSources = this.sources
      } else {
        const sources = this.videoQualityMap.get(quality)
        if (sources) {
          this.currentSources = sources
        }
      }
      
      // 重新加载视频源
      this.loading = true
      this.videoElement.load()
      this.videoElement.oncanplay = () => {
        this.loading = false
        if (this.autoplay || this.isPlaying) {
          this.videoElement.play()
        }
      }
    },
    
    // 改变清晰度
    changeQuality() {
      this.setCurrentQuality(this.currentQuality)
      this.$emit('quality-change', this.currentQuality)
    },
    
    // 猜测MIME类型
    guessMimeType(url) {
      if (/\.mp4$/i.test(url)) return 'video/mp4'
      if (/\.webm$/i.test(url)) return 'video/webm'
      if (/\.ogg$/i.test(url)) return 'video/ogg'
      return 'video/mp4' // 默认
    },
    
    // 检测浏览器功能
    detectFeatures() {
      // 检测画中画支持
      this.supportsPictureInPicture = 
        document.pictureInPictureEnabled && 
        !/iPad|iPhone|iPod/.test(navigator.userAgent)
      
      // 检测全屏支持
      this.supportsFullscreen = 
        document.fullscreenEnabled ||
        document.webkitFullscreenEnabled ||
        document.msFullscreenEnabled
    },
    
    // 播放控制
    togglePlay() {
      if (this.isPlaying) {
        this.pause()
      } else {
        this.play()
      }
    },
    play() {
      const promise = this.videoElement.play()
      
      if (promise) {
        this.loading = true
        promise
          .then(() => {
            this.loading = false
            this.isPlaying = true
          })
          .catch(error => {
            this.loading = false
            this.error = true
            this.errorMessage = '自动播放被阻止,请点击播放按钮'
            console.error('播放失败:', error)
          })
      }
    },
    pause() {
      this.videoElement.pause()
    },
    onPlay() {
      this.isPlaying = true
      this.$emit('play')
    },
    onPause() {
      this.isPlaying = false
      this.$emit('pause')
    },
    
    // 下一个视频
    playNext() {
      if (this.hasPlaylist && this.playlistIndex < this.sources.length - 1) {
        this.playlistIndex++
        this.initSources(this.sources[this.playlistIndex])
        this.play()
        this.$emit('next', this.playlistIndex)
      }
    },
    
    // 时间控制
    onTimeUpdate() {
      if (!this.isDragging) {
        this.currentTime = this.videoElement.currentTime
        this.progress = (this.currentTime / this.duration) * 100
      }
    },
    handleProgressClick(e) {
      const rect = this.$refs.progressContainer.getBoundingClientRect()
      const pos = (e.clientX - rect.left) / rect.width
      this.videoElement.currentTime = pos * this.duration
    },
    handleProgressDrag() {
      const time = (this.progress / 100) * this.duration
      this.currentTime = time
    },
    handleProgressDragEnd() {
      this.isDragging = false
      this.videoElement.currentTime = (this.progress / 100) * this.duration
    },
    
    // 音量控制
    toggleMute() {
      this.isMuted = !this.isMuted
    },
    handleVolumeChange() {
      this.isMuted = this.volume === 0
    },
    onVolumeChange() {
      this.volume = this.videoElement.volume
      this.isMuted = this.videoElement.muted
    },
    
    // 播放速度
    changePlaybackRate() {
      this.videoElement.playbackRate = this.playbackRate
      this.$emit('rate-change', this.playbackRate)
    },
    
    // 画中画
    togglePictureInPicture() {
      if (!this.supportsPictureInPicture) return
      
      if (document.pictureInPictureElement) {
        document.exitPictureInPicture()
      } else {
        this.videoElement.requestPictureInPicture()
      }
    },
    
    // 镜像
    toggleMirror() {
      this.isMirrored = !this.isMirrored
      this.videoElement.style.transform = this.isMirrored ? 'scaleX(-1)' : 'none'
    },
    
    // 宽屏模式
    toggleTheaterMode() {
      this.isTheaterMode = !this.isTheaterMode
      this.$emit('theater-mode', this.isTheaterMode)
    },
    
    // 网页全屏
    toggleWebFullscreen() {
      if (this.isWebFullscreen) {
        this.exitWebFullscreen()
      } else {
        this.enterWebFullscreen()
      }
    },
    enterWebFullscreen() {
      const container = this.$refs.container
      container.classList.add('web-fullscreen')
      this.isWebFullscreen = true
      document.body.style.overflow = 'hidden'
      this.$emit('web-fullscreen', true)
    },
    exitWebFullscreen() {
      const container = this.$refs.container
      container.classList.remove('web-fullscreen')
      this.isWebFullscreen = false
      document.body.style.overflow = ''
      this.$emit('web-fullscreen', false)
    },
    
    // 全屏
    toggleFullscreen() {
      if (this.isFullscreen) {
        this.exitFullscreen()
      } else {
        this.enterFullscreen()
      }
    },
    enterFullscreen() {
      const container = this.$refs.container
      
      if (container.requestFullscreen) {
        container.requestFullscreen()
      } else if (container.webkitRequestFullscreen) {
        container.webkitRequestFullscreen()
      } else if (container.msRequestFullscreen) {
        container.msRequestFullscreen()
      }
    },
    exitFullscreen() {
      if (document.exitFullscreen) {
        document.exitFullscreen()
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen()
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen()
      }
    },
    handleFullscreenChange() {
      this.isFullscreen = 
        document.fullscreenElement ||
        document.webkitFullscreenElement ||
        document.msFullscreenElement
      this.$emit('fullscreen', this.isFullscreen)
    },
    
    // 元数据加载
    onLoadedMetadata() {
      this.duration = this.videoElement.duration
      this.$emit('loaded-metadata', {
        duration: this.duration,
        videoWidth: this.videoElement.videoWidth,
        videoHeight: this.videoElement.videoHeight
      })
    },
    
    // 缓冲进度
    onProgress() {
      if (this.videoElement.buffered.length > 0) {
        const bufferedEnd = this.videoElement.buffered.end(this.videoElement.buffered.length - 1)
        this.bufferProgress = (bufferedEnd / this.duration) * 100
      }
    },
    
    // 视频结束
    onEnded() {
      this.isPlaying = false
      this.$emit('ended')
    },
    
    // 错误处理
    onError() {
      this.error = true
      this.loading = false
      
      const error = this.videoElement.error
      switch (error.code) {
        case MediaError.MEDIA_ERR_ABORTED:
          this.errorMessage = '视频加载被用户取消'
          break
        case MediaError.MEDIA_ERR_NETWORK:
          this.errorMessage = '网络错误导致视频加载失败'
          break
        case MediaError.MEDIA_ERR_DECODE:
          this.errorMessage = '视频解码错误'
          break
        case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
          this.errorMessage = '浏览器不支持该视频格式'
          break
        default:
          this.errorMessage = '视频播放出错'
      }
      
      this.$emit('error', {
        code: error.code,
        message: this.errorMessage
      })
    },
    
    // 工具方法
    formatTime(seconds) {
      if (isNaN(seconds)) return '00:00'
      const minutes = Math.floor(seconds / 60)
      const secs = Math.floor(seconds % 60)
      return `${minutes}:${secs < 10 ? '0' : ''}${secs}`
    },
    
    // 键盘快捷键
    handleKeyDown(e) {
      switch (e.key.toLowerCase()) {
        case ' ':
          e.preventDefault()
          this.togglePlay()
          break
        case 'arrowleft':
          this.videoElement.currentTime = Math.max(0, this.currentTime - 5)
          break
        case 'arrowright':
          this.videoElement.currentTime = Math.min(this.duration, this.currentTime + 5)
          break
        case 'arrowup':
          this.volume = Math.min(1, this.volume + 0.05)
          break
        case 'arrowdown':
          this.volume = Math.max(0, this.volume - 0.05)
          break
        case 'f':
          this.toggleFullscreen()
          break
        case 'm':
          this.toggleMute()
          break
        case 't':
          this.toggleTheaterMode()
          break
        case 'i':
          this.togglePictureInPicture()
          break
      }
    }
  }
}
</script>

<style scoped>
.video-player-container {
  position: relative;
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
  background: #000;
  overflow: hidden;
  border-radius: 4px;
}

.video-element {
  width: 100%;
  display: block;
  cursor: pointer;
}

.loading-indicator {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.5);
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  border-top-color: #fff;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.error-message {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  background: rgba(0, 0, 0, 0.7);
  padding: 20px;
  text-align: center;
}

.controls-container {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
  padding: 10px;
  box-sizing: border-box;
  transition: opacity 0.3s;
}

.controls {
  display: flex;
  align-items: center;
  gap: 10px;
  color: white;
  font-size: 14px;
}

.controls-hidden {
  opacity: 0;
  pointer-events: none;
}

.progress-container {
  width: 100%;
  height: 10px;
  margin-bottom: 10px;
  position: relative;
  cursor: pointer;
}

.progress-bar {
  width: 100%;
  height: 100%;
  cursor: pointer;
}

.buffer-bar {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  background: rgba(255, 255, 255, 0.3);
  pointer-events: none;
}

.control-btn {
  background: none;
  border: none;
  color: white;
  cursor: pointer;
  padding: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 3px;
}

.control-btn:hover {
  background: rgba(255, 255, 255, 0.2);
}

.time-display {
  min-width: 120px;
  text-align: center;
  font-size: 12px;
}

.volume-control {
  width: 80px;
}

.volume-slider {
  width: 100%;
  height: 4px;
  cursor: pointer;
}

.quality-selector select,
.speed-selector select {
  background: rgba(0, 0, 0, 0.3);
  color: white;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 3px;
  padding: 2px 5px;
  cursor: pointer;
}

/* 宽屏模式 */
.video-player-container.web-fullscreen {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  max-width: 100%;
  z-index: 1000;
  background: #000;
}

/* 响应式设计 */
@media (max-width: 600px) {
  .controls {
    flex-wrap: wrap;
  }
  
  .time-display {
    order: 3;
    width: 100%;
    text-align: center;
    margin-top: 5px;
  }
  
  .volume-control {
    width: 50px;
  }
  
  .quality-selector,
  .speed-selector {
    max-width: 80px;
  }
}
</style>

二、底层技术解析

1. 视频格式与码率

视频格式选择
  • MP4 (H.264): 最广泛支持的格式,兼容性好,但文件较大
  • WebM (VP8/VP9): 开放格式,压缩率高,适合网络传输
  • HLS (HTTP Live Streaming): 苹果开发的流媒体协议,支持自适应码率
  • DASH (Dynamic Adaptive Streaming over HTTP): 国际标准,支持多码率自适应
码率与清晰度
  • 码率 (Bitrate): 视频数据传输速率,单位kbps (千比特每秒)
  • 常见清晰度对应的码率范围:
    • 240p: 300-800 kbps
    • 360p: 800-1200 kbps
    • 480p: 1200-2000 kbps
    • 720p: 2000-5000 kbps
    • 1080p: 5000-10000 kbps
    • 4K: 15000-40000 kbps
自适应码率实现原理
javascript 复制代码
// 伪代码示例:自适应码率选择
function selectOptimalQuality() {
  // 获取网络信息
  const connection = navigator.connection || { effectiveType: '4g', downlink: 10 }
  
  // 根据网络状况选择清晰度
  if (connection.downlink > 5) {
    return '1080p' // 高网速选择高清
  } else if (connection.downlink > 2) {
    return '720p'
  } else if (connection.downlink > 1) {
    return '480p'
  } else {
    return '360p' // 低网速选择低清
  }
}

// 监听网络变化
navigator.connection.addEventListener('change', () => {
  const newQuality = selectOptimalQuality()
  if (newQuality !== currentQuality) {
    switchQuality(newQuality)
  }
})

2. 画中画 (PiP) 实现细节

javascript 复制代码
// 画中画事件监听
videoElement.addEventListener('enterpictureinpicture', () => {
  console.log('进入画中画模式')
  // 可以调整UI显示
})

videoElement.addEventListener('leavepictureinpicture', () => {
  console.log('退出画中画模式')
  // 恢复原始UI
})

// 请求画中画
async function enterPiP() {
  try {
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture()
    }
    await videoElement.requestPictureInPicture()
  } catch (error) {
    console.error('画中画错误:', error)
  }
}

3. 全屏API实现细节

javascript 复制代码
// 全屏API封装
const fullscreen = {
  request(element) {
    if (element.requestFullscreen) {
      return element.requestFullscreen()
    } else if (element.webkitRequestFullscreen) {
      return element.webkitRequestFullscreen()
    } else if (element.msRequestFullscreen) {
      return element.msRequestFullscreen()
    }
    return Promise.reject(new Error('全屏API不支持'))
  },
  
  exit() {
    if (document.exitFullscreen) {
      return document.exitFullscreen()
    } else if (document.webkitExitFullscreen) {
      return document.webkitExitFullscreen()
    } else if (document.msExitFullscreen) {
      return document.msExitFullscreen()
    }
    return Promise.reject(new Error('全屏API不支持'))
  },
  
  isFullscreen() {
    return !!(
      document.fullscreenElement ||
      document.webkitFullscreenElement ||
      document.msFullscreenElement
    )
  },
  
  addEventListener(callback) {
    document.addEventListener('fullscreenchange', callback)
    document.addEventListener('webkitfullscreenchange', callback)
    document.addEventListener('msfullscreenchange', callback)
  },
  
  removeEventListener(callback) {
    document.removeEventListener('fullscreenchange', callback)
    document.removeEventListener('webkitfullscreenchange', callback)
    document.removeEventListener('msfullscreenchange', callback)
  }
}

4. 视频解码与性能优化

硬件加速

现代浏览器默认使用硬件加速解码视频,可以通过以下方式验证:

javascript 复制代码
// 检查视频是否使用硬件加速
function isHardwareAccelerated(videoElement) {
  // 注意:没有标准API直接检测,但可以通过性能特征间接判断
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
  
  if (ctx) {
    const debugInfo = ctx.getExtension('WEBGL_debug_renderer_info')
    if (debugInfo) {
      const renderer = ctx.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
      return renderer.includes('Intel') || 
             renderer.includes('NVIDIA') || 
             renderer.includes('AMD')
    }
  }
  return false
}
内存优化
javascript 复制代码
// 动态调整分辨率
function adjustResolutionBasedOnMemory() {
  if ('deviceMemory' in navigator) {
    console.log(`设备内存: ${navigator.deviceMemory}GB`)
    // 低内存设备使用较低清晰度
    if (navigator.deviceMemory < 4) {
      return '480p'
    }
  }
  return 'auto'
}

三、使用示例

vue 复制代码
<template>
  <div>
    <VideoPlayer
      ref="player"
      :sources="videoSources"
      :tracks="subtitles"
      :initial-quality="'720p'"
      @play="onPlay"
      @pause="onPause"
      @quality-change="onQualityChange"
    />
    
    <div class="controls">
      <button @click="play">播放</button>
      <button @click="pause">暂停</button>
      <button @click="next">下一个</button>
      <button @click="toggleTheater">宽屏</button>
    </div>
  </div>
</template>

<script>
import VideoPlayer from './components/VideoPlayer.vue'

export default {
  components: { VideoPlayer },
  data() {
    return {
      videoSources: [
        { src: 'video_360p.mp4', type: 'video/mp4', quality: '360p' },
        { src: 'video_480p.mp4', type: 'video/mp4', quality: '480p' },
        { src: 'video_720p.mp4', type: 'video/mp4', quality: '720p' },
        { src: 'video_1080p.mp4', type: 'video/mp4', quality: '1080p' }
      ],
      subtitles: [
        { src: 'subtitles_en.vtt', kind: 'subtitles', srclang: 'en', label: 'English' },
        { src: 'subtitles_zh.vtt', kind: 'subtitles', srclang: 'zh', label: '中文' }
      ]
    }
  },
  methods: {
    play() {
      this.$refs.player.play()
    },
    pause() {
      this.$refs.player.pause()
    },
    next() {
      this.$refs.player.playNext()
    },
    toggleTheater() {
      this.$refs.player.toggleTheaterMode()
    },
    onPlay() {
      console.log('视频开始播放')
    },
    onPause() {
      console.log('视频已暂停')
    },
    onQualityChange(quality) {
      console.log('清晰度切换:', quality)
    }
  }
}
</script>

四、高级功能扩展建议

  1. 自适应码率流媒体 (ABR)

    • 实现HLS或DASH协议支持
    • 根据网络带宽动态调整视频质量
  2. 视频预加载

    • 使用<link rel="preload">预加载关键视频片段
    • 实现智能预加载策略
  3. 缩略图预览

    • 在进度条上实现缩略图预览功能
    • 可以使用雪碧图或单独加载缩略图
  4. 播放记录同步

    • 使用IndexedDB存储观看进度
    • 实现多设备同步
  5. 画质增强技术

    • 实现简单的锐化或对比度增强
    • 使用WebAssembly实现更复杂的图像处理
  6. 低延迟播放

    • 实现WebRTC低延迟播放
    • 支持超低延迟直播场景

这个实现提供了完整的视频播放器功能,涵盖了所有你要求的功能点,并深入到了视频播放的底层技术实现。组件设计为高度可配置,可以通过props进行定制,并通过事件与父组件通信。

相关推荐
G等你下课10 分钟前
告别刷新就丢数据!localStorage 全面指南
前端·javascript
该用户已不存在10 分钟前
不知道这些工具,难怪的你的Python开发那么慢丨Python 开发必备的6大工具
前端·后端·python
爱编程的喵13 分钟前
JavaScript闭包实战:从类封装到防抖函数的深度解析
前端·javascript
LovelyAqaurius13 分钟前
Unity URP管线着色器库攻略part1
前端
Xy91016 分钟前
开发者视角:App Trace 一键拉起(Deep Linking)技术详解
java·前端·后端
lalalalalalalala19 分钟前
开箱即用的 Vue3 无限平滑滚动组件
前端·vue.js
前端Hardy19 分钟前
8个你必须掌握的「Vue」实用技巧
前端·javascript·vue.js
snakeshe101021 分钟前
深入理解 React 中 useEffect 的 cleanUp 机制
前端
星月日22 分钟前
深拷贝还在用lodash吗?来试试原装的structuredClone()吧!
前端·javascript
爱学习的茄子23 分钟前
JavaScript闭包实战:解析节流函数的精妙实现 🚀
前端·javascript·面试