Vue3+TS 视频播放器组件封装(Video.js + Hls.js 最佳组合)

📺 前端开发者必看!2025年最全视频播放器终极指南:6大热门工具深度横评+实战避坑指南


🔥 为什么你需要这篇指南?

作为前端开发者,你是否遇到过这些问题:

  • 播放器在不同浏览器疯狂报错?
  • HLS/DASH流媒体加载卡成PPT?
  • 弹幕功能实现耗费两周还掉头发?
  • 老板突然要求支持8K HDR却不知从何下手?

本文为你带来六大主流播放器深度解剖,从底层原理到实战技巧,一次性解决所有视频播放难题!


🎮 六大播放器功能天梯图(2024版)

播放器 核心定位 协议支持 杀手锏功能 适用场景
Video.js 全能老将 HLS/DASH/MP4/FLV 700+插件生态 企业级复杂项目
Hls.js HLS救星 HLS(TS分片) 破解Safari垄断 直播/点播网站
DPlayer 二次元福音 MP4/FLV/WebTorrent 弹幕系统开箱即用 弹幕视频网站
Plyr 颜值担当 HTML5标准格式 无障碍支持+响应式美学 品牌官网/教育平台
ReactPlayer React生态专属 全协议通吃 声明式API+SSR友好 React技术栈项目
JW Player 商业王者 DRM/HLS/DASH/IMA广告 企业级监控系统 大型流媒体平台

💻 格式支持生死战

1. MP4

  • 全支持,但注意H.264专利陷阱
  • 最佳实践 :使用videojs-contrib-hls解决老版本IE兼容

2. HLS (m3u8+TS)

  • Hls.js:通过MSE在Chrome/Firefox实现解码
  • 避坑指南:iOS强制使用原生播放器

3. DASH

  • Video.js + dash.js组合技
  • 黑科技:自适应码率算法优化

4. FLV

  • DPlayer + flv.js黄金搭档
  • 性能预警:CPU解码消耗较大

🔧 核心问题解决矩阵

痛点 解决方案 技术原理揭秘
Safari HLS垄断 Hls.js模拟MSE管道 将TS流实时转封装为fMP4
弹幕卡顿 WebGL渲染+轨道分层算法 独立合成层避免重绘
首屏加载慢 分片预加载+Range请求优化 HTTP/2多路复用传输
4K卡顿 WASM SIMD解码加速 多线程FFmpeg编译
DRM版权保护 Widevine+硬件级解密 TEE可信执行环境

🚨 血泪教训------开发者必知的5大陷阱

  1. iOS静音策略 :自动播放必须添加muted属性
  2. 内存泄漏重灾区:Hls.js实例必须手动销毁
  3. 字幕编码坑:UTF-8 with BOM是万恶之源
  4. 跨域危机:CORS配置错误导致MP4无法seek
  5. 广告黑屏:IMA SDK需要动态加载证书链

🌟 最佳实践清单

场景1:企业官网宣传视频

  • 选型:Plyr + WebM VP9

  • 配置:

    html 复制代码
    <plyr crossorigin playsinline>  
      <source src="video.webm" type="video/webm">  
      <track kind="captions" label="中文字幕" src="subs.vtt" default>  
    </plyr>  

场景2:直播平台

  • 选型:Video.js + Hls.js

  • 秘技:

    js 复制代码
    // 开启硬件加速  
    video.style.transform = 'translateZ(0)';  
    // 关键帧预加载  
    hls.config.maxBufferLength = 30;  

场景3:二次元弹幕站

  • 选型:DPlayer + WebSocket

  • 优化:

    js 复制代码
    // 弹幕轨道分区  
    danmaku: {  
      channels: {  
        top: 3,    // 顶部3轨道  
        bottom: 2  // 底部2轨道  
      }  
    }  

🔮 未来趋势预警

  1. WebCodecs API:直接操作媒体帧的革命
  2. WebTransport:替代WebSocket的下一代传输协议
  3. AV1编码:Chrome 90+默认支持的次世代格式
  4. WebGPU渲染:3D弹幕/VR视频的新可能

🔧 技术栈搭配推荐

markdown 复制代码
- 中小企业标配:Plyr + Vimeo CDN  
- 高并发直播:Video.js + Wowza引擎  
- 黑科技实验室:WebCodecs + WebGPU  

掌握播放器核心技术,从此告别视频需求焦虑!💪

Video.js + Hls.js 最佳组合

基于上述分析,我选择 Video.js + Hls.js 作为基础技术栈,这是目前最稳定且功能全面的组合:

  • Video.js:提供强大的插件生态和全面的协议支持
  • Hls.js:解决HLS跨浏览器兼容问题
  • Vue3 Composition API:更清晰的逻辑组织和生命周期管理

1. 组件目录结构

md 复制代码
/components
  /VideoPlayer
    index.ts            # 导出组件
    VideoPlayer.vue     # 主组件(script setup版本)
    types.ts            # 类型定义
    useVideoPlayer.ts   # 组合式函数
    utils.ts            # 工具函数
    default-options.ts  # 默认配置

2. 核心代码实现

types.ts

typescript 复制代码
// 播放器配置类型定义
export interface VideoPlayerOptions {
  autoplay?: boolean;          // 是否自动播放
  muted?: boolean;             // 是否静音
  loop?: boolean;              // 是否循环播放
  controls?: boolean;          // 是否显示控制栏
  preload?: 'auto' | 'metadata' | 'none';  // 预加载策略
  fluid?: boolean;             // 是否自适应容器
  aspectRatio?: string;        // 宽高比,如"16:9"
  poster?: string;             // 封面图片URL
  sources: VideoSource[];      // 视频源列表
  hlsConfig?: Record<string, any>; // HLS配置选项
  plugins?: Record<string, any>;   // 插件配置
}

// 视频源定义
export interface VideoSource {
  src: string;  // 视频URL
  type: 'video/mp4' | 'application/x-mpegURL' | 'video/webm' | 'video/ogg' | 'application/dash+xml'; // 媒体类型
  size?: number; // 清晰度,如720, 1080等
}

// 播放器事件回调定义
export interface VideoPlayerEvents {
  onReady?: (player: any) => void;        // 播放器就绪
  onPlay?: (event: any) => void;          // 开始播放
  onPause?: (event: any) => void;         // 暂停播放
  onEnded?: (event: any) => void;         // 播放结束
  onError?: (error: any) => void;         // 播放错误
  onTimeUpdate?: (event: any) => void;    // 时间更新
  onSeeking?: (event: any) => void;       // 开始跳转
  onSeeked?: (event: any) => void;        // 跳转完成
  onQualityChanged?: (quality: number) => void; // 清晰度切换
}

// videojs实例类型
export type VideoPlayerInstance = any;

default-options.ts

typescript 复制代码
import { VideoPlayerOptions } from './types';

// 播放器默认配置
export const defaultOptions: Partial<VideoPlayerOptions> = {
  autoplay: false,       // 默认不自动播放
  muted: false,          // 默认不静音
  loop: false,           // 默认不循环
  controls: true,        // 默认显示控制栏
  preload: 'auto',       // 默认预加载策略
  fluid: true,           // 默认自适应容器
  aspectRatio: '16:9',   // 默认宽高比
  
  // 播放器技术优先级
  techOrder: ['html5'],
  
  // 语言设置
  language: 'zh-CN',
  
  // 控制栏配置
  controlBar: {
    playToggle: true,              // 播放/暂停按钮
    currentTimeDisplay: true,      // 当前时间
    timeDivider: true,             // 时间分隔符
    durationDisplay: true,         // 总时长
    progressControl: true,         // 进度条
    volumePanel: {
      inline: false,               // 音量控制是否内联
    },
    fullscreenToggle: true,        // 全屏按钮
  },
  
  // HLS默认配置
  hlsConfig: {
    enableWorker: true,           // 启用WebWorker
    lowLatencyMode: false,        // 低延迟模式
    backBufferLength: 90,         // 后退缓冲区长度
    maxBufferLength: 30,          // 最大缓冲长度
  }
};

utils.ts

typescript 复制代码
/**
 * 检测是否为移动设备
 * @returns 是否为移动设备
 */
export const isMobile = (): boolean => {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
};

/**
 * 检测视频格式
 * @param url 视频URL
 * @returns 格式标识符
 */
export const detectFormat = (url: string): string => {
  if (url.includes('.m3u8')) return 'hls';
  if (url.includes('.mpd')) return 'dash';
  if (url.includes('.mp4')) return 'mp4';
  if (url.includes('.webm')) return 'webm';
  if (url.includes('.flv')) return 'flv';
  return 'unknown';
};

/**
 * 检测浏览器是否支持HLS
 * @returns 是否支持HLS
 */
export const isHlsSupported = (): boolean => {
  const video = document.createElement('video');
  return Boolean(
    video.canPlayType('application/vnd.apple.mpegurl') || 
    (typeof Hls !== 'undefined' && Hls.isSupported())
  );
};

/**
 * 检测浏览器是否支持DASH
 * @returns 是否支持DASH
 */
export const isDashSupported = (): boolean => {
  const video = document.createElement('video');
  return Boolean(
    video.canPlayType('application/dash+xml') ||
    (typeof MediaSource !== 'undefined' && MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"'))
  );
};

/**
 * 格式化时间
 * @param seconds 秒数
 * @returns 格式化后的时间字符串
 */
export const formatTime = (seconds: number): string => {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = Math.floor(seconds % 60);
  
  return [
    hours > 0 ? hours : null,
    minutes < 10 && hours > 0 ? `0${minutes}` : minutes,
    secs < 10 ? `0${secs}` : secs
  ]
    .filter(Boolean)
    .join(':');
};

/**
 * 获取最佳视频质量
 * @param sources 视频源列表
 * @param isHighSpeed 是否高速网络
 * @returns 最合适的视频源
 */
export const getBestQuality = (sources: any[], isHighSpeed = true): any => {
  const sortedSources = [...sources].sort((a, b) => (b.size || 0) - (a.size || 0));
  
  if (isHighSpeed) {
    // 高速网络返回最高质量
    return sortedSources[0];
  } else {
    // 低速网络返回中等质量
    const midIndex = Math.floor(sortedSources.length / 2);
    return sortedSources[midIndex] || sortedSources[0];
  }
};

VideoPlayer.vue (Script Setup版本)

html 复制代码
<template>
  <div class="vue-video-player-container" :style="containerStyle">
    <video
      ref="videoRef"
      class="video-js vjs-big-play-centered"
      :class="playerClass"
      :poster="options.poster"
    ></video>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
import Hls from 'hls.js';
import { VideoPlayerOptions, VideoPlayerEvents, VideoSource } from './types';
import { defaultOptions } from './default-options';
import { isMobile, detectFormat, isHlsSupported, isDashSupported } from './utils';

// 定义组件属性
interface Props {
  options: VideoPlayerOptions;            // 播放器配置
  events?: VideoPlayerEvents;             // 事件处理函数
  responsive?: boolean;                   // 是否响应式
  playerClass?: string;                   // 播放器自定义类名
}

// 组件事件
const emits = defineEmits<{
  (e: 'ready', player: any): void;
  (e: 'play', event: any): void;
  (e: 'pause', event: any): void;
  (e: 'ended', event: any): void;
  (e: 'error', error: any): void;
  (e: 'timeupdate', event: any): void;
  (e: 'quality-changed', quality: number): void;
}>();

// 使用withDefaults设置属性默认值
const props = withDefaults(defineProps<Props>(), {
  events: () => ({}),
  responsive: true,
  playerClass: ''
});

// 响应式状态
const videoRef = ref<HTMLVideoElement | null>(null);
const player = ref<any>(null);
const hls = ref<Hls | null>(null);
const currentQuality = ref<number>(0);

// 计算属性:容器样式
const containerStyle = computed(() => ({
  width: props.responsive ? '100%' : undefined,
  position: 'relative' as const,
  overflow: 'hidden' as const,
}));

/**
 * 初始化播放器
 */
const initializePlayer = () => {
  if (!videoRef.value) return;

  // 合并默认配置
  const finalOptions = {
    ...defaultOptions,
    ...props.options,
    // iOS需要特殊处理
    playsinline: true,
    // 移动设备处理
    inactivityTimeout: isMobile() ? 3000 : 2000,
  };

  // 创建Video.js实例
  player.value = videojs(videoRef.value, finalOptions, function() {
    // 播放器就绪
    emits('ready', player.value);
    if (props.events?.onReady) props.events.onReady(player.value);
  });

  // 注册事件处理
  registerEvents();
  
  // 处理HLS格式
  setupHlsSupport();
  
  // 处理自适应码率
  setupQualityControl();
  
  // 性能优化
  applyPerformanceOptimizations();
};

/**
 * 注册事件监听
 */
const registerEvents = () => {
  if (!player.value) return;
  
  const eventsMap = [
    { name: 'play', handler: props.events?.onPlay, emit: 'play' },
    { name: 'pause', handler: props.events?.onPause, emit: 'pause' },
    { name: 'ended', handler: props.events?.onEnded, emit: 'ended' },
    { name: 'error', handler: props.events?.onError, emit: 'error' },
    { name: 'timeupdate', handler: props.events?.onTimeUpdate, emit: 'timeupdate' },
    { name: 'seeking', handler: props.events?.onSeeking },
    { name: 'seeked', handler: props.events?.onSeeked },
  ];
  
  eventsMap.forEach(event => {
    player.value.on(event.name, (e: any) => {
      if (event.handler) event.handler(e);
      if (event.emit) emits(event.emit as any, e);
    });
  });
};

/**
 * 设置HLS支持
 */
const setupHlsSupport = () => {
  if (!player.value) return;
  
  const hlsSource = props.options.sources.find(
    source => source.type === 'application/x-mpegURL'
  );
  
  if (!hlsSource) return;
  
  // 如果浏览器原生支持HLS,使用原生支持
  if (videoRef.value?.canPlayType('application/vnd.apple.mpegurl')) {
    player.value.src(hlsSource.src);
    return;
  }
  
  // 否则使用Hls.js
  if (Hls.isSupported()) {
    // 销毁已存在的Hls实例
    if (hls.value) {
      hls.value.destroy();
    }
    
    hls.value = new Hls({
      maxBufferLength: 30,
      maxMaxBufferLength: 60,
      ...props.options.hlsConfig,
    });
    
    hls.value.loadSource(hlsSource.src);
    hls.value.attachMedia(videoRef.value as HTMLVideoElement);
    
    // 错误处理
    hls.value.on(Hls.Events.ERROR, function(event, data) {
      if (data.fatal) {
        switch(data.type) {
          case Hls.ErrorTypes.NETWORK_ERROR:
            // 尝试恢复网络错误
            hls.value?.startLoad();
            break;
          case Hls.ErrorTypes.MEDIA_ERROR:
            // 尝试恢复媒体错误
            hls.value?.recoverMediaError();
            break;
          default:
            // 无法恢复的错误
            destroyPlayer();
            break;
        }
        
        emits('error', data);
        if (props.events?.onError) props.events.onError(data);
      }
    });
  }
};

/**
 * 设置清晰度切换
 */
const setupQualityControl = () => {
  if (!player.value || !props.options.sources || props.options.sources.length <= 1) return;
  
  // 如果有多个清晰度源
  if (props.options.sources.filter(s => s.size).length > 1) {
    const qualities = props.options.sources
      .filter(s => s.size)
      .sort((a, b) => (b.size || 0) - (a.size || 0));
      
    // 创建分辨率菜单
    const qualityMenu = player.value.controlBar.addChild('MenuButton', {
      controlText: '清晰度',
      menuButton: true,
    });
    
    // 添加清晰度选项
    qualities.forEach((quality, index) => {
      qualityMenu.addChild('MenuItem', {
        label: `${quality.size}p`,
        selected: index === 0,
        handleClick: () => {
          // 切换清晰度
          const currentTime = player.value.currentTime();
          const isPaused = player.value.paused();
          
          player.value.src({ src: quality.src, type: quality.type });
          player.value.load();
          player.value.currentTime(currentTime);
          
          if (!isPaused) {
            player.value.play();
          }
          
          currentQuality.value = index;
          emits('quality-changed', quality.size || 0);
          if (props.events?.onQualityChanged) {
            props.events.onQualityChanged(quality.size || 0);
          }
        }
      });
    });
  }
};

/**
 * 应用性能优化
 */
const applyPerformanceOptimizations = () => {
  if (!videoRef.value || !player.value) return;
  
  // 开启硬件加速
  videoRef.value.style.transform = 'translateZ(0)';
  
  // 预加载下一帧
  if (props.options.preload !== 'none') {
    player.value.preload('auto');
  }
  
  // 如果是移动设备,降低初始化清晰度
  if (isMobile() && props.options.sources.length > 1) {
    const lowestQuality = [...props.options.sources]
      .filter(s => s.size)
      .sort((a, b) => (a.size || 0) - (b.size || 0))[0];
      
    if (lowestQuality) {
      player.value.src({ src: lowestQuality.src, type: lowestQuality.type });
    }
  }
  
  // 监听网络状况,动态调整缓冲策略
  if (navigator.connection) {
    const connection = navigator.connection as any;
    if (connection.addEventListener) {
      connection.addEventListener('change', () => {
        if (connection.effectiveType === '4g') {
          // 高速连接,预加载更多
          if (hls.value) {
            hls.value.config.maxBufferLength = 60;
          }
        } else if (connection.effectiveType === '3g' || connection.effectiveType === '2g') {
          // 低速连接,减少预加载
          if (hls.value) {
            hls.value.config.maxBufferLength = 15;
          }
        }
      });
    }
  }
};

/**
 * 清理资源
 */
const destroyPlayer = () => {
  if (hls.value) {
    hls.value.destroy();
    hls.value = null;
  }
  
  if (player.value) {
    player.value.dispose();
    player.value = null;
  }
};

// 监听配置变化
watch(() => props.options.sources, () => {
  if (player.value) {
    destroyPlayer();
    nextTick(initializePlayer);
  }
}, { deep: true });

// 生命周期钩子
onMounted(() => {
  initializePlayer();
});

onBeforeUnmount(() => {
  destroyPlayer();
});

// 暴露方法和属性给父组件
defineExpose({
  player,
  videoRef,
  destroyPlayer
});
</script>

<style scoped>
.vue-video-player-container {
  margin: 0 auto;
  position: relative;
  overflow: hidden;
}

:deep(.video-js) {
  width: 100%;
  height: 100%;
}

:deep(.vjs-fullscreen) {
  background-color: #000;
}

/* 自定义控制栏样式 */
:deep(.vjs-control-bar) {
  background-color: rgba(0, 0, 0, 0.7);
}

/* 移动端优化 */
@media (max-width: 768px) {
  :deep(.vjs-big-play-button) {
    transform: scale(0.8);
  }
  
  :deep(.vjs-control-bar) {
    font-size: 0.9em;
  }
}
</style>

useVideoPlayer.ts

typescript 复制代码
import { ref, Ref, onMounted, onBeforeUnmount, watch } from 'vue';
import videojs from 'video.js';
import Hls from 'hls.js';
import { VideoPlayerOptions, VideoPlayerInstance } from './types';
import { defaultOptions } from './default-options';

/**
 * 视频播放器组合式API
 * @param options 播放器配置选项
 * @param elementRef DOM元素引用
 * @returns 播放器控制对象
 */
export const useVideoPlayer = (
  options: VideoPlayerOptions,
  elementRef: Ref<HTMLElement | null>
) => {
  const player: Ref<VideoPlayerInstance | null> = ref(null);
  const hls: Ref<Hls | null> = ref(null);
  const isReady = ref(false);
  const isPlaying = ref(false);
  const currentTime = ref(0);
  const duration = ref(0);
  const loadedPercent = ref(0);
  const error = ref<any>(null);

  /**
   * 初始化播放器
   */
  const initialize = () => {
    if (!elementRef.value) return;
    
    const finalOptions = { ...defaultOptions, ...options };
    
    // 创建播放器实例
    player.value = videojs(elementRef.value, finalOptions);
    
    // 事件监听
    player.value.on('ready', () => {
      isReady.value = true;
    });
    
    player.value.on('play', () => {
      isPlaying.value = true;
    });
    
    player.value.on('pause', () => {
      isPlaying.value = false;
    });
    
    player.value.on('timeupdate', () => {
      if (player.value) {
        currentTime.value = player.value.currentTime();
      }
    });
    
    player.value.on('loadedmetadata', () => {
      if (player.value) {
        duration.value = player.value.duration();
      }
    });
    
    player.value.on('progress', () => {
      if (player.value) {
        const buffered = player.value.buffered();
        if (buffered && buffered.length > 0) {
          loadedPercent.value = buffered.end(0) / duration.value;
        }
      }
    });
    
    player.value.on('error', (e) => {
      error.value = e;
    });

    // 初始化HLS(如果需要)
    initializeHls();
  };

  /**
   * 初始化HLS
   */
  const initializeHls = () => {
    if (!player.value) return;

    const hlsSource = options.sources.find(
      source => source.type === 'application/x-mpegURL'
    );
    
    if (!hlsSource) return;
    
    const video = player.value.el().querySelector('video');
    
    if (!video) return;
    
    // 检查是否原生支持HLS
    if (video.canPlayType('application/vnd.apple.mpegurl')) {
      player.value.src(hlsSource.src);
      return;
    }
    
    // 使用Hls.js
    if (Hls.isSupported()) {
      hls.value = new Hls(options.hlsConfig);
      hls.value.loadSource(hlsSource.src);
      hls.value.attachMedia(video);
      
      hls.value.on(Hls.Events.MANIFEST_PARSED, () => {
        if (options.autoplay) {
          player.value?.play();
        }
      });
      
      hls.value.on(Hls.Events.ERROR, (event, data) => {
        if (data.fatal) {
          switch(data.type) {
            case Hls.ErrorTypes.NETWORK_ERROR:
              hls.value?.startLoad();
              break;
            case Hls.ErrorTypes.MEDIA_ERROR:
              hls.value?.recoverMediaError();
              break;
            default:
              destroy();
              break;
          }
        }
      });
    }
  };

  /**
   * 播放方法
   */
  const play = async () => {
    try {
      if (player.value) {
        await player.value.play();
      }
    } catch (e) {
      console.error('无法自动播放:', e);
    }
  };

  /**
   * 暂停方法
   */
  const pause = () => {
    if (player.value) {
      player.value.pause();
    }
  };

  /**
   * 跳转方法
   * @param time 目标时间(秒)
   */
  const seek = (time: number) => {
    if (player.value) {
      player.value.currentTime(time);
    }
  };

  /**
   * 设置音量
   * @param volume 音量(0-1)
   */
  const setVolume = (volume: number) => {
    if (player.value) {
      player.value.volume(volume);
    }
  };

  /**
   * 切换静音
   */
  const toggleMute = () => {
    if (player.value) {
      player.value.muted(!player.value.muted());
    }
  };

  /**
   * 进入/退出全屏
   */
  const toggleFullscreen = () => {
    if (player.value) {
      if (player.value.isFullscreen()) {
        player.value.exitFullscreen();
      } else {
        player.value.requestFullscreen();
      }
    }
  };

  /**
   * 销毁播放器
   */
  const destroy = () => {
    if (hls.value) {
      hls.value.destroy();
      hls.value = null;
    }
    
    if (player.value) {
      player.value.dispose();
      player.value = null;
    }
    
    isReady.value = false;
    isPlaying.value = false;
    currentTime.value = 0;
    duration.value = 0;
    loadedPercent.value = 0;
    error.value = null;
  };

  // 监视配置变化
  watch(() => options.sources, () => {
    if (player.value) {
      destroy();
      initialize();
    }
  }, { deep: true });

  // 生命周期钩子
  onMounted(initialize);
  onBeforeUnmount(destroy);

  return {
    player,
    isReady,
    isPlaying,
    currentTime,
    duration,
    loadedPercent,
    error,
    play,
    pause,
    seek,
    setVolume,
    toggleMute,
    toggleFullscreen,
    destroy
  };
};

index.ts

typescript 复制代码
import VideoPlayer from './VideoPlayer.vue';
import { useVideoPlayer } from './useVideoPlayer';
import type { VideoPlayerOptions, VideoPlayerEvents, VideoSource } from './types';

export { VideoPlayer, useVideoPlayer, VideoPlayerOptions, VideoPlayerEvents, VideoSource };
export default VideoPlayer;

3. 使用示例

基础调用方式

html 复制代码
<template>
  <!-- 最简单的基础调用 -->
  <VideoPlayer :options="videoOptions" />
</template>

<script setup lang="ts">
import { VideoPlayer } from '@/components/VideoPlayer';
import type { VideoPlayerOptions } from '@/components/VideoPlayer';

// 基础配置
const videoOptions: VideoPlayerOptions = {
  controls: true,
  poster: '/poster.jpg',
  sources: [
    {
      src: 'https://example.com/video.mp4',
      type: 'video/mp4'
    }
  ]
};
</script>

完整调用方式

html 复制代码
<template>
  <div class="player-wrapper">
    <h2>{{ title }}</h2>
    
    <!-- 完整配置的播放器 -->
    <VideoPlayer
      ref="playerRef"
      :options="videoOptions"
      :events="videoEvents"
      :responsive="true"
      playerClass="my-custom-player"
      @ready="onPlayerReady"
      @play="onPlay"
      @pause="onPause" 
      @ended="onEnded"
      @error="onPlayerError"
      @timeupdate="onTimeUpdate"
      @quality-changed="onQualityChanged"
    />
    
    <!-- 自定义控制区域 -->
    <div class="custom-controls">
      <div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
      
      <button @click="togglePlayback" class="control-btn">
        {{ isPlaying ? '暂停' : '播放' }}
      </button>
      
      <button @click="toggleMute" class="control-btn">
        {{ isMuted ? '取消静音' : '静音' }}
      </button>
      
      <button @click="rewind10" class="control-btn">
        -10秒
      </button>
      
      <button @click="forward10" class="control-btn">
        +10秒
      </button>
      
      <button @click="toggleFullscreen" class="control-btn">
        全屏
      </button>
      
      <!-- 清晰度选择 -->
      <div class="quality-selector">
        <span>清晰度:</span>
        <select v-model="selectedQuality" @change="changeQuality">
          <option v-for="source in qualitySources" :key="source.size" :value="source.size">
            {{ source.size }}p
          </option>
        </select>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { VideoPlayer } from '@/components/VideoPlayer';
import type { VideoPlayerOptions, VideoPlayerEvents, VideoSource } from '@/components/VideoPlayer';
import { formatTime } from '@/components/VideoPlayer/utils';

// 标题
const title = ref('高清视频播放器示例');

// 播放器引用
const playerRef = ref<InstanceType<typeof VideoPlayer> | null>(null);

// 播放器状态
const isPlaying = ref(false);
const isMuted = ref(false);
const currentTime = ref(0);
const duration = ref(0);
const selectedQuality = ref(720);

// 提取所有带有尺寸的视频源
const qualitySources = computed(() => {
  return videoOptions.sources.filter(source => source.size).sort((a, b) => (b.size || 0) - (a.size || 0));
});

// 播放器配置
const videoOptions: VideoPlayerOptions = reactive({
  // 基础设置
  autoplay: false,
  muted: false,
  loop: false,
  controls: true,
  fluid: true,
  poster: 'https://example.com/poster.jpg',
  
  // 视频源列表(包含多种格式和清晰度)
  sources: [
    // HLS流(自适应清晰度)
    {
      src: 'https://example.com/video/master.m3u8',
      type: 'application/x-mpegURL',
    },
    // 不同清晰度的MP4
    {
      src: 'https://example.com/video/1080p.mp4',
      type: 'video/mp4',
      size: 1080
    },
    {
      src: 'https://example.com/video/720p.mp4',
      type: 'video/mp4',
      size: 720
    },
    {
      src: 'https://example.com/video/480p.mp4',
      type: 'video/mp4',
      size: 480
    },
    // WebM格式(用于兼容性)
    {
      src: 'https://example.com/video/720p.webm',
      type: 'video/webm',
      size: 720
    }
  ],
  
  // HLS特定配置
  hlsConfig: {
    maxBufferLength: 30,
    maxMaxBufferLength: 60,
    enableWorker: true,
    lowLatencyMode: false,
  }
});

// 播放器事件处理
const videoEvents: VideoPlayerEvents = {
  onReady: (player) => {
    console.log('播放器初始化完成', player);
    // 可以在这里对player进行进一步配置
  },
  
  onPlay: (event) => {
    console.log('开始播放', event);
    isPlaying.value = true;
  },
  
  onPause: (event) => {
    console.log('暂停播放', event);
    isPlaying.value = false;
  },
  
  onEnded: (event) => {
    console.log('播放结束', event);
    isPlaying.value = false;
  },
  
  onError: (error) => {
    console.error('播放出错', error);
    // 可以在这里添加错误恢复逻辑
  },
  
  onTimeUpdate: (event) => {
    // 更新当前播放时间
    if (playerRef.value?.player) {
      currentTime.value = playerRef.value.player.currentTime();
    }
  },
  
  onQualityChanged: (quality) => {
    console.log(`切换到${quality}p清晰度`);
    selectedQuality.value = quality;
  },
};

// 事件处理函数
const onPlayerReady = (player: any) => {
  console.log('播放器就绪');
  
  // 获取视频时长
  duration.value = player.duration();
  
  // 预加载元数据
  player.preload('metadata');
  
  // 添加键盘快捷键
  document.addEventListener('keydown', handleKeyboardControls);
};

const onPlay = (event: any) => {
  console.log('播放事件', event);
  isPlaying.value = true;
};

const onPause = (event: any) => {
  console.log('暂停事件', event);
  isPlaying.value = false;
};

const onEnded = (event: any) => {
  console.log('结束事件', event);
  isPlaying.value = false;
};

const onTimeUpdate = (event: any) => {
  // 更新进度信息
  if (playerRef.value?.player) {
    currentTime.value = playerRef.value.player.currentTime();
  }
};

const onPlayerError = (error: any) => {
  console.error('播放器错误:', error);
  // 显示用户友好的错误消息
};

const onQualityChanged = (quality: number) => {
  console.log(`画质已切换到: ${quality}p`);
  selectedQuality.value = quality;
};

// 自定义控制方法
const togglePlayback = () => {
  if (!playerRef.value?.player) return;
  
  if (isPlaying.value) {
    playerRef.value.player.pause();
  } else {
    playerRef.value.player.play();
  }
};

const toggleMute = () => {
  if (!playerRef.value?.player) return;
  
  const player = playerRef.value.player;
  player.muted(!player.muted());
  isMuted.value = player.muted();
};

const rewind10 = () => {
  if (!playerRef.value?.player) return;
  
  const player = playerRef.value.player;
  const newTime = Math.max(player.currentTime() - 10, 0);
  player.currentTime(newTime);
};

const forward10 = () => {
  if (!playerRef.value?.player) return;
  
  const player = playerRef.value.player;
  const newTime = Math.min(player.currentTime() + 10, player.duration());
  player.currentTime(newTime);
};

const toggleFullscreen = () => {
  if (!playerRef.value?.player) return;
  
  const player = playerRef.value.player;
  if (player.isFullscreen()) {
    player.exitFullscreen();
  } else {
    player.requestFullscreen();
  }
};

// 质量切换
const changeQuality = () => {
  if (!playerRef.value?.player) return;
  
  const player = playerRef.value.player;
  const currentTime = player.currentTime();
  const isPaused = player.paused();
  
  // 寻找选定清晰度的源
  const selectedSource = videoOptions.sources.find(source => source.size === selectedQuality.value);
  
  if (selectedSource) {
    // 切换视频源
    player.src({ src: selectedSource.src, type: selectedSource.type });
    player.load();
    
    // 恢复播放位置
    player.one('loadedmetadata', () => {
      player.currentTime(currentTime);
      
      // 如果之前在播放,继续播放
      if (!isPaused) {
        player.play();
      }
    });
  }
};

// 键盘控制
const handleKeyboardControls = (e: KeyboardEvent) => {
  if (!playerRef.value?.player) return;
  
  const player = playerRef.value.player;
  
  switch(e.key) {
    case ' ':  // 空格键
      togglePlayback();
      e.preventDefault();
      break;
    case 'ArrowLeft':  // 左箭头
      rewind10();
      e.preventDefault();
      break;
    case 'ArrowRight':  // 右箭头
      forward10();
      e.preventDefault();
      break;
    case 'f':  // F键
      toggleFullscreen();
      e.preventDefault();
      break;
    case 'm':  // M键
      toggleMute();
      e.preventDefault();
      break;
  }
};

// 清理工作
onMounted(() => {
  // 在组件卸载时移除事件监听器
  return () => {
    document.removeEventListener('keydown', handleKeyboardControls);
  };
});
</script>

<style scoped>
.player-wrapper {
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

h2 {
  text-align: center;
  margin-bottom: 20px;
}

.custom-controls {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 10px;
  margin-top: 15px;
  padding: 10px;
  background: #f5f5f5;
  border-radius: 4px;
}

.time-display {
  font-family: monospace;
  font-size: 14px;
  margin-right: 15px;
}

.control-btn {
  padding: 8px 15px;
  border-radius: 4px;
  background: #3498db;
  color: white;
  border: none;
  cursor: pointer;
  transition: background 0.3s;
}

.control-btn:hover {
  background: #2980b9;
}

.quality-selector {
  display: flex;
  align-items: center;
  gap: 5px;
}

.quality-selector select {
  padding: 5px;
  border-radius: 4px;
  border: 1px solid #ddd;
}

@media (max-width: 768px) {
  .custom-controls {
    flex-direction: column;
    gap: 8px;
  }
}
</style>

4. 组件逻辑和最佳实践总结

核心逻辑概述

  1. 组件架构

    • 使用<script setup>语法简化组件结构
    • 利用definePropsdefineEmits明确定义接口
    • 通过defineExpose暴露必要方法给父组件
  2. 播放器封装

    • 基于Video.js实现核心播放能力
    • 使用Hls.js解决HLS格式跨浏览器兼容
    • 实现自适应清晰度切换功能
  3. 性能优化

    • CSS硬件加速提高渲染性能
    • 根据网络情况动态调整缓冲
    • 移动设备自动调整初始清晰度
  4. 错误处理和恢复

    • HLS错误自动恢复机制
    • 网络错误智能重试
    • 完善的资源清理避免内存泄漏

最佳实践要点

  1. 兼容性处理

    • iOS视频播放: 添加playsinline属性
    • 自动播放: 默认添加muted属性绕过限制
    • 格式兼容: 提供多种格式回退机制
  2. 性能优化

    typescript 复制代码
    // 硬件加速
    videoRef.value.style.transform = 'translateZ(0)';
    
    // 移动设备降低初始清晰度
    if (isMobile() && sources.length > 1) {
      player.src({ src: lowestQuality.src, type: lowestQuality.type });
    }
    
    // 网络自适应缓冲
    if (connection.effectiveType === '4g') {
      hls.config.maxBufferLength = 60;
    } else {
      hls.config.maxBufferLength = 15;
    }
  3. 资源管理

    typescript 复制代码
    // 必须销毁Hls实例避免内存泄漏
    onBeforeUnmount(() => {
      if (hls.value) {
        hls.value.destroy();
        hls.value = null;
      }
      
      if (player.value) {
        player.value.dispose();
        player.value = null;
      }
    });
  4. 错误恢复策略

    typescript 复制代码
    hls.on(Hls.Events.ERROR, (event, data) => {
      if (data.fatal) {
        switch(data.type) {
          case Hls.ErrorTypes.NETWORK_ERROR:
            // 网络错误尝试恢复
            hls.startLoad();
            break;
          case Hls.ErrorTypes.MEDIA_ERROR:
            // 媒体错误尝试恢复
            hls.recoverMediaError();
            break;
        }
      }
    });

5. 总结

本组件实现了一个功能完善、性能优化的视频播放器,主要特点:

  • 强大格式支持:HLS/MP4/WebM等主流视频格式
  • 跨浏览器兼容:解决了iOS/Safari的HLS播放问题
  • 响应式设计:自适应不同屏幕尺寸
  • 多清晰度切换:支持流畅切换不同清晰度
  • 性能优化:硬件加速、网络感知缓冲
  • 类型安全:完整的TypeScript类型定义

相比原始HTML5 video标签,此组件解决了各平台兼容性问题并提供了更友好的用户体验,适合中大型Web应用中的视频播放需求。

相关推荐
returnShitBoy1 小时前
前端面试:如何实现预览 PDF 文件?
前端·pdf
烂蜻蜓2 小时前
HTML 表格的详细介绍与应用
开发语言·前端·css·html·html5
圣京都4 小时前
react和vue 基础使用对比
javascript·vue.js·react.js
returnShitBoy4 小时前
前端面试:axios 是否可以取消请求?
前端
u0103754564 小时前
fiddler+雷电模拟器(安卓9)+https配置
前端·测试工具·fiddler
海上彼尚4 小时前
Vue3中全局使用Sass变量方法
前端·css·sass
ᥬ 小月亮4 小时前
TypeScript基础
前端·javascript·typescript
烛阴5 小时前
JavaScript 函数进阶之:Rest 参数与 Spread 语法(二)
前端·javascript
GISer_Jing5 小时前
ES6回顾:闭包->(优点:实现工厂函数、记忆化和异步实现)、(应用场景:Promise的then与catch的回调、async/await、柯里化函数)
前端·ecmascript·es6