在视频回放、监控分析等系统中,视频通常不能简单地当作普通文件下载。用户需要在网页中直接播放、拖动进度条、快进后退、从上次位置继续播放,并且不同角色还要看到不同范围的视频。

本文介绍一套基于 Spring Boot + MinIO + MySQL + Vue 的视频管理方案,实现:
- 视频上传、删除、分页查看
- 后端从 MinIO 读取视频流
- 支持浏览器原生进度条拖动
- 支持断点续播
- 支持指定时间戳跳转
- 支持前进 10 秒、后退 10 秒
- 按角色和部门控制视频可见范围
一、整体架构
上传/列表/删除/进度
video Range 请求
携带用户角色 Header
视频元数据/播放进度
对象上传/分段读取
Vue 前端
Spring Boot API
Vite 视频代理
MySQL
MinIO
这里 MySQL 不保存视频本体,只保存视频元数据和播放进度;视频文件本体存储在 MinIO 中。
二、为什么拖动进度条依赖 HTTP Range
浏览器 <video> 播放视频时,如果用户拖动进度条,浏览器不会重新下载完整视频,而是发送类似这样的请求:
http
Range: bytes=10485760-20971519
后端必须返回:
http
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 10485760-20971519/73400320
Content-Length: 10485760
Content-Type: video/mp4
只要后端正确支持 Range,浏览器原生播放器就能自然支持拖动、快进、后退和时间跳转。
三、数据表设计
视频元数据表建议命名为 train_record_video:
| 字段 | 说明 |
|---|---|
id |
视频 ID |
train_record_id |
关联训练记录 ID |
original_name |
原始文件名 |
bucket_name |
MinIO bucket |
object_name |
MinIO 对象路径 |
content_type |
视频类型 |
file_size |
文件大小 |
duration_seconds |
视频时长 |
owner_user_id |
上传用户 ID |
owner_dept_id |
上传用户部门 ID |
owner_dept_path |
上传用户部门路径 |
create_time |
创建时间 |
播放进度表 train_record_video_progress:
| 字段 | 说明 |
|---|---|
video_id |
视频 ID |
user_id |
当前播放用户 ID |
current_time_seconds |
当前播放秒数 |
duration_seconds |
视频总时长 |
last_played_at |
最近播放时间 |
四、后端核心接口
text
POST /minio/video/upload
GET /minio/video
GET /minio/video/{id}
GET /minio/video/{id}/stream
DELETE /minio/video/{id}
GET /minio/video/{id}/progress
PUT /minio/video/{id}/progress
其中最关键的是播放接口:
java
@GetMapping("/{id}/stream")
public void stream(@PathVariable String id,
HttpServletRequest request,
HttpServletResponse response) {
trainRecordVideoService.stream(id, request, response);
}
服务层根据 Range 读取 MinIO 指定字节段:
java
InputStream inputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(video.getBucketName())
.object(video.getObjectName())
.offset(rangeStart)
.length(contentLength)
.build()
);
然后返回 206 Partial Content:
java
response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + totalSize);
response.setHeader("Content-Length", String.valueOf(contentLength));
response.setContentType(video.getContentType());
五、权限控制
视频权限不根据用户名判断,而是根据角色和部门判断:
video_admin_user
video_manager_user
video_normal_user
当前用户
角色
查看全部视频
查看本部门及下级部门视频
仅查看自己上传的视频
查询列表时动态拼接条件:
text
admin:
不加 owner 过滤
manager:
owner_dept_path like 当前部门路径%
normal:
owner_user_id = 当前用户ID
详情、播放流、删除、播放进度接口也必须做同样的权限校验,避免用户绕过列表直接访问视频 ID。
六、Vue 前端播放
前端使用原生 <video>:
html
<video
controls
preload="metadata"
:src="streamUrl"
@loadedmetadata="onLoadedMetadata"
@timeupdate="onTimeUpdate"
/>
给定时间戳自动跳转:
js
videoEl.value.currentTime = timestampSeconds
前进 10 秒:
js
videoEl.value.currentTime += 10
后退 10 秒:
js
videoEl.value.currentTime = Math.max(0, videoEl.value.currentTime - 10)
保存断点续播:
js
await fetch(`/api/minio/video/${videoId}/progress`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currentTimeSeconds: videoEl.value.currentTime,
durationSeconds: videoEl.value.duration
})
})
再次打开视频时读取进度:
js
const progress = await fetch(`/api/minio/video/${videoId}/progress`).then(r => r.json())
videoEl.value.currentTime = progress.currentTimeSeconds || 0
七、前端播放代理的必要性
浏览器原生 <video> 标签不能自定义 X-User-Id、X-Roles、X-Dept-Id 这类 Header。
所以在本地联调时,可以通过 Vite 增加一个代理:
text
/video-stream/{id}
由代理把 <video> 的 Range 请求转发给后端,同时补充用户、角色、部门 Header。
MinIO Spring Boot Vite 代理 Vue video MinIO Spring Boot Vite 代理 Vue video GET /video-stream/{id} Range: bytes=0-1023 GET /minio/video/{id}/stream Range + 用户角色Header getObject offset/length 视频字节流 206 Partial Content 206 Partial Content
八、上传大文件配置
视频文件通常较大,需要调整 Spring Boot multipart 限制:
yaml
spring:
servlet:
multipart:
max-file-size: 2048MB
max-request-size: 2048MB
前端代理也建议设置较长超时,避免大文件上传时连接被提前断开。
九、测试重点
上传后先测试列表接口:
bash
GET /minio/video?page=0&size=20
再测试 Range:
bash
curl -i \
-H "Range: bytes=0-1023" \
-H "X-User-Id: user-001" \
-H "X-Roles: video_normal_user" \
http://localhost:8099/minio/video/{id}/stream
期望结果:
text
HTTP/1.1 206
Accept-Ranges: bytes
Content-Range: bytes 0-1023/视频总大小
Content-Length: 1024
只要这里正确,网页播放器拖动进度条、指定时间跳转、快进后退就有了可靠基础。
十、总结
这套方案的关键不是"把视频文件返回给前端",而是:
- 视频本体存 MinIO,元数据和播放进度存 MySQL。
- 播放接口必须完整支持 HTTP Range。
- 前端使用原生
<video>控制时间跳转和快进后退。 - 断点续播由前端上报当前秒数,后端按用户和视频保存。
- 权限必须覆盖列表、详情、播放流、删除和进度接口。
如果视频源主要是 MP4,还建议上传前或上传后确保 MP4 的 moov atom 位于文件头部,例如使用 ffmpeg -movflags +faststart,这样浏览器首帧加载和拖动体验会更稳定。