简介:基于Java Spring Boot与Vue.js开发的前后端分离视频点播系统,面向计算机相关专业学生、毕业设计及课程设计人员,也适合希望掌握全栈开发的学习者。平台采用B/S结构,界面美观、功能齐全,经过测试可快速部署运行,整体包含前台与后台两大板块。压缩包共278个文件,约10.25MB,以Java、Vue、TypeScript、SVG图标及少量MP4演示视频为主,server目录为Spring Boot后端工程,web目录为Vue前端工程,结构清晰便于上手。前台涵盖首页、视频列表、视频详情、交互中心、个人中心等模块,后台提供总览、视频管理、分类管理、标签管理、评论管理、用户管理、运营管理、日志管理与系统信息功能,基本覆盖视频点播系统的业务闭环。已有126人浏览学习,适合作为毕业设计、课程设计或项目初期演示的基础代码,开发者可基于现有模块快速扩展。

1. 视频点播项目真正的工作量在哪里

视频点播是课程设计和毕业设计里出现频率最高的题目之一,因为看起来足够“完整”:有用户、有列表、有播放器、有后台管理。但很多人在答辩时被一个问题问住——“上传一个 200MB 的视频,前端怎么播不卡?”这时候才意识到,视频点播最重的部分不是页面和 CRUD,而是视频文件怎么存储、怎么切片、怎么通过浏览器稳定播放。

用 Java + Spring Boot + Vue 做这套系统,核心原因有两层。第一层是技术栈经典:Spring Boot 负责接口、权限和数据处理,Vue 负责交互和播放器集成,前后端分离之后,每一侧的工作都能单独交付和展示。第二层是视频链路的完整性:Java 后端要能接收上传、调用 FFmpeg 转码切片、提供 M3U8 播放地址,Vue 前端要能处理播放器兼容和路由参数。

这篇博文从架构选型讲到接口设计,再到 Vue 播放 M3U8 的具体参数和部署排错,最后落在答辩验收时怎么把这套项目讲清楚。新手可以顺着步骤复现,有经验的开发者也能直接拿走版本匹配、切片参数和播放器踩坑这几段。

2. 视频点播的项目边界与 Java + Spring Boot 的版本选型

2.1 视频点播项目到底在做什么

点播和直播是两套不同的技术体系。直播讲究低延迟和持续推流,点播讲究文件存储、索引和按需播放。课程设计里做点播,服务端实际只有四件正经事:接收视频文件、对视频做转码切片、维护视频元数据、给前端提供播放地址。

很多人把点播做成“文件上传 + 播放器页面”,这等于把系统降级成了一个简单的文件服务器。真正的点播系统需要在视频入库时生成 HLS 切片,也就是把 MP4 转换成 M3U8 文件加一段一段的 TS 分片。这样做的好处是播放器可以按需拉取片段,拖动进度条时不需要加载整个文件,网络差时也能从低码率片段开始播放。

M3U8 本质上是一个文本索引文件,里面记录了 TS 分片的路径和时长。浏览器本身不直接支持播放 M3U8,需要借助 video.js 或 hls.js 这类前端库来解析。这一步是整个项目里最容易出问题的地方,也是答辩时最值得讲清楚的技术点。

2.2 Spring Boot 版本与 JDK 组合

Spring Boot 版本选错是视频点播项目最常见的“出师未捷”。多数课程设计还在用 MyBatis-Plus、PageHelper 这些依赖,它们的更新节奏未必跟得上 Spring Boot 3.x。加上“springboot版本太高”是高频检索词,说明很多人被版本兼容卡住了。

组合 JDK 适用场景
Spring Boot 2.7.18 JDK 8 或 11 依赖老、资料多、兼容性最好
Spring Boot 3.2.x JDK 17 新项目、GraalVM 相关特性
Spring Boot 3.4.x JDK 17 或 21 追踪新版本、不在意依赖滞后

我一般建议课程设计选 Spring Boot 2.7.18 + JDK 8,并不是因为技术落后,而是这个组合遇到问题时搜得到答案,mybatis-plus-boot-starter 等依赖也不用特意挑版本。如果你已经安装了 JDK 17,选 Spring Boot 3.2.x 也没问题,但要确认依赖的版本兼容。POM 文件的父依赖这样写:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
    <relativePath/>
</parent>

版本号固定成具体数字很重要。不要用 SNAPSHOT,也不要写 LATEST,否则今天是可用的依赖,明天就可能因为远程仓库更新而构建失败。Spring Boot 的父 POM 统一管理了常用依赖版本,子模块只需要声明依赖的 groupId 和 artifactId,版本可以省略,这正是它省心的原因。

2.3 Vue 选 2 还是选 3

Vue 版本选择影响的是前端生态。Vue 2 搭配 Element UI 的写法在旧教程里非常多,但 Element UI 已经停止维护;Vue 3 搭配 Element Plus 或 Naive UI,组件风格更现代,Vite 的启动速度也比 Webpack 快很多。

视频播放器层面其实不受 Vue 版本影响,因为 video.js 和 hls.js 都是原生 JavaScript 库,Vue 只是负责在 mounted 生命周期里初始化播放器实例。我的建议是直接选 Vue 3 + Vite,原因有三个:项目创建简单、依赖安装快、后续如果要做管理系统后台,Element Plus 的表单和弹窗组件比 Vue 2 生态更顺手。

2.4 视频文件不存数据库,只存路径

视频文件的存储策略决定了后续所有代码的写法。把视频二进制内容存进数据库是绝对错误的做法——一张表几行数据就能撑爆数据库内存,查询速度也会被拖垮。常见做法是视频文件保存在磁盘目录,数据库只记录相对路径和元数据。

存储方式 优点 缺点
数据库 BLOB 备份简单 查询慢、内存爆炸
本地磁盘路径 实现简单、读取快 单机存储上限
对象存储 扩展性好、自带 CDN 需要额外服务或费用

课程设计用本地磁盘路径最合适。后台上传文件时写入某个目录,播放时由 Nginx 或 Spring Boot 静态资源映射直接吐出文件。要注意把存储路径配置在 application.yml 里,不要硬编码到 Java 代码中,因为部署环境和开发环境的路径一定不一样。

upload:
  video-path: E:/data/vod/videos
  slice-path: E:/data/vod/hls

一个文件在磁盘上会对应两个产物:原始文件放在 video-path 下,切片后的 M3U8 和 TS 放在 slice-path 下。后续转码任务只读取原始文件,往切片目录写结果,数据库里存的是 M3U8 的相对路径。这样即使切片失败,原始文件还在,可以重新转码。

3. Spring Boot 后端:表结构、上传接口与 FFmpeg 切片链路

3.1 三张核心表和接口清单

视频点播后端不需要设计得太复杂,三张表足够支撑主流程:用户表、视频信息表、分类表。评论和收藏属于加分项,等主链路跑通后再加不迟。

CREATE TABLE `video_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(128) NOT NULL COMMENT '视频标题',
  `cover_url` varchar(255) DEFAULT NULL COMMENT '封面URL',
  `video_url` varchar(255) NOT NULL COMMENT 'M3U8播放地址',
  `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID',
  `duration` int(11) DEFAULT NULL COMMENT '视频时长(秒)',
  `status` tinyint(4) DEFAULT 1 COMMENT '1-正常 0-下架',
  `view_count` bigint(20) DEFAULT 0 COMMENT '播放次数',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

video_url 字段存的是相对路径,比如 /hls/20240512/abc/index.m3u8 。前端拿到这个路径后拼上服务器地址就能播放。接口清单按照页面反推就可以确定:首页要分页查询视频列表、支持关键字搜索;播放页要查询视频详情;后台要上传视频、更新视频信息;用户模块要注册登录。

接口返回统一用 JSON,状态码、消息和数据三件套。这样前端 axios 拦截器可以统一处理错误提示,不用每个页面单独写异常逻辑。

3.2 接收上传文件与分片合并

上传接口是后端第一道关卡。Spring Boot 接收文件用 MultipartFile,但默认最大上传文件只有 1MB,必须配置才能接收大视频。

spring:
  servlet:
    multipart:
      max-file-size: 1024MB
      max-request-size: 1024MB

max-request-size 表示整个 HTTP 请求的大小,除了文件还包括表单字段。只改 max-file-size 不改 request-size 是常见配置错误,上传一个视频附带 title 字段时,请求总大小超过 10MB 就会报错。配置之后接口代码这样写:

@PostMapping("/api/video/upload")
public Result upload(@RequestParam("file") MultipartFile file,
                     @RequestParam("title") String title) {
    String originalFilename = file.getOriginalFilename();
    String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
    String savePath = videoPath + "/" + UUID.randomUUID() + ext;
    file.transferTo(new File(savePath));

    // 调用转码服务,生成 m3u8 切片
    String m3u8Url = transcodeService.convertToHls(savePath);

    // 保存视频信息到数据库
    VideoInfo videoInfo = new VideoInfo();
    videoInfo.setTitle(title);
    videoInfo.setVideoUrl(m3u8Url);
    videoInfo.setStatus(1);
    videoInfoMapper.insert(videoInfo);
    return Result.success(videoInfo);
}

文件名用 UUID 重命名,避免中文文件名和非法字符导致存储出问题。这里做了一个假设:文件上传完成之后直接同步转码。对课程设计来说同步转码足够,但如果视频很大,同步转码会让 HTTP 请求长时间挂着。进阶做法是上传完成后立即返回“转码中”,转码由异步任务执行完成后更新数据库状态。

对于超大视频,前端可以采用分片上传:把文件切成多个 5MB 的分片逐个上传,后端合并。这个技术点如果做进毕设,踩的是“并发上传顺序乱”的坑——一定要用分片序号来保证合并顺序,不能依赖网络请求的到达顺序。

// 接收分片
@PostMapping("/api/video/upload/chunk")
public Result uploadChunk(@RequestParam("file") MultipartFile file,
                          @RequestParam("chunkIndex") Integer chunkIndex,
                          @RequestParam("totalChunks") Integer totalChunks,
                          @RequestParam("identifier") String identifier) {
    File chunkDir = new File(tempPath + "/" + identifier);
    if (!chunkDir.exists()) {
        chunkDir.mkdirs();
    }
    file.transferTo(new File(chunkDir, chunkIndex + ".part"));
    return Result.success("分片上传成功");
}

分片参数说明:identifier 是前端生成的文件唯一标识,确保不同文件不会被合并到一起;chunkIndex 从 0 开始,是合并时的排序依据;totalChunks 用来判断所有分片是否上传完毕。合并时按 chunkIndex 顺序读取所有 .part 文件,写入同一个输出流。断点续传的实现思路就是检查服务端已存在的分片数量,跳过已上传的部分。

3.3 FFmpeg 转码切片:Java 进程调用

原始视频不能直接作为点播资源,需要先转码为 HLS 格式。FFmpeg 是业内最通用的工具,Spring Boot 里通过 ProcessBuilder 调用系统命令完成转码。

ffmpeg -i input.mp4 -codec:v libx264 -codec:a aac -hls_time 10 \
  -hls_list_size 0 -hls_segment_filename "output_%03d.ts" output.m3u8

参数含义分别为: -hls_time 10 指定每个 TS 分片时长为 10 秒,数值越小切出的分片越多,拖动进度条加载越快但文件数量更大; -hls_list_size 0 表示在 M3U8 文件中保留所有分片记录,如果不设这个参数,FFmpeg 默认只保留最近 5 个分片,会导致播放器只能播最近半分钟的内容; -hls_segment_filename 定义分片文件的命名模板, %03d 表示三位序号,从 000 开始递增。

public String convertToHls(String sourcePath) {
    String dirName = UUID.randomUUID().toString();
    String outputDir = slicePath + "/" + dirName;
    new File(outputDir).mkdirs();

    String m3u8Path = outputDir + "/index.m3u8";
    ProcessBuilder builder = new ProcessBuilder(
        "ffmpeg", "-i", sourcePath,
        "-codec:v", "libx264",
        "-codec:a", "aac",
        "-hls_time", "10",
        "-hls_list_size", "0",
        "-hls_segment_filename", outputDir + "/segment_%03d.ts",
        m3u8Path
    );
    builder.redirectErrorStream(true);
    Process process = builder.start();
    // 读取输出流,防止缓冲区阻塞
    process.waitFor();
    return "/hls/" + dirName + "/index.m3u8";
}

redirectErrorStream(true) 的作用是把 FFmpeg 的错误输出合并到标准输出,便于查看转码日志。一个隐蔽的问题是 Java 进程在读取子进程控制台输出时必须不断读取,否则缓冲区满后 FFmpeg 会被阻塞住,表现为转码任务卡死。一定要 waitFor 之前启动一个线程消费 process 的输入流。

3.4 播放地址的静态资源映射

转码完成后,M3U8 和 TS 文件需要通过 HTTP 协议暴露给前端。直接在 Controller 里写接口返回文件字节流效率不高,静态资源映射的方式更简单可靠。

spring:
  web:
    resources:
      static-locations: file:E:/data/vod/

配置后,访问 http://localhost:8080/hls/xxx/index.m3u8 就能直接读取到 E:/data/vod/hls/xxx/index.m3u8 。之所以不直接放在 classpath 下,是因为每次转码都会生成新目录,项目重新部署会清空 classpath 内容。映射到外部磁盘目录,视频文件和应用代码完全分离。如果部署到 Linux,路径换成 /data/vod/ 并把对应目录加入 Nginx 配置即可。

前端拿到 M3U8 地址后,播放器请求时会自动加载 TS 分片。观察浏览器 Network 面板,如果 M3U8 请求返回 200 但 TS 分片返回 404,大概率是切片目录和静态资源映射路径对不上。

4. Vue 前端:播放器选型、M3U8 播放参数与路由跳转

4.1 创建 Vue 项目和安装播放器依赖

Vue 前端用 Vite 创建项目,命令如下:

npm create vue@latest vod-frontend
cd vod-frontend
npm install
npm install video.js

video.js 是目前兼容性最好的 HTML5 视频播放器之一,本身已经内置了 HLS 支持。早期版本需要单独安装 videojs-contrib-hls 插件,现在 video.js 8.x 已经内置 HLS 能力,不需要额外装插件。不推荐为了播放 M3U8 去找 flash 方案,浏览器早已停止支持。

安装依赖如果报 EPERM 错误,通常是 Node 版本和依赖版本冲突。建议固定 Node 16 或 18 LTS 版本,npm install 失败时先删掉 node_modules 和 package-lock.json 再重试。这一步在“vue安装依赖”、“vue安装及环境配置”这些搜索场景里都是高频问题。

4.2 Player.vue 核心代码与参数含义

播放器组件是整个前端最核心的部分。新建一个 Player.vue,在 mounted 钩子里初始化播放器。

<template>
  <div>
    <video
      id="video-player"
      class="video-js vjs-big-play-centered"
      controls
      preload="auto"
    ></video>
  </div>
</template>

<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
import videojs from 'video.js'
import 'video.js/dist/video-js.css'

const props = defineProps({
  src: {
    type: String,
    required: true
  }
})

let player = null

onMounted(() => {
  player = videojs('video-player', {
    autoplay: false,
    controls: true,
    sources: [{
      src: props.src,
      type: 'application/x-mpegURL'
    }]
  })
})

onBeforeUnmount(() => {
  if (player) {
    player.dispose()
  }
})
</script>

说明几个关键参数: type: 'application/x-mpegURL' 告诉 video.js 当前资源是 HLS 流,这一步写漏了会导致播放器无法识别 M3U8 文件,报错信息通常是 “No compatible source was found”; autoplay: false 是推荐值,浏览器对带声音的视频自动播放有严格限制,设成 true 大概率被拦截; controls: true 显示播放控制条。onBeforeUnmount 里的 dispose 是防止 Vue 组件销毁后播放器实例仍然占用资源,尤其在列表页频繁切换时。

4.3 列表页到播放页的路由参数

视频列表页点击卡片后跳转到播放页,需要将视频 ID 通过路由参数传递。“vue路由参数”是这个场景下的热门搜索词,核心写法如下:

// 路由配置
{
  path: '/player/:id',
  name: 'Player',
  component: () => import('../views/PlayerView.vue')
}
// 列表页跳转
this.$router.push({ path: `/player/${row.id}` })
// 播放页获取参数并请求接口
import { useRoute } from 'vue-router'

const route = useRoute()
const videoId = route.params.id

// 请求详情接口
axios.get(`/api/video/${videoId}`).then(res => {
  player.src({
    src: res.data.videoUrl,
    type: 'application/x-mpegURL'
  })
})

跳转时要把参数放在 path 里而不是 query 里,因为播放页刷新后 query 参数会丢失,而 path 参数作为路由的一部分在刷新时仍然保留。更稳妥的做法是把 path 参数和组件数据绑定:进入播放页先只加载播放器框架,拿到视频 ID 再请求详情接口,再将返回的 M3U8 地址设置给播放器。

4.4 开发环境的代理与生产环境的跨域

前后端分离开发时,前端页面在 5173 端口,后端接口在 8080 端口,直接请求必然跨域。最省事的方案是开发环境配置 Vite 代理,让浏览器以为请求的是同源地址。

// vite.config.js
export default defineConfig({
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      },
      '/hls': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
})

这个配置把两个前缀都转发到了后端: /api 对应接口请求, /hls 对应 M3U8 切片文件请求。如果不配置 /hls 代理,前端通过 http://localhost:5173/hls/xxx/index.m3u8 播放时会 404,因为 Vite 开发服务器上根本没有这个路径。生产环境则通过 Nginx 同时托管前端静态资源和后端 API 转发,代理思路一致。

5. 打包部署与答辩验收:本地跑通到现场演示

视频点播项目在答辩前至少要完整走通三条链路:视频上传链路、视频切片链路、前端播放链路。这三条链路任何一条断掉,当场演示就会翻车。

发布前用 Maven 打包后端,注意跳过测试:

mvn clean package -DskipTests

生成的 jar 包用 java -jar 启动。前端构建产物是静态文件:

npm run build

构建完成后 dist 目录就是可部署的静态站点。生产环境用 Nginx 统一托管前后端:

server {
    listen 80;
    server_name vod.example.com;

    # 前端静态文件
    root /opt/vod-frontend/dist;
    index index.html;

    # 后端 API 转发
    location /api/ {
        proxy_pass http://127.0.0.1:8080/api/;
    }

    # 视频切片文件转发
    location /hls/ {
        proxy_pass http://127.0.0.1:8080/hls/;
    }
}

答辩演示前用 VLC 或浏览器直接打开 M3U8 地址测试切片是否可播放。如果视频列表页有图有文字,点进去却黑屏,先用浏览器直接访问 M3U8 链接,能下载就说明后端正常,问题出在前端播放器。播放器报 “No compatible source” 时,检查 type 参数是不是 application/x-mpegURL 。

现象 排查步骤
上传接口报 413 检查 Nginx client_max_body_size
M3U8 能打开但分片 404 检查 hls 目录静态映射路径
播放只有声音没画面 FFmpeg 转码时用 libx264 视频编码器

答辩讲解的角度建议是:先讲清楚点播和直播的区别,再展示 FFmpeg 切片参数调整对加载速度的影响。准备一个小分辨率视频做演示,切片后拖到视频中段,对比加载速度。最后把数据库表中 video_url 字段的值指给评委看,说明系统里存的是 M3U8 路径而不是二进制文件,整个链路的逻辑就闭环了。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐