📺 前端开发者必看!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大陷阱
- iOS静音策略 :自动播放必须添加
muted
属性 - 内存泄漏重灾区:Hls.js实例必须手动销毁
- 字幕编码坑:UTF-8 with BOM是万恶之源
- 跨域危机:CORS配置错误导致MP4无法seek
- 广告黑屏: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轨道 } }
🔮 未来趋势预警
- WebCodecs API:直接操作媒体帧的革命
- WebTransport:替代WebSocket的下一代传输协议
- AV1编码:Chrome 90+默认支持的次世代格式
- 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. 组件逻辑和最佳实践总结
核心逻辑概述
-
组件架构
- 使用
<script setup>
语法简化组件结构 - 利用
defineProps
和defineEmits
明确定义接口 - 通过
defineExpose
暴露必要方法给父组件
- 使用
-
播放器封装
- 基于Video.js实现核心播放能力
- 使用Hls.js解决HLS格式跨浏览器兼容
- 实现自适应清晰度切换功能
-
性能优化
- CSS硬件加速提高渲染性能
- 根据网络情况动态调整缓冲
- 移动设备自动调整初始清晰度
-
错误处理和恢复
- HLS错误自动恢复机制
- 网络错误智能重试
- 完善的资源清理避免内存泄漏
最佳实践要点
-
兼容性处理
- iOS视频播放: 添加
playsinline
属性 - 自动播放: 默认添加
muted
属性绕过限制 - 格式兼容: 提供多种格式回退机制
- iOS视频播放: 添加
-
性能优化
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; }
-
资源管理
typescript// 必须销毁Hls实例避免内存泄漏 onBeforeUnmount(() => { if (hls.value) { hls.value.destroy(); hls.value = null; } if (player.value) { player.value.dispose(); player.value = null; } });
-
错误恢复策略
typescripthls.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应用中的视频播放需求。