# Spring Boot + MinIO + MySQL + Vue 实现视频管理与流式播放

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

本文介绍一套基于 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-IdX-RolesX-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

只要这里正确,网页播放器拖动进度条、指定时间跳转、快进后退就有了可靠基础。

十、总结

这套方案的关键不是"把视频文件返回给前端",而是:

  1. 视频本体存 MinIO,元数据和播放进度存 MySQL。
  2. 播放接口必须完整支持 HTTP Range。
  3. 前端使用原生 <video> 控制时间跳转和快进后退。
  4. 断点续播由前端上报当前秒数,后端按用户和视频保存。
  5. 权限必须覆盖列表、详情、播放流、删除和进度接口。

如果视频源主要是 MP4,还建议上传前或上传后确保 MP4 的 moov atom 位于文件头部,例如使用 ffmpeg -movflags +faststart,这样浏览器首帧加载和拖动体验会更稳定。

相关推荐
罗超驿3 小时前
14.MySQL索引底层原理:从数据结构到B+树的深度解析
数据结构·b树·mysql
中国胖子风清扬3 小时前
PageIndex:用推理替代向量的下一代 RAG 架构
java·spring boot·python·spring·ai·embedding·rag
罗超驿3 小时前
15.面试高频考点:MySQL索引底层原理与实战要点全梳理
mysql·面试·职场和发展
长谷深风1113 小时前
SpringBoot开发秘籍【个人八股】
java·spring boot·后端·spring·八股
Front思3 小时前
1040 - Too many connections
mysql
文青小兵4 小时前
云计算Linux——数据库MySQL部分命令 (十五)
linux·数据库·mysql·nginx·云计算
就爱瞎逛4 小时前
解决Ant Design Vue 日期选择器中文不生效
前端·javascript·vue.js
YL200404264 小时前
MySQL-进阶篇-视图/存储过程/触发器
数据库·mysql
墨着染霜华4 小时前
MySQL字符串数字筛选与转换 + Java Integer/Long数值长度避坑指南
java·数据库·mysql