鼠标悬停进度条就能预览画面?聊聊视频缩略图与雪碧图技术
在 B 站或 YouTube 看视频时,鼠标悬停在进度条上,会弹出一个小预览图,让你快速定位想看的片段。这个功能背后是一张雪碧图(Sprite)——把几十上百帧画面拼成一张大图,前端按时间映射到对应的位置裁切显示。本文拆开视频缩略图生成的完整技术链路。
TL;DR:视频缩略图预览 = ffmpeg 定时截图 + ImageMagick 拼雪碧图 + 前端 CSS background-position 裁切。也可以用 VTT 缩略图轨道的标准方案。本质是在"下载整个视频"和"完全不知道内容"之间,给用户一个低成本预览的折中方案。
目录
一、为什么要用雪碧图
直观想法是"生成 100 张缩略图,前端加载 100 张小图"。但 HTTP 请求很贵——100 张图 = 100 次请求,页面会卡成 PPT。
雪碧图的思路:把 100 张小图拼成 1 张大图,1 次请求全部搞定。前端用 CSS 的 background-position 只显示对应格子。
雪碧图结构(5列 × 20行 = 100帧):
┌───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ ← 第 0 秒 到 第 4 秒
├───┼───┼───┼───┼───┤
│ 5 │ 6 │ 7 │ 8 │ 9 │ ← 第 5 秒 到 第 9 秒
├───┼───┼───┼───┼───┤
│... │...│...│...│...│
└───┴───┴───┴───┴───┘
一张大图解决所有预览,这就是雪碧图的价值。
二、用 ffmpeg 定时截图
第一步:从视频中按固定间隔抽帧。
# 每 10 秒截一张,缩放到 160x90
ffmpeg -i video.mp4 \
-vf "fps=1/10,scale=160:90" \
thumb_%04d.png
参数解释:
- fps=1/10:每 10 秒 1 帧(即 0.1 fps)
- scale=160:90:缩略图分辨率(16:9 比例)
如果你的视频是 30 分钟(1800 秒),fps=1/10 会生成 180 张缩略图。如果想更密集(比如每 5 秒一张),改成 fps=1/5。
# 更精确的方式:指定总帧数
# 生成 100 张均匀分布的缩略图
ffmpeg -i video.mp4 -vf "fps=1/$(echo '1800/100' | bc)" thumb_%04d.png
三、拼成雪碧图
第二步:把散图拼成一张大图。ImageMagick 的 montage 命令是最简单的方案:
# 把当前目录所有 thumb_*.png 拼成雪碧图
# 每行 10 张,自动换行
montage thumb_*.png -tile 10x -geometry 160x90+0+0 sprite.jpg
参数解释:
- -tile 10x:每行 10 张
- -geometry 160x90+0+0:每个格子 160×90,无间距
- 输出 sprite.jpg(用 JPG 格式体积更小)
如果你需要更精细的控制,可以用 Python + Pillow:
from PIL import Image
import glob
thumb_files = sorted(glob.glob("thumb_*.png"))
cols = 10
rows = (len(thumb_files) + cols - 1) // cols
sprite = Image.new('RGB', (160 * cols, 90 * rows))
for i, f in enumerate(thumb_files):
img = Image.open(f)
x = (i % cols) * 160
y = (i // cols) * 90
sprite.paste(img, (x, y))
sprite.save('sprite.jpg', quality=85)
四、前端如何"裁切"显示
拿到雪碧图后,前端根据鼠标在进度条上的位置,算出对应的格子坐标:
<div id="thumbnail" style="
width: 160px; height: 90px;
background-image: url('sprite.jpg');
background-position: -320px -180px; /* 第3列第2行 */
background-size: 1600px 1800px; /* 总尺寸 = 160*10 × 90*20 */
"></div>
JavaScript 动态计算 background-position:
const COLS = 10;
const THUMB_W = 160, THUMB_H = 90;
progressBar.addEventListener('mousemove', (e) => {
const percent = e.offsetX / progressBar.offsetWidth;
const frameIndex = Math.floor(percent * totalFrames);
const col = frameIndex % COLS;
const row = Math.floor(frameIndex / COLS);
thumbnail.style.backgroundPosition = `-${col * THUMB_W}px -${row * THUMB_H}px`;
thumbnail.style.display = 'block';
});
五、VTT 缩略图标准方案
除了自造雪碧图,还有一种更标准的做法——WebVTT 缩略图轨道:
WEBVTT
00:00:00.000 --> 00:00:10.000
sprite.jpg#xywh=0,0,160,90
00:00:10.000 --> 00:00:20.000
sprite.jpg#xywh=160,0,160,90
00:00:20.000 --> 00:00:30.000
sprite.jpg#xywh=320,0,160,90
每行指定时间段 + 雪碧图上的裁切坐标(xywh = x, y, width, height)。前端解析 VTT 文件后,直接取对应坐标显示。这种方式的好处是:
- 标准化,播放器可直接支持
- 可以配合多张雪碧图(超长视频分多张)
- 语义清晰,维护方便
六、性能优化建议
| 优化项 | 做法 |
|---|---|
| 缩略图分辨率 | 160×90 就够了,别用 320×180 |
| 图片格式 | JPG quality=85,体积比 PNG 小 5-10 倍 |
| 雪碧图分片 | 长视频拆成多张雪碧图,每张不超过 2MB |
| 懒加载 | 鼠标悬停进度条时才加载雪碧图 |
| CDN 缓存 | 雪碧图不常变,设长缓存时间 |
# ffmpeg 直接输出 JPG(省去 PNG→JPG 转换)
ffmpeg -i video.mp4 -vf "fps=1/10,scale=160:90" -q:v 3 thumb_%04d.jpg
七、合规与温馨提示
技术是中性的,用法见人心。我们强烈建议:
- 仅对你自己拥有版权或平台明确允许的视频生成缩略图;
- 不将缩略图用于误导性用途(如封面党);
- 尊重创作者与平台服务条款。
视频缩略图雪碧图是一个"小功能、大体验"的典型。它让用户在不下载整个视频的情况下快速预览内容——本质是在带宽成本和用户体验之间找一个优雅的平衡点。如果你在自己的视频站上实现了这个功能,用户拖进度条的体验会直线上升。
本文由 VidDown 技术博客原创发布。VidDown 是一个免费、本地优先的在线视频解析与开发者工具站,支持多平台视频下载、格式转换、m3u8 合并等实用功能,所有数据处理均在本地完成,保护你的隐私。欢迎访问 www.viddown.cn 体验。