📖 开场:电影院的进化
想象电影院的进化史 🎥:
1.0版(传统电影院):
看电影:
↓
去电影院(30分钟)🚗
排队买票(10分钟)🎫
等待开场(20分钟)⏰
看完回家(30分钟)🏠
↓
总耗时:2小时 ❌
缺点:
- 麻烦 ❌
- 时间成本高 ❌
- 不能快进 ❌
2.0版(视频网站):
看视频:
↓
打开B站 📱
搜索视频 🔍
点击播放 ▶️
↓
总耗时:10秒 ✅
优点:
- 方便 ✅
- 随时随地 ✅
- 可以快进/倍速 ✅
- 弹幕互动 💬
这就是视频网站:革新了观影体验!
🤔 核心挑战
挑战1:视频存储 💾
问题:
一部1080P电影:5GB
一部4K电影:20GB
↓
B站有百万级视频
↓
需要PB级存储 💀
解决:
- 对象存储(OSS)✅
- 视频压缩(H.264/H.265)✅
- 多分辨率(360P/720P/1080P)✅
挑战2:视频播放 ▶️
问题:
用户点击播放:
↓
下载5GB视频?💀
↓
等待10分钟?❌
解决:
- 视频切片(HLS/DASH)✅
- 边下边播 ✅
- CDN加速 ✅
挑战3:视频转码 🔄
问题:
用户上传的视频格式千奇百怪:
- MP4
- AVI
- MOV
- FLV
- ...
如何统一播放?
解决:
- 视频转码 ✅
- 多分辨率转码(360P/720P/1080P)✅
- 异步处理(消息队列)✅
🎯 核心设计
设计1:系统架构 🏗️
视频网站系统架构
┌────────────────────────────────────┐
│ 客户端 │
│ - Web/App/小程序 │
│ - 视频播放器 │
└──────────────┬─────────────────────┘
│
↓
┌────────────────────────────────────┐
│ CDN(内容分发) │
│ - 视频加速 │
│ - 就近访问 │
└──────────────┬─────────────────────┘
│
↓
┌────────────────────────────────────┐
│ 应用服务器 │
│ - 视频上传 │
│ - 视频管理 │
│ - 播放记录 │
└──────────────┬─────────────────────┘
│
┌───────┼───────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ OSS │ │ 转码 │ │ 数据库 │
│ 对象存储 │ │ 服务 │ │ MySQL │
└──────────┘ └──────────┘ └──────────┘
设计2:视频上传 📤
上传流程
视频上传流程:
1. 客户端:选择视频文件
↓
2. 获取上传凭证(服务器)
↓
3. 分片上传到OSS
↓
4. 上传完成,通知服务器
↓
5. 触发视频转码任务
↓
6. 转码完成,视频可播放 ✅
代码实现
@RestController
@RequestMapping("/video")
public class VideoUploadController {
@Autowired
private VideoService videoService;
@Autowired
private OSSService ossService;
/**
* ⭐ 获取上传凭证
*/
@GetMapping("/upload/credential")
public Result<UploadCredential> getUploadCredential(@RequestParam Long userId) {
// 生成唯一的视频ID
String videoId = UUID.randomUUID().toString();
// 生成OSS上传凭证(有效期1小时)
UploadCredential credential = ossService.generateUploadCredential(
"video/original/" + videoId,
3600
);
credential.setVideoId(videoId);
return Result.success(credential);
}
/**
* ⭐ 上传完成通知
*/
@PostMapping("/upload/complete")
public Result<Void> uploadComplete(@RequestBody VideoUploadRequest request) {
// 1. 创建视频记录
Video video = new Video();
video.setId(request.getVideoId());
video.setUserId(request.getUserId());
video.setTitle(request.getTitle());
video.setDescription(request.getDescription());
video.setOriginalUrl(request.getOssUrl());
video.setStatus(VideoStatus.TRANSCODING); // 转码中
video.setCreateTime(new Date());
videoService.save(video);
// ⭐ 2. 触发转码任务
TranscodeTask task = new TranscodeTask();
task.setVideoId(request.getVideoId());
task.setOriginalUrl(request.getOssUrl());
transcodeService.submitTask(task);
return Result.success();
}
}
设计3:视频转码 🔄
转码流程
转码流程:
1. 从OSS下载原视频
↓
2. FFmpeg转码:
- 360P(流畅)
- 720P(高清)
- 1080P(超清)
↓
3. 切片(HLS):
- 每10秒一个分片(.ts文件)
- 生成索引文件(.m3u8)
↓
4. 上传到OSS
↓
5. 更新视频状态:可播放 ✅
代码实现
@Service
public class TranscodeService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private VideoMapper videoMapper;
/**
* ⭐ 提交转码任务
*/
public void submitTask(TranscodeTask task) {
// 发送到MQ,异步处理
rocketMQTemplate.syncSend("transcode-topic", task);
}
}
/**
* ⭐ 转码消费者
*/
@Component
@RocketMQMessageListener(
topic = "transcode-topic",
consumerGroup = "transcode-consumer"
)
public class TranscodeConsumer implements RocketMQListener<TranscodeTask> {
@Autowired
private VideoMapper videoMapper;
@Autowired
private OSSService ossService;
@Override
public void onMessage(TranscodeTask task) {
String videoId = task.getVideoId();
String originalUrl = task.getOriginalUrl();
try {
// 1. 从OSS下载原视频到本地
String localPath = "/tmp/" + videoId + ".mp4";
ossService.download(originalUrl, localPath);
// ⭐ 2. 转码多个分辨率
transcode360P(videoId, localPath);
transcode720P(videoId, localPath);
transcode1080P(videoId, localPath);
// 3. 删除本地临时文件
new File(localPath).delete();
// ⭐ 4. 更新视频状态
Video video = videoMapper.selectById(videoId);
video.setStatus(VideoStatus.PUBLISHED);
videoMapper.updateById(video);
System.out.println("⭐ 转码完成:" + videoId);
} catch (Exception e) {
e.printStackTrace();
// 转码失败
Video video = videoMapper.selectById(videoId);
video.setStatus(VideoStatus.TRANSCODE_FAILED);
videoMapper.updateById(video);
}
}
/**
* ⭐ 转码360P
*/
private void transcode360P(String videoId, String inputPath) throws Exception {
String outputDir = "/tmp/" + videoId + "/360p/";
new File(outputDir).mkdirs();
// FFmpeg命令
String command = String.format(
"ffmpeg -i %s " +
"-c:v libx264 " + // 视频编码器
"-s 640x360 " + // 分辨率
"-b:v 800k " + // 视频比特率
"-c:a aac " + // 音频编码器
"-b:a 128k " + // 音频比特率
"-hls_time 10 " + // 每个分片10秒
"-hls_list_size 0 " + // m3u8包含所有分片
"-f hls " + // 输出格式HLS
"%s/index.m3u8",
inputPath,
outputDir
);
// 执行FFmpeg命令
Process process = Runtime.getRuntime().exec(command);
process.waitFor();
// 上传到OSS
uploadToOSS(videoId, outputDir, "360p");
// 删除本地文件
FileUtils.deleteDirectory(new File(outputDir));
}
/**
* 转码720P
*/
private void transcode720P(String videoId, String inputPath) throws Exception {
// 类似360P,分辨率改为1280x720,比特率改为2000k
// ...
}
/**
* 转码1080P
*/
private void transcode1080P(String videoId, String inputPath) throws Exception {
// 类似360P,分辨率改为1920x1080,比特率改为4000k
// ...
}
/**
* 上传到OSS
*/
private void uploadToOSS(String videoId, String dir, String quality) {
File folder = new File(dir);
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
String ossKey = "video/" + videoId + "/" + quality + "/" + file.getName();
ossService.upload(file.getAbsolutePath(), ossKey);
}
}
}
}
设计4:视频播放 ▶️
播放流程
播放流程:
1. 客户端:请求视频播放地址
↓
2. 服务器:返回m3u8地址(CDN)
↓
3. 播放器:解析m3u8,获取分片列表
↓
4. 播放器:依次下载分片(.ts文件)
↓
5. 播放器:边下边播 ✅
代码实现
@RestController
@RequestMapping("/video")
public class VideoPlayController {
@Autowired
private VideoService videoService;
@Autowired
private CDNService cdnService;
/**
* ⭐ 获取视频播放地址
*/
@GetMapping("/{videoId}/play")
public Result<VideoPlayInfo> getPlayInfo(
@PathVariable String videoId,
@RequestParam(defaultValue = "720p") String quality) {
// 1. 查询视频信息
Video video = videoService.getById(videoId);
if (video == null) {
return Result.fail("视频不存在");
}
if (video.getStatus() != VideoStatus.PUBLISHED) {
return Result.fail("视频未就绪");
}
// ⭐ 2. 生成m3u8播放地址(CDN加速)
String m3u8Url = cdnService.getVideoUrl(
"video/" + videoId + "/" + quality + "/index.m3u8"
);
// 3. 构造播放信息
VideoPlayInfo playInfo = new VideoPlayInfo();
playInfo.setVideoId(videoId);
playInfo.setTitle(video.getTitle());
playInfo.setM3u8Url(m3u8Url);
playInfo.setDuration(video.getDuration());
// 支持的清晰度列表
List<QualityOption> qualities = Arrays.asList(
new QualityOption("360p", "流畅"),
new QualityOption("720p", "高清"),
new QualityOption("1080p", "超清")
);
playInfo.setQualities(qualities);
return Result.success(playInfo);
}
/**
* ⭐ 上报播放进度
*/
@PostMapping("/{videoId}/progress")
public Result<Void> reportProgress(
@PathVariable String videoId,
@RequestParam Long userId,
@RequestParam Integer progress) {
// 保存播放进度到Redis
String key = "video:progress:" + userId + ":" + videoId;
redisTemplate.opsForValue().set(key, String.valueOf(progress), 7, TimeUnit.DAYS);
return Result.success();
}
/**
* ⭐ 获取播放进度
*/
@GetMapping("/{videoId}/progress")
public Result<Integer> getProgress(
@PathVariable String videoId,
@RequestParam Long userId) {
String key = "video:progress:" + userId + ":" + videoId;
String progress = redisTemplate.opsForValue().get(key);
return Result.success(progress != null ? Integer.parseInt(progress) : 0);
}
}
设计5:CDN加速 🚀
CDN原理:
用户(北京) → 请求视频
↓
CDN:选择最近的边缘节点(北京节点)
↓
北京节点:
- 有缓存 → 直接返回 ✅(超快)
- 无缓存 → 回源到OSS → 缓存 → 返回
优点:
- 就近访问 ✅
- 减轻源站压力 ✅
- 提高播放速度 ✅
配置CDN:
@Service
public class CDNService {
private String cdnDomain = "video.cdn.example.com";
/**
* ⭐ 获取CDN视频地址
*/
public String getVideoUrl(String ossKey) {
// CDN域名 + OSS Key
return "https://" + cdnDomain + "/" + ossKey;
}
/**
* ⭐ 刷新CDN缓存(视频更新时)
*/
public void refreshCache(String ossKey) {
// 调用CDN API刷新缓存
// ...
}
}
🎓 面试题速答
Q1: 视频如何存储?
A: 对象存储OSS:
视频存储路径:
video/{videoId}/{quality}/index.m3u8
video/{videoId}/{quality}/segment0.ts
video/{videoId}/{quality}/segment1.ts
...
例子:
video/abc123/720p/index.m3u8
video/abc123/720p/segment0.ts
video/abc123/720p/segment1.ts
优点:
- 海量存储(PB级)
- 高可靠(多副本)
- CDN加速
Q2: 视频如何播放?
A: HLS切片 + 边下边播:
1. 视频切片(每10秒一个.ts文件)
2. 生成索引文件(.m3u8)
3. 播放器解析m3u8,获取分片列表
4. 依次下载分片,边下边播 ✅
优点:
- 不需要等待整个视频下载
- 支持多清晰度切换
Q3: 视频如何转码?
A: FFmpeg + MQ异步:
// FFmpeg转码命令
ffmpeg -i input.mp4 \
-c:v libx264 \ // 视频编码H.264
-s 1280x720 \ // 分辨率720P
-b:v 2000k \ // 比特率2000k
-hls_time 10 \ // 每个分片10秒
-f hls \ // 输出HLS格式
output.m3u8
异步处理:
- 上传完成 → 发送MQ消息
- 转码Consumer消费 → FFmpeg转码
- 转码完成 → 更新状态
Q4: 如何保证播放流畅?
A: CDN + 多分辨率:
-
CDN加速:
- 就近访问边缘节点
- 减少延迟
-
多分辨率:
- 360P(流畅)
- 720P(高清)
- 1080P(超清)
- 根据网速自动切换
-
预加载:
- 提前下载下一个分片
Q5: 播放进度如何记录?
A: Redis存储:
// 上报播放进度(每5秒)
String key = "video:progress:" + userId + ":" + videoId;
redisTemplate.opsForValue().set(key, progress, 7, TimeUnit.DAYS);
// 下次播放时获取进度
String progress = redisTemplate.opsForValue().get(key);
player.seek(progress); // 跳转到上次播放位置
Q6: 如何防止视频盗链?
A: URL签名:
// 生成带签名的URL
public String generateSignedUrl(String videoId, Long userId) {
// 过期时间(1小时)
long expireTime = System.currentTimeMillis() + 3600000;
// 生成签名
String sign = MD5.encode(videoId + userId + expireTime + SECRET_KEY);
// 拼接URL
return String.format("%s?videoId=%s&userId=%s&expire=%d&sign=%s",
CDN_DOMAIN, videoId, userId, expireTime, sign);
}
// 验证签名
public boolean verifySign(String videoId, Long userId,
long expireTime, String sign) {
// 检查是否过期
if (System.currentTimeMillis() > expireTime) {
return false;
}
// 验证签名
String expectedSign = MD5.encode(videoId + userId + expireTime + SECRET_KEY);
return expectedSign.equals(sign);
}
🎬 总结
视频网站系统核心
┌────────────────────────────────────┐
│ 1. 视频上传 │
│ - 分片上传到OSS │
│ - 获取上传凭证 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 2. 视频转码(FFmpeg)⭐ │
│ - 多分辨率(360P/720P/1080P) │
│ - HLS切片(10秒/片) │
│ - MQ异步处理 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 3. 视频播放 │
│ - m3u8索引文件 │
│ - 边下边播 │
│ - 多清晰度切换 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 4. CDN加速 🚀 │
│ - 就近访问 │
│ - 边缘缓存 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 5. 播放进度(Redis) │
│ - 记录播放位置 │
│ - 断点续播 │
└────────────────────────────────────┘
🎉 恭喜你!
你已经完全掌握了视频网站系统的设计!🎊
核心要点:
- 对象存储OSS:海量存储,高可靠
- FFmpeg转码:多分辨率,HLS切片
- MQ异步处理:上传完成触发转码
- CDN加速:就近访问,边缘缓存
- 播放进度:Redis存储,断点续播
下次面试,这样回答:
"视频网站系统的核心是视频存储、转码和播放。视频存储使用对象存储OSS,支持PB级海量存储和多副本高可靠。用户上传视频时,先获取OSS上传凭证,直接上传到OSS,上传完成后通知服务器,服务器发送MQ消息触发转码任务。
视频转码使用FFmpeg实现。Consumer从MQ消费转码任务,下载原视频到本地,使用FFmpeg命令转码成360P、720P、1080P三种分辨率。转码同时进行HLS切片,每10秒生成一个.ts分片文件,并生成index.m3u8索引文件。转码完成后上传到OSS,更新视频状态为可播放。
视频播放采用HLS协议。客户端请求播放地址,服务器返回CDN加速后的m3u8地址。播放器解析m3u8获取分片列表,依次下载.ts分片文件边下边播。支持多清晰度切换,用户可根据网速选择不同分辨率。
CDN加速是性能关键。视频文件通过CDN分发到全国各地的边缘节点,用户就近访问最近的边缘节点获取视频。边缘节点有缓存直接返回,无缓存则回源到OSS并缓存。这样大大减少了用户到源站的距离,提高播放速度。
播放进度使用Redis存储。客户端每5秒上报一次播放进度,key为'video:progress:用户ID:视频ID',7天过期。下次播放时先查询Redis获取上次播放位置,实现断点续播。
防盗链通过URL签名实现。生成播放地址时加入用户ID、过期时间和签名,签名由这些参数加密钥MD5生成。CDN节点验证签名和过期时间,防止视频被盗链。"
面试官:👍 "很好!你对视频网站的设计理解很深刻!"
本文完 🎬
上一篇: 217-设计一个分布式爬虫系统.md
下一篇: 219-设计一个推荐系统架构.md
作者注:写完这篇,我都想去B站当UP主了!🎬
如果这篇文章对你有帮助,请给我一个Star⭐!