十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue视频管理系统:上传、ffmpeg转码到HLS播放全链路

SpringBoot+Vue视频管理系统:上传、ffmpeg转码到HLS播放全链路 简介面向需要掌握前后端分离视频管理系统的开发者这套SpringBootVue实战案例完整实现了视频上传、在线观看、增删改查、分页与多条件搜索。后端基于SpringBoot与MySQL前端使用Vue与el-upload手动上传并采用“先存视频信息、再携带ID上传”的两步流程视频封面通过FFmpeg截取首帧存储路径与视频保持一致跨域由Nginx反向代理解决。资源共181个文件以Java源码、Vue组件为主辅以JS、XML、SQL等配置与脚本文件压缩包仅5.37MB目录结构清晰便于快速导入运行。已有4712人学习下载适合初中级Java/前端开发者用于毕业设计或项目练手。读者可获得完整项目源码、数据库脚本及部署配置参考其上传逻辑、FFmpeg集成与Nginx代理思路可直接复用或扩展为自己的视频管理模块。1. SpringBoot Vue 视频管理系统先想清楚三件事再动手公司内部培训视频越堆越多前端拿到 mp4 文件直接用video src去播结果 Chrome 能放、iPhone 的 Safari 黑屏拖进度条缓冲半天。这不是播放器的问题是视频没做流化适配。用 SpringBoot Vue 做视频管理系统重点不在增删改查而在「存储、转码、播放」这条链路能否闭环后端收文件、落盘、调 ffmpeg 转码、把 HLS 切片暴露成 URL前端做列表、上传、播放器两端走 REST API也就是前后端分离。做这套系统的人通常有两类想完整过一遍全栈流程的开发者和需要可演示毕设的学生。后面给出一套能直接改的骨架本地存储起步、ffmpeg 转 HLS、video.js 播 m3u8不依赖云服务。每一步都给出了命令、参数和排查方向照着走能跑通「上传 MP4 → 自动转码 → 网页播放」的闭环。2. 存储与数据模型SpringBoot 后端骨架和视频落盘方案2.1 独立工程加 REST 约定前后端分离的工程结构一个视频管理系统在代码上是两套独立工程springboot-video-admin 负责接口默认端口 8080vue-video-admin 负责页面Vue 3 Vite 开发端口 5173。两者之间唯一的契约是接口路径和返回结构前端只认后端返回的 JSON后端不关心页面长什么样。生产环境用 Nginx 把两个服务合成一个入口域名按路径分流。后端依赖最精简的三件套就够起步dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencystarter-web 提供 MVC 和 multipart 上传解析mybatis-plus 主要是省掉手写分页 SQL视频列表天然要分页用它的分页插件能少写不少样板代码mysql 驱动按实际数据库替换想免安装可以用 H2 的 file 模式。如果你对 SQL 更熟换成 spring-boot-starter-jdbc 加 JdbcTemplate 也完全可以视频系统没有高频关联查询数据模型很薄。2.2 视频文件落盘三选一本地目录、MinIO、云 OSS视频文件从几十 MB 到几个 GB 都有存储选型会影响后续所有代码。三种常见方案的差异如下方案实现成本扩容方式适用场景本地磁盘最低File 写直接落盘换盘、加机器毕设、内网小规模MinIO中等S3 协议 SDK分布式多节点私有化、团队内网云对象存储低SDK 封装完善按量付费自动扩容公网产品我的建议是教程、毕设直接选本地磁盘先把链路跑通省去配置对象存储的额外变量如果你考虑以后迁 MinIO代码里定义一个 StorageService 接口本地实现和 MinIO 实现各写一个类Controller 只依赖接口换存储不动业务代码。现在很多视频管理系统翻车都是因为起步时把存储绑定死后面换存储要大面积改。本地落盘有一个容易踩的坑不要把视频目录放在 src/main/resources 下resources 会被打进 jar 包重启后文件丢失或路径不可写。正确做法是独立外部目录用 yml 配置video: storage-path: /data/videos # 改成本机任意绝对路径目录结构约定为{storage-path}/{videoId}/videoId 是数据库主键。转码产物和原始文件都放在以 videoId 命名的子目录里删除视频时直接删整个目录不同视频的文件不会混在一起排查问题时顺着 videoId 就能找到全部现场。2.3 视频信息表一个 status 字段管住转码流程文件落盘只是第一步视频的标题、时长、状态这些元数据要进数据库。视频管理系统最小的表结构CREATE TABLE video_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 视频标题, original_name VARCHAR(255) DEFAULT COMMENT 原始文件名, m3u8_url VARCHAR(500) DEFAULT COMMENT 播放地址, duration INT DEFAULT 0 COMMENT 时长单位秒, size BIGINT DEFAULT 0 COMMENT 原始文件字节数, status TINYINT DEFAULT 0 COMMENT 0转码中 1可播放 2转码失败, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT视频信息表;status 是整个系统的状态机。上传接口先插一条 status0 的记录ffmpeg 转码成功回填 m3u8_url 并更新为 1命令退出码非 0 则更新为 2。前端列表页根据 status 分别展示「转码中」「可播放」「失败」播放按钮只在 status1 时可点。这个小约定能避免用户点进一个转码还没结束的播放地址看到 404。后面每个接口都围着 status 转先把它定清楚前后端联调会顺畅很多。3. SpringBoot 上传与转码MultipartFile 接文件、ffmpeg 输出 m3u83.1 上传接口MultipartFile 接收与 multipart 大小参数上传是整条链路的第一站。Controller 层用 MultipartFile 接收文件接口设计为 POST /api/video/upload文件参数名 file标题参数名 titlePostMapping(/upload) public ResultLong upload(RequestParam(file) MultipartFile file, RequestParam(title) String title) throws IOException { Long videoId IdWorker.getId(); // 雪花 ID避免依赖自增回填 Path videoDir Paths.get(storagePath, videoId.toString()); Files.createDirectories(videoDir); String originalName file.getOriginalFilename(); Path sourceFile videoDir.resolve(source_ originalName); file.transferTo(sourceFile.toFile()); // 先落原始文件转码再读 VideoInfo video new VideoInfo(); video.setId(videoId); video.setTitle(title); video.setOriginalName(originalName); video.setStatus(0); // 转码中 videoMapper.insert(video); transcodeService.transcodeAsync(videoId, sourceFile); // 异步转码 return Result.ok(videoId); }transferTo 把临时文件移动到目标位置比 getInputStream 手动拷贝省事但要求目标目录存在且可写所以先 createDirectories。ID 生成用雪花算法而不是数据库自增因为后续目录名和文件名都要用这个 ID先拿到值可以少一次查询。转码必须异步否则一个大文件会让上传请求挂住几十秒前端会以为接口超时通常在 Service 里用 Async 或线程池提交。Result 是统一响应包装code/data 的结构前后端约定好即可。SpringBoot 默认的 multipart 上限是 1MB视频文件几乎必然超限不调的话请求直接 413spring: servlet: multipart: max-file-size: 2GB max-request-size: 2GBmax-file-size 是单个文件上限max-request-size 是单次请求总大小按系统定位调整。内网工具 2GB 够用公网产品建议做分片上传这个边界放到第 5 章说。3.2 ffmpeg 转 HLS命令参数与执行日志定位视频转码是把这个系统做「流化」的关键也是和普通文件管理系统最大的分水岭。常见做法是用 ffmpeg 把 MP4 转成 HLS 协议产物是一份 index.m3u8 播放清单加上一串 .ts 切片ffmpeg -i source.mp4 \ -c:v libx264 -profile:v main -crf 23 \ -c:a aac \ -hls_time 10 -hls_list_size 0 \ -hls_segment_filename segment_%03d.ts \ index.m3u8关键参数按实际场景调参数作用建议值-c:v libx264H.264 视频编码兼容性最好固定-crf 23画质档位数值越小画质越高文件越大23演示可 25-hls_time 10每个切片的时长秒10 或 5切片越短拖动越灵敏-hls_list_size 0m3u8 中保留全部切片0默认只保留最近 5 个为什么转 m3u8 而不是直接播 mp4浏览器原生支持 mp4 渐进式播放但 iPhone 的 Safari 对部分编码格式兼容性差大文件拖动进度条需要服务端支持 Range 请求整体体验不如分片流。HLS 是 Apple 提出、目前全平台兼容的分片流协议浏览器端可以用 MSE 播放移动端原生支持。把视频转成 HLS本质上是拿 ffmpeg 的一次转换换掉所有终端的播放兼容问题。Java 侧用 ProcessBuilder 执行命令并采集日志ProcessBuilder pb new ProcessBuilder( ffmpeg, -y, -i, sourceFile.toString(), -c:v, libx264, -profile:v, main, -crf, 23, -c:a, aac, -hls_time, 10, -hls_list_size, 0, -hls_segment_filename, videoDir.resolve(segment_%03d.ts).toString(), videoDir.resolve(index.m3u8).toString()); pb.redirectErrorStream(true); Process process pb.start(); try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream()))) { String line; while ((line reader.readLine()) ! null) { log.info(ffmpeg: {}, line); } } int exitCode process.waitFor(); if (exitCode 0) { video.setM3u8Url(/hls/ videoId /index.m3u8); video.setStatus(1); } else { video.setStatus(2); } videoMapper.updateById(video);redirectErrorStream(true) 把 stderr 并到 stdout循环读日志是为了避免进程输出缓冲区写满导致 ffmpeg 阻塞。一旦转码失败日志里的「Invalid data found」「Unknown encoder」这类信息就是定位的第一现场。两个容易忽略的点ffmpeg 要在系统 PATH 里否则 ProcessBuilder 抛 IOException-y参数让转码覆盖已有输出重试时不会卡在交互确认。提示转码失败时 ffmpeg 的 stderr 里能看到具体原因日志一定要落盘并且带上 videoId后面排查能直接对应到脏数据。3.3 m3u8 播放地址静态资源映射与 URL 拼接转码完的 index.m3u8 和 ts 切片在本地磁盘浏览器没法直接访问文件系统要让 SpringBoot 把它们暴露成 URL。做法是配置资源映射把 /hls/** 路径映射到存储目录Configuration public class WebConfig implements WebMvcConfigurer { Value(${video.storage-path}) private String storagePath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/hls/**) .addResourceLocations(file: storagePath /); } }这里有个细节addResourceLocations 的 file: 前缀不能省结尾的斜杠也不能省否则路径拼接会出问题。映射完成后请求 /hls/1001/index.m3u8 对应磁盘上的 {storage-path}/1001/index.m3u8m3u8 里的切片路径是相对路径 segment_000.tsffmpeg 生成的播放清单引用的相对地址恰好能顺着同一个 URL 前缀请求到 ts 文件。所以数据库里 m3u8_url 存相对路径即可前端拼上服务器地址就能播播放域名变了也不影响数据。4. Vue 前端实现视频列表、上传组件与 m3u8 播放器4.1 Vue 3 项目初始化和路由配置前端用 Vue 3 Vite比 Vue 2 Webpack 起步更快Vite 的 dev server 对 /api 的代理配置也简单。初始化命令npm create vitelatest vue-video-admin -- --template vue cd vue-video-admin npm install npm install vue-router4 axios video.jsvue-router 负责页面路由axios 负责接口请求video.js 负责播放。三个依赖就是视频管理系统前端的全部家当。路由设计成两页就够路由组件说明/VideoList视频列表页/play/:idVideoPlay视频播放页import { createRouter, createWebHistory } from vue-router import VideoList from ../views/VideoList.vue import VideoPlay from ../views/VideoPlay.vue const router createRouter({ history: createWebHistory(), routes: [ { path: /, component: VideoList }, { path: /play/:id, component: VideoPlay } ] })路由参数:id在播放页里用route.params.id取出然后请求视频详情接口拿 m3u8 地址。开发环境要把 /api 和 /hls 都代理到后端 8080这是前后端分离联调最容易漏掉的一步// vite.config.js server: { proxy: { /api: http://localhost:8080, /hls: http://localhost:8080 } }很多人只代理了 /api结果接口通、播放器黑屏因为 m3u8 和 ts 的请求还是直连前端开发服务器返回的是 index.html。把 /hls 也代理上开发阶段的问题能少一半。4.2 列表页axios 分页请求和状态展示列表页是视频管理系统的门面。用 axios 请求分页接口配合 Element Plus 的表格展示script setup import { ref, onMounted } from vue import axios from axios const list ref([]) const total ref(0) const page ref(1) async function fetchList() { const { data } await axios.get(/api/video/list, { params: { page: page.value, size: 20 } }) list.value data.records total.value data.total } onMounted(fetchList) /script template el-table :datalist el-table-column proptitle label标题 / el-table-column propduration label时长(秒) / el-table-column label状态 template #default{ row } el-tag v-ifrow.status 1 typesuccess可播放/el-tag el-tag v-else-ifrow.status 2 typedanger转码失败/el-tag el-tag v-else typewarning转码中/el-tag /template /el-table-column el-table-column label操作 template #default{ row } el-button :disabledrow.status ! 1 clickgoPlay(row)播放/el-button /template /el-table-column /el-table /template后端分页响应按 MyBatis Plus 的 Page 对象序列化字段固定是 records 和 total如果用别的分页框架前后端约定好结构就行。status 和 UI 的映射关系固定成一张表前端不要再写散落的判断逻辑status含义前端展示0转码中warning 标签播放按钮禁用1可播放success 标签可点击播放2转码失败danger 标签可删除或重试状态列直接映射成标签转码中的视频播放按钮置灰这个交互细节能让用户直观感知「后台还在处理」而不是以为系统坏了。4.3 播放页video.js 播放 m3u8 的配置播放页是这条链路的终点。video.js 从 8.x 起内置了 HTTP Streaming 模块播放 m3u8 不需要再装 videojs-contrib-hls这是经常在网上搜到过时教程的地方script setup import { ref, onMounted, onUnmounted } from vue import { useRoute } from vue-router import videojs from video.js import video.js/dist/video-js.css const route useRoute() const playerRef ref(null) let player null onMounted(async () { const { data } await axios.get(/api/video/${route.params.id}) player videojs(playerRef.value, { controls: true, sources: [{ src: data.m3u8Url, type: application/x-mpegURL }] }) }) onUnmounted(() player player.dispose()) /script template video refplayerRef classvideo-js vjs-big-play-centered / /templatedata.m3u8Url 是后端存的相对路径比如 /hls/1001/index.m3u8浏览器会基于当前页面地址自动补全域名。type 写成 application/x-mpegURL 是关键video.js 靠这个 MIME 判断走 HLS 处理链路。组件卸载时调用 player.dispose() 释放播放器避免路由切换后音频还在后台播放这个细节在 SPA 里经常被漏掉。注意video.js 8.x 已内置 HLS 播放能力不要再按旧教程安装 videojs-contrib-hls否则会出现两个播放内核抢资源的问题。如果播放器黑屏且控制台出现跨域报错优先检查后端 CORS 是否放行了 /hls 路径以及代理是否覆盖了这个前缀。m3u8 是文本清单里面的 ts 切片地址是 ffmpeg 生成的相对路径请求的域名和 m3u8 页面一致所以代理规则只要覆盖视频前缀就全部生效。5. 播放链路排错三个必调参数和 m3u8 完整性验证5.1 三个必调的参数链路写完后最常翻车的就是下面三个地方环境配置没错播放就通了配置位置参数默认值的问题视频系统建议application.ymlspring.servlet.multipart.max-file-size默认 1MB传视频直接 4132GB 或按业务调整WebConfigaddResourceHandlers /hls/**不配则 m3u8 请求 404file: 前缀和尾斜杠都要有vite.config.js / Nginx/api 和 /hls 代理只配 /api 则播放黑屏两个前缀都代理到 8080multipart 上限忘了调前端表现是上传接口报 413这类错误不会出现在后端日志因为请求在进入 Controller 之前就被拦截排查时先看浏览器 Network 面板的状态码。资源映射漏配m3u8 请求直接 404。代理只配一半最常见也最隐蔽接口能通、播放器黑屏逐条看网络请求会发现 ts 请求返回的是 HTML。5.2 验证 m3u8 切片完整性的技巧转码完成后不要急着点播放。先用 ffprobe 验证一遍播放清单ffprobe -v error -show_entries formatduration -of csvp0 index.m3u8这个命令读取 m3u8 清单并输出总时长。输出 0 说明清单格式有问题报错说明切片引用了不存在的文件。再配合统计切片数量ls -l segment_*.ts | wc -l grep -c ^#EXTINF index.m3u8第二个命令统计清单里的切片条目数两个数字一致说明切片都写进了播放清单。这三个命令一分钟内就能判断转码产物能不能播不用打开浏览器可以固化成一个 check.sh每次转码结束自动跑一遍失败的视频在入库前就被拦住。最后多说一个日常排查手段写一个定时任务扫描 status0 但创建时间超过 5 分钟的视频强制置为失败。ffmpeg 进程被杀、服务器重启都会留下永远停在转码中的死记录这个兜底逻辑在视频管理系统里属于必须有的边界处理。本文还有配套的精品资源点击获取
返回列表