前端媒体资源完全指南 #
从图片格式选择到视频直播实现,从懒加载优化到西瓜播放器集成,覆盖 Vue3 项目中媒体资源的完整解决方案
目录 #
- 核心概念
- 图片格式与优化
- 图片懒加载实现
- Vue3 图片网站优化
- 视频格式与编码
- 视频播放器选型
- 西瓜播放器详解
- 直播流实现
- 视频网站性能优化
- 实战场景
- 常见问题
- 总结速记
一、核心概念 #
1.1 媒体资源类型概览 #
前端项目中的媒体资源主要分为两大类:
| 类型 |
常见格式 |
特点 |
典型场景 |
| 图片 |
JPEG、PNG、WebP、AVIF、GIF、SVG |
静态、体积较小、加载快 |
商品图、头像、背景图 |
| 视频 |
MP4、WebM、HLS、DASH |
动态、体积大、需要流式加载 |
点播、直播、短视频 |
| 音频 |
MP3、AAC、OGG、Opus |
只有声音 |
音乐播放、语音消息 |
| 直播流 |
HLS、FLV、DASH、RTMP |
实时传输、低延迟要求 |
直播、监控、会议 |
1.2 核心名词解释 #
| 名词 |
大白话解释 |
举例 |
| 编码格式 |
视频/图片的"压缩方式",决定质量和体积 |
H.264、H.265、VP9 |
| 封装格式 |
视频/图片的"容器格式",文件后缀名 |
.mp4、.webm、.mkv |
| 码率(Bitrate) |
每秒的数据量,越高越清晰但体积越大 |
1080p 通常 4-8 Mbps |
| 分辨率 |
视频的宽高像素数 |
720p、1080p、4K |
| 帧率(FPS) |
每秒的画面数,越高越流畅 |
24fps(电影)、60fps(游戏) |
| 关键帧(I帧) |
完整的一帧画面,可独立解码 |
直播切片的起点 |
| P帧/B帧 |
差异帧,依赖其他帧解码 |
压缩率更高 |
| CDN |
内容分发网络,就近获取资源 |
加速图片/视频加载 |
| 流媒体 |
边下边播,不需要全部下载 |
HLS、DASH |
| 转码 |
转换视频编码格式 |
H.264 → H.265 |
1.3 编码 vs 封装(重要区分) #
很多人搞混编码和封装:
文件:video.mp4
↓
封装格式:MP4(容器,里面可以装不同编码的视频)
↓
视频编码:H.264、H.265、VP9(压缩算法)
音频编码:AAC、MP3、Opus(声音压缩)
举个例子:
| 文件名 |
封装格式 |
视频编码 |
音频编码 |
| video.mp4 |
MP4 |
H.264 |
AAC |
| video.webm |
WebM |
VP9 |
Opus |
| video.mkv |
MKV |
H.265 |
AAC |
| video.avi |
AVI |
MPEG-4 |
MP3 |
核心要点:
- 封装格式决定文件后缀和兼容性
- 编码格式决定压缩效率和浏览器支持
- 同一个封装格式可以装不同编码的视频
二、图片格式与优化 #
2.1 图片格式对比 #
| 格式 |
优点 |
缺点 |
适用场景 |
浏览器支持 |
| JPEG |
压缩率高、兼容性好 |
不支持透明、有损压缩 |
照片、商品图 |
✅ 全支持 |
| PNG |
支持透明、无损压缩 |
体积较大 |
图标、Logo、截图 |
✅ 全支持 |
| WebP |
体积小(比 JPEG 小 30%)、支持透明 |
老浏览器不支持 |
现代网站的通用选择 |
✅ 97%+ |
| AVIF |
压缩率最高(比 WebP 还小) |
编码慢、兼容性一般 |
追求极致压缩 |
⚠️ 90%+ |
| GIF |
支持动画 |
体积大、颜色少(256色) |
简单动图、表情包 |
✅ 全支持 |
| SVG |
矢量图、无限缩放 |
不适合复杂图片 |
图标、Logo、图表 |
✅ 全支持 |
2.2 格式选择决策树 #
需要图片?
├── 照片/复杂图片
│ ├── 追求兼容 → JPEG
│ └── 追求体积 → WebP 或 AVIF
│
├── 需要透明
│ ├── 老浏览器 → PNG
│ └── 现代浏览器 → WebP
│
├── 图标/Logo
│ ├── 简单图形 → SVG
│ └── 复杂图形 → PNG/WebP
│
├── 需要动画
│ ├── 简单动画 → GIF(或 WebP 动画)
│ └── 复杂动画 → 视频(MP4/WebM)
│
└── 追求极致压缩
└── AVIF(后备 WebP)
2.3 WebP 与 AVIF 的实战使用 #
<!-- 方式 1:<picture> 标签自动选择最优格式 -->
<picture>
<!-- 浏览器按顺序选择第一个支持的 -->
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="后备图片" loading="lazy">
</picture>
<!-- 方式 2:服务端根据 Accept 头自动返回 -->
<!-- 请求时带上 Accept: image/webp,image/avif -->
<!-- 服务端检测后返回对应格式 -->
<img src="/api/image/123" alt="自动适配格式">
// 检测浏览器支持
function checkImageSupport() {
return {
webp: document.createElement('canvas').toDataURL('image/webp').startsWith('data:image/webp'),
avif: document.createElement('canvas').toDataURL('image/avif').startsWith('data:image/avif'),
};
}
// 根据支持情况请求图片
async function getOptimalImage(id) {
const support = checkImageSupport();
const format = support.avif ? 'avif' : support.webp ? 'webp' : 'jpg';
return `/api/image/${id}.${format}`;
}
2.4 图片压缩工具 #
| 工具 |
用途 |
特点 |
| TinyPNG |
在线压缩 PNG/JPEG |
简单易用、压缩率高 |
| Squoosh |
Google 在线工具 |
支持多种格式、可调参数 |
| sharp |
Node.js 图片处理 |
批量处理、自动化 |
| imagemin |
构建工具插件 |
Webpack/Vite 集成 |
// Vite 构建时自动压缩图片
import viteImagemin from 'vite-plugin-imagemin';
export default {
plugins: [
viteImagemin({
gifsicle: { optimizationLevel: 3 },
optipng: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
svgo: { plugins: [{ name: 'removeViewBox' }] },
webp: { quality: 80 },
}),
],
};
三、图片懒加载实现 #
3.1 什么是懒加载? #
懒加载(Lazy Loading) = 图片进入视口时才加载,而不是页面加载时全部加载。
好处:
- 减少首屏加载时间
- 节省带宽(用户可能不滚动到底部)
- 提升用户体验
3.2 原生懒加载(最简单) #
<!-- 浏览器原生支持,只需加 loading="lazy" -->
<img src="image.jpg" loading="lazy" alt="自动懒加载">
<!-- 兼容性:Chrome 77+、Firefox 75+、Safari 15.4+ -->
3.3 IntersectionObserver 实现(可控性更强) #
// HTML: <img class="lazy" data-src="real-image.jpg" src="placeholder.jpg">
class LazyLoader {
constructor(options = {}) {
this.selector = options.selector || '.lazy';
this.rootMargin = options.rootMargin || '50px';
this.threshold = options.threshold || 0;
this.init();
}
init() {
// 检查浏览器是否支持 IntersectionObserver
if ('IntersectionObserver' in window) {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
rootMargin: this.rootMargin,
threshold: this.threshold,
}
);
this.observe();
} else {
// 降级:直接加载所有图片
this.loadAll();
}
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadImage(img) {
const src = img.dataset.src;
if (!src) return;
// 可选:添加淡入效果
img.style.opacity = '0';
img.style.transition = 'opacity 0.3s';
img.onload = () => {
img.style.opacity = '1';
};
img.src = src;
img.classList.remove('lazy');
}
observe() {
const images = document.querySelectorAll(this.selector);
images.forEach(img => this.observer.observe(img));
}
loadAll() {
const images = document.querySelectorAll(this.selector);
images.forEach(img => this.loadImage(img));
}
}
// 使用
new LazyLoader({
selector: '.lazy',
rootMargin: '100px', // 提前 100px 开始加载
});
3.4 Vue3 懒加载组件 #
<!-- LazyImage.vue -->
<template>
<img
ref="imgRef"
:src="placeholder"
:data-src="src"
:alt="alt"
:class="['lazy-image', { loaded: isLoaded }]"
/>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps({
src: { type: String, required: true },
placeholder: { type: String, default: 'data:image/svg+xml,...' }, // 1x1 透明图
alt: { type: String, default: '' },
});
const imgRef = ref(null);
const isLoaded = ref(false);
let observer = null;
onMounted(() => {
if ('IntersectionObserver' in window) {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadImage();
observer.unobserve(entry.target);
}
});
},
{ rootMargin: '50px' }
);
observer.observe(imgRef.value);
} else {
loadImage();
}
});
onUnmounted(() => {
observer?.disconnect();
});
function loadImage() {
if (imgRef.value && imgRef.value.dataset.src) {
imgRef.value.src = imgRef.value.dataset.src;
imgRef.value.onload = () => {
isLoaded.value = true;
};
}
}
</script>
<style scoped>
.lazy-image {
opacity: 0;
transition: opacity 0.3s ease;
}
.lazy-image.loaded {
opacity: 1;
}
</style>
<!-- 使用 -->
<LazyImage
src="/images/photo.jpg"
placeholder="/images/placeholder.jpg"
alt="商品图"
/>
3.5 懒加载最佳实践 #
| 场景 |
建议方案 |
| 简单网站 |
原生 loading="lazy" |
| 需要动画效果 |
IntersectionObserver |
| Vue/React 项目 |
封装懒加载组件 |
| 动态内容 |
MutationObserver 监听新增元素 |
// 动态内容懒加载(SPA 路由切换后)
function setupDynamicLazyLoad() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.classList?.contains('lazy')) {
lazyObserver.observe(node);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
四、Vue3 图片网站优化 #
4.1 图片列表优化策略 #
<!-- ImageList.vue - 图片列表优化示例 -->
<template>
<div class="image-list">
<!-- 虚拟列表:只渲染可见区域 -->
<VirtualList
:items="images"
:item-height="300"
:buffer="5"
>
<template #default="{ item }">
<ImageCard :image="item" />
</template>
</VirtualList>
<!-- 无限滚动加载 -->
<div ref="loaderRef" class="loader">
<span v-if="loading">加载中...</span>
<span v-else-if="!hasMore">没有更多了</span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import VirtualList from './VirtualList.vue';
import ImageCard from './ImageCard.vue';
const images = ref([]);
const loading = ref(false);
const hasMore = ref(true);
const page = ref(1);
const loaderRef = ref(null);
// 无限滚动
onMounted(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !loading.value && hasMore.value) {
loadMore();
}
});
observer.observe(loaderRef.value);
});
async function loadMore() {
loading.value = true;
try {
const newImages = await fetchImages(page.value++);
if (newImages.length === 0) {
hasMore.value = false;
} else {
images.value.push(...newImages);
}
} finally {
loading.value = false;
}
}
</script>
4.2 图片预加载 #
// 预加载下一张图片(提升用户体验)
function preloadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
// Vue3 Composable
import { ref } from 'vue';
export function useImagePreload() {
const preloaded = ref(new Set());
async function preload(urls) {
const promises = urls
.filter(url => !preloaded.value.has(url))
.map(url => preloadImage(url).then(() => preloaded.value.add(url)));
await Promise.all(promises);
}
return { preload, preloaded };
}
// 使用:预加载下一页图片
const { preload } = useImagePreload();
async function loadPage(page) {
const images = await fetchImages(page);
// 预加载下一页
const nextPage = await fetchImages(page + 1);
preload(nextPage.map(img => img.url));
return images;
}
4.3 图片占位符策略 #
<!-- 渐进式图片加载 -->
<template>
<div class="progressive-image">
<!-- 1. 占位符(模糊缩略图) -->
<img
v-if="!isLoaded"
:src="thumbnail"
class="thumbnail"
:style="{ filter: 'blur(20px)' }"
/>
<!-- 2. 高清图 -->
<img
ref="fullImage"
:src="fullSrc"
:class="['full-image', { hidden: !isLoaded }]"
@load="onLoad"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
thumbnail: String, // 20x20 的模糊缩略图
fullSrc: String, // 原图
});
const isLoaded = ref(false);
const fullImage = ref(null);
function onLoad() {
isLoaded.value = true;
}
</script>
<style scoped>
.progressive-image {
position: relative;
overflow: hidden;
}
.thumbnail,
.full-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.full-image.hidden {
opacity: 0;
}
.full-image {
position: absolute;
top: 0;
left: 0;
transition: opacity 0.3s;
}
</style>
4.4 响应式图片 #
<!-- 根据设备加载不同尺寸 -->
<template>
<picture>
<!-- 移动端:小图 -->
<source
media="(max-width: 640px)"
:srcset="`${src}?w=640 1x, ${src}?w=1280 2x`"
/>
<!-- 平板:中图 -->
<source
media="(max-width: 1024px)"
:srcset="`${src}?w=1024 1x, ${src}?w=2048 2x`"
/>
<!-- 桌面:大图 -->
<img
:src="`${src}?w=1920`"
:srcset="`${src}?w=1920 1x, ${src}?w=3840 2x`"
:alt="alt"
loading="lazy"
/>
</picture>
</template>
4.5 图片 CDN 优化 #
// CDN 图片处理函数
function optimizeImage(url, options = {}) {
const {
width,
height,
quality = 80,
format = 'webp',
} = options;
const params = new URLSearchParams();
if (width) params.set('w', width);
if (height) params.set('h', height);
params.set('q', quality);
params.set('f', format);
// 阿里云 OSS 示例
// https://oss.example.com/image.jpg?x-oss-process=image/resize,w_640/quality,q_80/format,webp
return `${url}?x-oss-process=image/resize,w_${width}/quality,q_${quality}/format,${format}`;
// 七牛云示例
// return `${url}?imageView2/1/w/${width}/h/${height}/q/${quality}/format/${format}`;
// 腾讯云 COS 示例
// return `${url}?imageMogr2/thumbnail/${width}x/quality/${quality}/format/${format}`;
}
// 使用
const optimizedUrl = optimizeImage('https://cdn.example.com/photo.jpg', {
width: 640,
quality: 80,
format: 'webp',
});
五、视频格式与编码 #
5.1 视频格式对比 #
| 格式 |
封装 |
常用编码 |
优点 |
缺点 |
浏览器支持 |
| MP4 |
MP4 |
H.264 + AAC |
兼容性最好 |
体积较大 |
✅ 全支持 |
| WebM |
WebM |
VP9/AV1 + Opus |
体积小、开源 |
老 Safari 不支持 |
✅ 95%+ |
| MOV |
QuickTime |
H.264/H.265 |
Apple 生态 |
Windows 兼容差 |
⚠️ 部分 |
| MKV |
Matroska |
任意编码 |
灵活、支持多轨道 |
浏览器不直接支持 |
❌ 需转码 |
5.2 视频编码对比 #
| 编码 |
发布年份 |
压缩效率 |
兼容性 |
适用场景 |
| H.264 (AVC) |
2003 |
基准 |
✅ 全支持 |
通用视频、直播 |
| H.265 (HEVC) |
2013 |
比 H.264 小 50% |
⚠️ Safari/Edge |
4K 视频、高质量 |
| VP9 |
2013 |
接近 H.265 |
✅ Chrome/Firefox |
YouTube、WebM |
| AV1 |
2018 |
比 VP9 小 30% |
⚠️ 较新浏览器 |
追求极致压缩 |
5.3 编码选择建议 #
选择视频编码?
├── 追求最大兼容 → H.264(MP4)
│
├── 追求体积 + 兼容 → H.264 + WebM 双格式
│
├── 追求极致体积 → AV1(后备 VP9)
│
└── 4K/高清视频
├── Safari 用户多 → H.265
└── Chrome 用户多 → VP9 或 AV1
5.4 浏览器编码支持检测 #
// 检测视频编码支持
function checkVideoSupport() {
const video = document.createElement('video');
const codecs = {
'H.264': 'video/mp4; codecs="avc1.42E01E"',
'H.265': 'video/mp4; codecs="hvc1.1.6.L93.90"',
'VP9': 'video/webm; codecs="vp9"',
'AV1': 'video/mp4; codecs="av01.0.01M.08"',
'WebM VP8': 'video/webm; codecs="vp8"',
};
const support = {};
for (const [name, mime] of Object.entries(codecs)) {
support[name] = video.canPlayType(mime) !== '';
}
return support;
}
// 结果示例
// {
// 'H.264': true,
// 'H.265': true, // Safari/Edge
// 'VP9': true, // Chrome/Firefox
// 'AV1': false, // 需要较新浏览器
// }
// 根据支持情况选择视频源
function getOptimalVideo(sources) {
const support = checkVideoSupport();
// 优先级:AV1 > VP9 > H.265 > H.264
if (support.AV1 && sources.av1) return sources.av1;
if (support.VP9 && sources.vp9) return sources.vp9;
if (support['H.265'] && sources.h265) return sources.h265;
return sources.h264; // 后备
}
5.5 视频转码工具 #
| 工具 |
用途 |
特点 |
| FFmpeg |
命令行转码 |
功能强大、自动化 |
| HandBrake |
GUI 转码 |
简单易用 |
| Cloudflare Stream |
云转码 |
自动适配 |
| 阿里云/腾讯云 媒体处理 |
云转码 |
批量处理 |
# FFmpeg 常用命令
# 转换为 H.264 MP4(最大兼容)
ffmpeg -i input.mov -c:v libx264 -crf 23 -c:a aac output.mp4
# 转换为 WebM(VP9)
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -c:a libopus output.webm
# 压缩视频(降低分辨率)
ffmpeg -i input.mp4 -vf scale=1280:-1 -c:v libx264 -crf 28 output.mp4
# 提取视频缩略图
ffmpeg -i input.mp4 -ss 00:00:05 -vframes 1 thumbnail.jpg
# 生成 HLS 切片
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -hls_time 10 -hls_list_size 0 output.m3u8
六、视频播放器选型 #
6.1 播放器对比 #
| 播放器 |
类型 |
优点 |
缺点 |
适用场景 |
原生 <video> |
原生 |
简单、无依赖 |
功能少、样式不统一 |
简单视频播放 |
| Video.js |
开源 |
功能全、插件多 |
体积较大(~200KB) |
通用视频网站 |
| 西瓜播放器 |
开源 |
国产、文档中文、功能强 |
社区较小 |
国内项目、直播 |
| DPlayer |
开源 |
轻量、弹幕支持 |
功能较少 |
简单视频、弹幕 |
| Plyr |
开源 |
轻量、现代UI |
高级功能少 |
简洁播放器 |
| ijkplayer |
开源 |
B站出品、功能强 |
配置复杂 |
直播、点播 |
6.2 原生 video 标签 #
<!-- 最简单的视频播放 -->
<video src="video.mp4" controls></video>
<!-- 完整属性 -->
<video
id="myVideo"
src="video.mp4"
controls <!-- 显示控制条 -->
autoplay <!-- 自动播放(需配合 muted) -->
muted <!-- 静音( autoplay 必需) -->
loop <!-- 循环播放 -->
poster="cover.jpg" <!-- 封面图 -->
preload="metadata" <!-- 预加载:none、metadata、auto -->
playsinline <!-- iOS 内联播放 -->
webkit-playsinline <!-- iOS 兼容 -->
width="640"
height="360"
>
<!-- 多格式后备 -->
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
<p>您的浏览器不支持视频播放</p>
</video>
// JavaScript 控制
const video = document.getElementById('myVideo');
// 播放控制
video.play(); // 播放
video.pause(); // 暂停
video.paused; // 是否暂停
// 进度控制
video.currentTime; // 当前时间(秒)
video.currentTime = 30; // 跳转到 30 秒
video.duration; // 总时长(秒)
// 音量控制
video.volume; // 音量(0-1)
video.muted; // 是否静音
video.muted = true; // 静音
// 播放速率
video.playbackRate; // 播放速度(1 = 正常)
video.playbackRate = 2; // 2 倍速
// 全屏
video.requestFullscreen(); // 进入全屏
document.exitFullscreen(); // 退出全屏
// 常用事件
video.addEventListener('play', () => console.log('开始播放'));
video.addEventListener('pause', () => console.log('暂停'));
video.addEventListener('ended', () => console.log('播放结束'));
video.addEventListener('timeupdate', () => {
console.log('当前时间:', video.currentTime);
});
video.addEventListener('loadedmetadata', () => {
console.log('视频信息加载完成,时长:', video.duration);
});
6.3 Video.js 集成 #
<!-- Vue3 Video.js 组件 -->
<template>
<div ref="videoContainer" class="video-container">
<video ref="videoRef" class="video-js vjs-default-skin">
<source :src="src" :type="type" />
</video>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
const props = defineProps({
src: { type: String, required: true },
type: { type: String, default: 'video/mp4' },
poster: String,
autoplay: { type: Boolean, default: false },
controls: { type: Boolean, default: true },
});
const videoRef = ref(null);
const videoContainer = ref(null);
let player = null;
onMounted(() => {
player = videojs(videoRef.value, {
controls: props.controls,
autoplay: props.autoplay,
poster: props.poster,
fluid: true, // 响应式
playbackRates: [0.5, 1, 1.5, 2], // 倍速选项
controlBar: {
children: [
'playToggle',
'volumePanel',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'progressControl',
'playbackRateMenuButton',
'fullscreenToggle',
],
},
});
player.on('play', () => emit('play'));
player.on('pause', () => emit('pause'));
player.on('ended', () => emit('ended'));
});
onUnmounted(() => {
if (player) {
player.dispose();
}
});
watch(() => props.src, (newSrc) => {
if (player) {
player.src({ src: newSrc, type: props.type });
}
});
const emit = defineEmits(['play', 'pause', 'ended']);
</script>
<style scoped>
.video-container {
width: 100%;
max-width: 800px;
}
.video-js {
width: 100%;
height: 100%;
}
</style>
七、西瓜播放器详解 #
7.1 西瓜播放器简介 #
西瓜播放器(xgplayer) 是字节跳动开源的 HTML5 视频播放器,特点:
- 国产开源,中文文档完善
- 支持 MP4、HLS、FLV 等多种格式
- 支持直播流播放
- 插件化架构,按需加载
- 移动端适配好
7.2 基础使用 #
# 安装
npm install xgplayer
// 基础播放器
import Player from 'xgplayer';
import 'xgplayer/dist/index.min.css';
const player = new Player({
id: 'player-container',
url: 'https://cdn.example.com/video.mp4',
poster: 'https://cdn.example.com/cover.jpg',
autoplay: false,
volume: 0.6,
videoInit: true, // 显示封面
playbackRate: [0.5, 1, 1.5, 2, 3], // 倍速
defaultPlaybackRate: 1,
rotatable: true, // 旋转
screenShot: true, // 截图
pip: true, // 画中画
keyboard: { // 键盘控制
seekStep: 10, // 方向键快进/退 10 秒
keyCode: {
fullscreen: 70, // F 全屏
},
},
});
// 常用方法
player.play(); // 播放
player.pause(); // 暂停
player.start('new.mp4'); // 切换视频源
player.destroy(); // 销毁播放器
// 常用属性
player.currentTime; // 当前时间
player.duration; // 总时长
player.volume; // 音量
player.paused; // 是否暂停
// 常用事件
player.on('play', () => {});
player.on('pause', () => {});
player.on('ended', () => {});
player.on('error', (err) => {});
player.on('timeupdate', () => {});
7.3 Vue3 集成 #
<!-- XgPlayer.vue -->
<template>
<div ref="playerRef" class="xgplayer-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import Player from 'xgplayer';
import 'xgplayer/dist/index.min.css';
const props = defineProps({
url: { type: String, required: true },
poster: String,
autoplay: { type: Boolean, default: false },
controls: { type: Boolean, default: true },
width: { type: String, default: '100%' },
height: { type: String, default: '100%' },
});
const emit = defineEmits(['play', 'pause', 'ended', 'error', 'ready']);
const playerRef = ref(null);
let player = null;
onMounted(() => {
initPlayer();
});
onUnmounted(() => {
destroyPlayer();
});
async function initPlayer() {
await nextTick();
player = new Player({
el: playerRef.value,
url: props.url,
poster: props.poster,
autoplay: props.autoplay,
controls: props.controls,
width: props.width,
height: props.height,
fluid: true,
videoInit: true,
playbackRate: [0.5, 1, 1.5, 2],
});
// 绑定事件
player.on('play', () => emit('play'));
player.on('pause', () => emit('pause'));
player.on('ended', () => emit('ended'));
player.on('error', (err) => emit('error', err));
player.on('ready', () => emit('ready', player));
}
function destroyPlayer() {
if (player) {
player.destroy();
player = null;
}
}
// 监听 URL 变化
watch(() => props.url, (newUrl) => {
if (player && newUrl) {
player.src = newUrl;
player.reload();
}
});
// 暴露方法
defineExpose({
play: () => player?.play(),
pause: () => player?.pause(),
seek: (time) => { if (player) player.currentTime = time; },
destroy: destroyPlayer,
});
</script>
<style scoped>
.xgplayer-container {
width: 100%;
aspect-ratio: 16 / 9;
}
</style>
7.4 HLS 直播支持 #
// 安装 HLS 插件
import Player from 'xgplayer';
import HlsPlugin from 'xgplayer-hls';
import 'xgplayer/dist/index.min.css';
const player = new Player({
id: 'player-container',
url: 'https://cdn.example.com/live.m3u8',
isLive: true, // 直播模式
plugins: [HlsPlugin], // 启用 HLS 插件
hls: {
retryTimes: 3, // 重试次数
retryDelay: 1000, // 重试间隔
},
// 直播特有配置
lang: 'zh-cn',
preloadTime: 30, // 预加载时间
minBufferLen: 3, // 最小缓冲时长
latency: 2, // 延迟时间(秒)
});
7.5 FLV 直播支持 #
// 安装 FLV 插件
import Player from 'xgplayer';
import FlvPlugin from 'xgplayer-flv';
const player = new Player({
id: 'player-container',
url: 'https://cdn.example.com/live.flv',
isLive: true,
plugins: [FlvPlugin],
flv: {
isLive: true,
hasVideo: true,
hasAudio: true,
cors: true,
},
});
7.6 自定义控件 #
// 自定义控件插件
import Player from 'xgplayer';
// 自定义按钮
class CustomButton {
constructor(player) {
this.player = player;
this.root = document.createElement('div');
this.root.className = 'custom-button';
this.root.innerHTML = '自定义';
this.root.onclick = () => this.onClick();
}
onClick() {
console.log('自定义按钮点击');
}
render() {
return this.root;
}
}
const player = new Player({
id: 'player-container',
url: 'video.mp4',
controls: {
mode: 'bottom',
autoHide: true,
},
// 添加自定义控件
// ... 在 CSS 中定义样式
});
八、直播流实现 #
8.1 直播协议对比 #
| 协议 |
延迟 |
兼容性 |
特点 |
适用场景 |
| HLS |
5-30s |
✅ 全支持 |
HTTP 协议、CDN 友好 |
点播、低要求直播 |
| FLV |
1-3s |
⚠️ 需要 Flash/JS |
低延迟、B站首选 |
直播 |
| DASH |
5-30s |
✅ 现代浏览器 |
自适应码率 |
点播 |
| RTMP |
0.5-2s |
❌ 浏览器不支持 |
推流协议 |
推流端 |
| WebRTC |
0.2-1s |
✅ 现代浏览器 |
超低延迟 |
视频会议、连麦 |
| HTTP-FLV |
1-3s |
⚠️ 需要 JS 解析 |
FLV over HTTP |
直播 |
8.2 HLS 直播原理 #
HLS (HTTP Live Streaming) 工作原理:
1. 服务器把视频切成小片段(.ts 文件)
2. 生成播放列表(.m3u8 文件)
3. 播放器下载 m3u8,获取 ts 列表
4. 按顺序下载 ts 片段播放
5. 定期刷新 m3u8,获取新片段
文件结构:
live.m3u8(播放列表)
├── segment0.ts(视频片段 1)
├── segment1.ts(视频片段 2)
├── segment2.ts(视频片段 3)
└── ...
m3u8 内容:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXTINF:10.0,
segment0.ts
#EXTINF:10.0,
segment1.ts
#EXTINF:10.0,
segment2.ts
8.3 HLS 播放实现 #
// 方式 1:原生 HLS(Safari 原生支持)
<video src="live.m3u8" controls></video>
// 方式 2:hls.js(跨浏览器支持)
import Hls from 'hls.js';
const video = document.querySelector('video');
const hlsUrl = 'https://cdn.example.com/live.m3u8';
if (Hls.isSupported()) {
const hls = new Hls({
debug: false,
enableWorker: true,
lowLatencyMode: true,
liveDurationInfinity: true, // 直播模式
liveBackBufferLength: 0, // 不缓存
});
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play();
});
hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.log('网络错误,尝试恢复');
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log('媒体错误,尝试恢复');
hls.recoverMediaError();
break;
default:
console.log('无法恢复,销毁播放器');
hls.destroy();
break;
}
}
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生支持
video.src = hlsUrl;
video.addEventListener('loadedmetadata', () => {
video.play();
});
}
8.4 FLV 直播实现 #
// flv.js 播放 FLV 直播流
import flvjs from 'flv.js';
const video = document.querySelector('video');
const flvUrl = 'https://cdn.example.com/live.flv';
if (flvjs.isSupported()) {
const flvPlayer = flvjs.createPlayer({
type: 'flv',
url: flvUrl,
isLive: true, // 直播模式
hasAudio: true,
hasVideo: true,
cors: true,
}, {
enableWorker: true,
enableStashBuffer: false, // 直播建议关闭
stashInitialSize: 128,
lazyLoad: false, // 直播关闭懒加载
});
flvPlayer.attachMediaElement(video);
flvPlayer.load();
flvPlayer.play();
// 错误处理
flvPlayer.on(flvjs.Events.ERROR, (errType, errDetail) => {
console.error('FLV 错误:', errType, errDetail);
if (errType === flvjs.Events.NETWORK_ERROR) {
// 网络错误,重新加载
flvPlayer.unload();
flvPlayer.load();
flvPlayer.play();
}
});
// 销毁
// flvPlayer.destroy();
}
8.5 WebRTC 超低延迟直播 #
// WebRTC 客户端实现
class WebRTCPlayer {
constructor(videoElement) {
this.video = videoElement;
this.pc = null;
}
async connect(signalUrl) {
// 创建 RTCPeerConnection
this.pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
// 接收远程流
this.pc.ontrack = (event) => {
this.video.srcObject = event.streams[0];
};
// 创建并发送 offer
const offer = await this.pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
await this.pc.setLocalDescription(offer);
// 发送 offer 到信令服务器
const response = await fetch(signalUrl, {
method: 'POST',
body: JSON.stringify({ sdp: offer.sdp }),
headers: { 'Content-Type': 'application/json' },
});
const answer = await response.json();
// 设置远程 answer
await this.pc.setRemoteDescription(
new RTCSessionDescription({ type: 'answer', sdp: answer.sdp })
);
}
disconnect() {
if (this.pc) {
this.pc.close();
this.pc = null;
}
}
}
// 使用
const player = new WebRTCPlayer(document.querySelector('video'));
await player.connect('https://signal.example.com/offer');
8.6 直播延迟优化 #
| 延迟级别 |
协议 |
延迟时间 |
优化手段 |
| 高延迟 |
HLS |
10-30s |
CDN 优化、减少分片时长 |
| 中延迟 |
HLS LL |
2-5s |
低延迟 HLS |
| 低延迟 |
FLV |
1-3s |
减小缓冲 |
| 超低延迟 |
WebRTC |
0.2-1s |
UDP 传输 |
// HLS 低延迟配置
const hls = new Hls({
lowLatencyMode: true,
liveBackBufferLength: 0,
liveDurationInfinity: true,
maxBufferLength: 5, // 最大缓冲 5 秒
maxMaxBufferLength: 10,
maxBufferSize: 10 * 1000 * 1000, // 10MB
maxBufferHole: 0.5,
});
// FLV 低延迟配置
const flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive: true,
}, {
enableStashBuffer: false,
stashInitialSize: 128,
lazyLoad: false,
seekType: 'range',
});
九、视频网站性能优化 #
9.1 视频预加载策略 #
// 预加载视频元数据
function preloadVideoMetadata(url) {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = () => {
const info = {
duration: video.duration,
width: video.videoWidth,
height: video.videoHeight,
};
video.src = ''; // 清除,释放资源
resolve(info);
};
video.onerror = reject;
video.src = url;
});
}
// 预加载视频首帧
function preloadFirstFrame(url) {
return new Promise((resolve) => {
const video = document.createElement('video');
video.preload = 'auto';
video.muted = true;
video.onloadeddata = () => {
video.pause();
resolve(video);
};
video.src = url;
video.play();
});
}
9.2 自适应码率(ABR) #
// 根据网速自动切换清晰度
class AdaptiveBitrate {
constructor(player, sources) {
this.player = player;
this.sources = sources; // { '1080p': url, '720p': url, '480p': url }
this.currentQuality = '720p';
this.lastSpeedCheck = Date.now();
this.bytesLoaded = 0;
}
// 监听下载速度
onProgress(loaded) {
const now = Date.now();
const elapsed = (now - this.lastSpeedCheck) / 1000;
if (elapsed >= 5) { // 每 5 秒检测一次
const speed = (loaded - this.bytesLoaded) / elapsed; // bytes/s
this.bytesLoaded = loaded;
this.lastSpeedCheck = now;
this.adjustQuality(speed);
}
}
// 调整清晰度
adjustQuality(speed) {
// 根据速度选择清晰度
// 1080p 需要 > 5MB/s
// 720p 需要 > 2MB/s
// 480p 需要 > 1MB/s
const mbps = speed / 1024 / 1024;
let newQuality = '480p';
if (mbps > 5) newQuality = '1080p';
else if (mbps > 2) newQuality = '720p';
if (newQuality !== this.currentQuality) {
this.switchQuality(newQuality);
}
}
// 切换清晰度(无缝切换)
switchQuality(quality) {
const currentTime = this.player.currentTime;
const paused = this.player.paused;
this.player.src = this.sources[quality];
this.player.currentTime = currentTime;
if (!paused) {
this.player.play();
}
this.currentQuality = quality;
console.log(`切换到 ${quality}`);
}
}
9.3 视频缩略图预览 #
// 生成视频缩略图精灵图
async function generateThumbnails(videoUrl, count = 10) {
const video = document.createElement('video');
video.src = videoUrl;
video.muted = true;
await new Promise(resolve => {
video.onloadedmetadata = resolve;
});
const duration = video.duration;
const interval = duration / count;
const thumbnails = [];
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 160;
canvas.height = 90;
for (let i = 0; i < count; i++) {
video.currentTime = i * interval;
await new Promise(resolve => {
video.onseeked = resolve;
});
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
thumbnails.push({
time: video.currentTime,
url: canvas.toDataURL('image/jpeg', 0.7),
});
}
return thumbnails;
}
// 生成精灵图(所有缩略图合成一张图)
async function generateSprite(videoUrl, options = {}) {
const {
cols = 5,
rows = 5,
width = 160,
height = 90,
} = options;
const video = document.createElement('video');
video.src = videoUrl;
video.muted = true;
await new Promise(resolve => {
video.onloadedmetadata = resolve;
});
const duration = video.duration;
const total = cols * rows;
const interval = duration / total;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = width * cols;
canvas.height = height * rows;
for (let i = 0; i < total; i++) {
video.currentTime = i * interval;
await new Promise(resolve => {
video.onseeked = resolve;
});
const x = (i % cols) * width;
const y = Math.floor(i / cols) * height;
ctx.drawImage(video, x, y, width, height);
}
return canvas.toDataURL('image/jpeg', 0.8);
}
9.4 视频缓存策略 #
// 使用 Service Worker 缓存视频
// sw.js
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// 视频请求使用 Range 缓存
if (url.pathname.endsWith('.mp4') || url.pathname.endsWith('.webm')) {
event.respondWith(
caches.open('video-cache').then((cache) => {
return cache.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request).then((response) => {
// 只缓存完整响应(非 Range 请求)
if (!event.request.headers.has('Range')) {
cache.put(event.request, response.clone());
}
return response;
});
});
})
);
}
});
// 使用 IndexedDB 缓存视频片段
async function cacheVideoSegment(url, blob) {
const db = await openDB('video-cache', 1, {
upgrade(db) {
db.createObjectStore('segments');
},
});
await db.put('segments', blob, url);
}
async function getCachedSegment(url) {
const db = await openDB('video-cache', 1);
return await db.get('segments', url);
}
9.5 内存优化 #
// 视频播放器内存管理
class VideoPlayerManager {
constructor() {
this.players = new Map();
this.maxPlayers = 3; // 最大同时播放数
}
createPlayer(id, container, options) {
// 如果超过限制,销毁最旧的
if (this.players.size >= this.maxPlayers) {
const [oldestId] = this.players.keys();
this.destroyPlayer(oldestId);
}
const player = new Player({
el: container,
...options,
});
this.players.set(id, player);
return player;
}
destroyPlayer(id) {
const player = this.players.get(id);
if (player) {
player.destroy();
this.players.delete(id);
}
}
// 页面隐藏时暂停所有播放器
pauseAll() {
this.players.forEach(player => {
if (!player.paused) {
player.pause();
}
});
}
}
// 页面可见性监听
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
playerManager.pauseAll();
}
});
十、实战场景 #
场景 1:图片网站完整实现 #
<!-- ImageGallery.vue - 图片画廊 -->
<template>
<div class="image-gallery">
<!-- 瀑布流布局 -->
<div class="masonry" ref="masonryRef">
<div
v-for="(image, index) in images"
:key="image.id"
class="masonry-item"
:style="{ height: `${image.displayHeight}px` }"
>
<LazyImage
:src="image.url"
:thumbnail="image.thumbnail"
:alt="image.title"
@click="openLightbox(index)"
/>
</div>
</div>
<!-- 加载更多 -->
<div ref="loaderRef" class="loader">
<span v-if="loading">加载中...</span>
</div>
<!-- 图片预览弹窗 -->
<ImageLightbox
v-if="lightboxIndex !== null"
:images="images"
:index="lightboxIndex"
@close="lightboxIndex = null"
@prev="lightboxIndex--"
@next="lightboxIndex++"
/>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import LazyImage from './LazyImage.vue';
import ImageLightbox from './ImageLightbox.vue';
const images = ref([]);
const loading = ref(false);
const page = ref(1);
const loaderRef = ref(null);
const masonryRef = ref(null);
const lightboxIndex = ref(null);
// 瀑布流布局
const columnWidth = 300;
const gap = 16;
function layoutMasonry() {
const container = masonryRef.value;
if (!container) return;
const containerWidth = container.offsetWidth;
const columns = Math.floor(containerWidth / (columnWidth + gap));
const heights = new Array(columns).fill(0);
const items = container.querySelectorAll('.masonry-item');
items.forEach((item, index) => {
const col = heights.indexOf(Math.min(...heights));
const x = col * (columnWidth + gap);
const y = heights[col];
item.style.transform = `translate(${x}px, ${y}px)`;
item.style.width = `${columnWidth}px`;
const image = images.value[index];
if (image) {
const aspectRatio = image.height / image.width;
image.displayHeight = columnWidth * aspectRatio;
heights[col] += image.displayHeight + gap;
}
});
container.style.height = `${Math.max(...heights)}px`;
}
// 无限滚动
onMounted(() => {
loadImages();
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !loading.value) {
loadImages();
}
});
observer.observe(loaderRef.value);
window.addEventListener('resize', layoutMasonry);
});
async function loadImages() {
loading.value = true;
try {
const newImages = await fetch(`/api/images?page=${page.value++}`);
images.value.push(...newImages);
// 等待 DOM 更新后布局
setTimeout(layoutMasonry, 0);
} finally {
loading.value = false;
}
}
function openLightbox(index) {
lightboxIndex.value = index;
}
</script>
<style scoped>
.masonry {
position: relative;
}
.masonry-item {
position: absolute;
transition: transform 0.3s;
}
.loader {
text-align: center;
padding: 20px;
}
</style>
场景 2:视频点播网站 #
<!-- VideoPlayer.vue -->
<template>
<div class="video-page">
<!-- 播放器 -->
<div class="player-container">
<XgPlayer
ref="playerRef"
:url="currentSource.url"
:poster="video.poster"
@ready="onPlayerReady"
@ended="onVideoEnded"
/>
<!-- 清晰度选择 -->
<div class="quality-selector">
<button
v-for="(source, quality) in sources"
:key="quality"
:class="{ active: currentQuality === quality }"
@click="changeQuality(quality)"
>
{{ quality }}
</button>
</div>
</div>
<!-- 视频信息 -->
<div class="video-info">
<h1>{{ video.title }}</h1>
<p>{{ video.description }}</p>
</div>
<!-- 推荐列表 -->
<div class="recommend-list">
<VideoCard
v-for="item in recommendations"
:key="item.id"
:video="item"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import XgPlayer from './XgPlayer.vue';
import VideoCard from './VideoCard.vue';
const props = defineProps({
videoId: String,
});
const video = ref({});
const sources = ref({});
const currentQuality = ref('720p');
const playerRef = ref(null);
const currentSource = computed(() => {
return sources.value[currentQuality.value] || {};
});
onMounted(async () => {
// 加载视频信息
const data = await fetch(`/api/videos/${props.videoId}`);
video.value = data.video;
sources.value = data.sources;
// 加载推荐
loadRecommendations();
});
function changeQuality(quality) {
const currentTime = playerRef.value?.currentTime || 0;
currentQuality.value = quality;
// 恢复播放进度
setTimeout(() => {
playerRef.value?.seek(currentTime);
}, 100);
}
function onPlayerReady(player) {
// 预加载下一集
preloadNextEpisode();
}
function onVideoEnded() {
// 自动播放下一集
playNextEpisode();
}
async function preloadNextEpisode() {
const nextId = video.value.nextEpisodeId;
if (nextId) {
// 预加载视频元数据
const video = document.createElement('video');
video.preload = 'metadata';
video.src = `/api/videos/${nextId}/preview`;
}
}
</script>
场景 3:直播平台 #
<!-- LiveStream.vue -->
<template>
<div class="live-page">
<!-- 直播播放器 -->
<div class="live-player">
<XgPlayer
ref="playerRef"
:url="streamUrl"
:is-live="true"
@error="onStreamError"
/>
<!-- 直播状态 -->
<div class="live-status">
<span class="live-badge">LIVE</span>
<span class="viewer-count">{{ viewerCount }} 观看</span>
</div>
<!-- 延迟显示 -->
<div class="latency-info">
延迟: {{ latency }}s
</div>
</div>
<!-- 聊天室 -->
<div class="chat-room">
<div class="messages" ref="messagesRef">
<div
v-for="msg in messages"
:key="msg.id"
class="message"
>
<span class="username">{{ msg.username }}:</span>
<span class="content">{{ msg.content }}</span>
</div>
</div>
<input
v-model="newMessage"
@keyup.enter="sendMessage"
placeholder="发送弹幕..."
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import XgPlayer from './XgPlayer.vue';
const props = defineProps({
roomId: String,
});
const playerRef = ref(null);
const streamUrl = ref('');
const viewerCount = ref(0);
const latency = ref(0);
const messages = ref([]);
const newMessage = ref('');
const messagesRef = ref(null);
let ws = null;
onMounted(async () => {
// 获取直播流地址
const { url, viewers } = await fetch(`/api/live/${props.roomId}`);
streamUrl.value = url;
viewerCount.value = viewers;
// 连接聊天 WebSocket
connectChat();
// 监控延迟
monitorLatency();
});
onUnmounted(() => {
ws?.close();
});
function connectChat() {
ws = new WebSocket(`wss://chat.example.com/room/${props.roomId}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
messages.value.push(data.message);
// 滚动到底部
setTimeout(() => {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight;
}, 0);
} else if (data.type === 'viewers') {
viewerCount.value = data.count;
}
};
}
function sendMessage() {
if (!newMessage.value.trim()) return;
ws.send(JSON.stringify({
type: 'message',
content: newMessage.value,
}));
newMessage.value = '';
}
function monitorLatency() {
setInterval(() => {
if (playerRef.value) {
// 计算延迟(直播时间 - 当前播放时间)
const liveTime = Date.now() / 1000;
const playTime = playerRef.value.currentTime;
latency.value = Math.round(liveTime - playTime);
}
}, 1000);
}
function onStreamError(err) {
console.error('直播流错误:', err);
// 自动重连
setTimeout(() => {
playerRef.value?.reload();
}, 3000);
}
</script>
十一、常见问题 #
Q1:为什么视频自动播放失败? #
原因: 浏览器策略限制,自动播放必须静音。
<!-- ❌ 不会自动播放 -->
<video src="video.mp4" autoplay></video>
<!-- ✅ 静音后可以自动播放 -->
<video src="video.mp4" autoplay muted></video>
解决方案:
// 尝试播放,失败后提示用户
async function tryAutoplay(video) {
try {
await video.play();
} catch (err) {
// 自动播放失败,显示播放按钮
showPlayButton();
}
}
Q2:视频在 iOS 上全屏播放怎么办? #
解决方案: 添加 playsinline 属性。
<video
src="video.mp4"
playsinline
webkit-playsinline
></video>
Q3:如何实现视频无缝循环? #
// 方式 1:loop 属性(有短暂停顿)
<video src="video.mp4" loop></video>
// 方式 2:双视频无缝循环
const video1 = document.querySelector('#video1');
const video2 = document.querySelector('#video2');
video1.onended = () => {
video2.play();
video1.style.display = 'none';
video2.style.display = 'block';
};
video2.onended = () => {
video1.play();
video2.style.display = 'none';
video1.style.display = 'block';
};
Q4:如何获取视频的第一帧作为封面? #
async function getVideoPoster(videoUrl) {
const video = document.createElement('video');
video.src = videoUrl;
video.muted = true;
await new Promise(resolve => {
video.onloadeddata = resolve;
});
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0);
return canvas.toDataURL('image/jpeg');
}
// 使用
const poster = await getVideoPoster('video.mp4');
Q5:HLS 直播卡顿怎么办? #
常见原因和解决方案:
| 原因 |
解决方案 |
| 网络问题 |
降低清晰度、增加缓冲 |
| CDN 问题 |
切换 CDN 节点 |
| 分片过大 |
减小分片时长(2-5 秒) |
| 解码性能差 |
使用硬件加速 |
// 优化 HLS 配置
const hls = new Hls({
maxBufferLength: 30, // 增加缓冲
maxMaxBufferLength: 60,
maxBufferSize: 60 * 1000 * 1000,
startLevel: -1, // 自动选择清晰度
capLevelToPlayerSize: true, // 根据播放器大小选择
});
Q6:如何在移动端优化视频体验? #
<!-- 移动端优化配置 -->
<video
src="video.mp4"
playsinline <!-- iOS 内联播放 -->
webkit-playsinline <!-- iOS 兼容 -->
x5-video-player-type="h5" <!-- 微信 X5 内核 -->
x5-video-player-fullscreen="true"
preload="metadata" <!-- 只预加载元数据 -->
poster="cover.jpg" <!-- 封面图 -->
></video>
/* 移动端视频样式 */
video {
width: 100%;
height: auto;
object-fit: contain;
background: #000;
}
/* 避免视频被挤压 */
.video-container {
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 */
}
.video-container video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
Q7:图片加载失败如何处理? #
<!-- Vue3 图片错误处理 -->
<template>
<img
:src="imageSrc"
@error="onImageError"
:alt="alt"
/>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
src: String,
alt: String,
fallback: { type: String, default: '/images/placeholder.jpg' },
});
const imageSrc = ref(props.src);
function onImageError() {
imageSrc.value = props.fallback;
}
</script>
十二、总结速记 #
图片格式选择 #
| 需求 |
推荐格式 |
| 照片 |
WebP > JPEG |
| 透明图 |
WebP > PNG |
| 图标/Logo |
SVG |
| 动画 |
WebP 动画 > GIF(短视频用 MP4) |
| 极致压缩 |
AVIF |
视频编码选择 #
| 场景 |
推荐编码 |
| 最大兼容 |
H.264 (MP4) |
| 追求体积 |
H.265 或 VP9 |
| 极致压缩 |
AV1 |
| 直播 |
H.264 |
直播协议选择 #
| 延迟要求 |
推荐协议 |
| 10-30s 可接受 |
HLS |
| 1-5s |
FLV / HTTP-FLV |
| < 1s |
WebRTC |
懒加载方式 #
| 方式 |
兼容性 |
推荐场景 |
loading="lazy" |
现代浏览器 |
简单网站 |
| IntersectionObserver |
全浏览器 |
Vue/React 组件 |
播放器选型 #
| 播放器 |
适用场景 |
原生 <video> |
简单播放 |
| Video.js |
通用视频网站 |
| 西瓜播放器 |
国内项目、直播 |
| DPlayer |
弹幕视频 |
CDN 图片处理 #
// 通用格式
`${url}?w=${width}&q=${quality}&f=${format}`;
// 阿里云 OSS
`${url}?x-oss-process=image/resize,w_${width}/quality,q_${quality}/format,${format}`;
视频性能优化 #
| 策略 |
方法 |
| 预加载 |
preload="metadata" |
| 懒加载 |
进入视口再初始化播放器 |
| 自适应码率 |
根据网速切换清晰度 |
| 内存管理 |
页面隐藏时暂停、限制播放器数量 |
| 缓存 |
Service Worker / IndexedDB |
附录:工具推荐 #
图片工具 #
视频工具 #
测试工具 #
| 工具 |
用途 |
| caniuse.com |
浏览器兼容性查询 |
| Cloudflare Stream |
视频托管 + 自动转码 |
| Mux |
视频分析 + 优化建议 |
最后更新:2026-03-29