前后端分离视频点播系统源码部署与Nginx整合实战
简介:一套完整的视频点播系统源码,基于PHP 5.6与MySQL 5.6环境,采用前后端分离架构,适合具备PHP开发基础、希望快速搭建或二次开发视频平台的站长、开发者及学习者。压缩包共2000个文件,约81.5MB,类型涵盖PHP后端逻辑、JS交互脚本、HTML页面模板、CSS样式表、SQL数据库文件及JSON配置等,其中PHP负责接口与数据管理,JS/CSS构建前台交互,SQL提供数据库初始化内容,结构完整,可支撑从环境部署到功能定制的全过程。目前已有265人学习下载。源码附带详细搭建说明,前后端站点分离部署,后台默认管理员账号便于直接登录体验;配套的伪静态规则、运行目录配置、附件设置及缓存刷新模块,能帮助使用者快速完成站点上线与调优。大量静态资源与前端组件,也为理解视频点播系统的整体架构、接口调用及页面渲染提供了高价值参考。
1. 拿到「视频点播系统完美版源码」后,先搞清楚这份 zip 里到底是什么
一个标着「完美版」「前后端分离」的视频点播系统源码包,解压出来通常不超过 300MB,里面一般就是几个固定角色:前端工程目录、后端工程目录、一个 database 或 sql 目录,外加一份 README 或部署文档。这类包在网盘和源码站上流传很广,多数是某套开源项目的二次打包,原作者信息往往被抹掉了,真正有价值的部分是数据库脚本和接口设计,而不是「完美」两个字本身。
我第一次拿到类似的包时,第一件事是先把目录树打出来,判断前后端各自用的什么技术栈。因为「前后端分离」四个字在不同项目里的含义差距很大:有的是 Vue + Spring Boot,有的是 Vue + Django,还有的是 React + Node。技术栈不同,启动方式、依赖安装、代理配置完全是另一套逻辑。先花 10 分钟把结构摸清,比你直接 npm install 之后撞一堆报错要快得多。这一章解决的就是定位问题:这份 zip 里哪些文件决定它能不能跑起来,以及跑起来之后你大概需要准备哪些外部依赖。
2. 判断前后端技术栈与准备最小运行环境
2.1 先看配置文件和依赖清单,不猜技术栈
解压后的第一件事,不是急着找启动脚本,而是看根目录和每个子目录里的特征文件。前端项目基本都有 package.json ,后端项目要么有 pom.xml (Maven)、 build.gradle (Gradle),要么有 requirements.txt 或 Pipfile 。用一行命令就能把技术栈摸清:
# 在解压后的根目录执行
find . -maxdepth 3 -name "package.json" -o -name "pom.xml" -o -name "requirements.txt" -o -name "build.gradle" | sort
find 限制了最大深度为 3,避免把 node_modules 里的依赖配置也扫出来。正常情况你能看到 2 个关键路径: frontend/package.json 和 server/pom.xml ,这就说明是标准的 Vue + Spring Boot 前后端分离项目;如果看到的是 requirements.txt ,那后端就是 Python 系。输出的路径顺序能帮你把前端和后端目录对应起来,后面配代理时要用到。
国内视频点播类源码最常见的技术栈组合,按我在实际项目里见过的频率排序如下:
| 前端 | 后端 | 特征文件 | 常见改造场景 |
|---|---|---|---|
| Vue 2 / Vue 3 | Spring Boot | pom.xml 、 application.yml | 企业内训、在线教育、付费点播 |
| Vue 2 | Django / Flask | requirements.txt 、 manage.py | 中小型站、快速二次开发 |
| Vue 3 + TypeScript | Node.js (Express/Nest) | app.js 、 src/main.ts | 视频课程平台、个人项目 |
判断完框架后,还要看数据库类型。大多数系统用的是 MySQL,极少数会带 MongoDB。看 application.yml 或 settings.py 里的连接串最直接, jdbc:mysql:// 开头就是 MySQL, mongodb:// 开头就是 MongoDB。这一步别省,因为后面建库导数据完全不一样。
2.2 初始化数据库和中间件,这是「完美版」最容易翻车的一步
不管代码写得怎么样,视频点播系统一定绕不开三样东西:数据库存用户和视频元数据、Redis 做缓存和登录态、对象存储(或本地磁盘)放视频文件。如果你只在本地演示,对象存储用本地目录就够了,但数据库和 Redis 一个都不能少。
mysql -uroot -p -e "CREATE DATABASE vod_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
mysql -uroot -p vod_system < ./database/vod_system.sql
第一个命令建库时指定了 utf8mb4 字符集,而不是默认的 utf8 。原因很简单:视频点播系统几乎必然要存用户昵称、视频标题、弹幕内容,这些都是用户输入,一旦遇到 emoji 或生僻字, utf8 会直接报错或存成乱码。 utf8mb4 是 MySQL 5.5.3 之后推荐的完整 UTF-8 实现,后面你在代码里看到的 characterEncoding=utf-8 是指应用和数据库之间的连接编码,两个都要配对,否则中文标题存进去再查出来还是正常的,但搜索时可能对不上。
导完数据后启动 Redis:
# 启动并指定配置文件,后台运行
redis-server /etc/redis/redis.conf --daemonize yes
redis-cli ping
redis-cli ping 返回 PONG 表示 Redis 正常。注意检查 redis.conf 里是否设置了 requirepass 。很多开源项目默认不设密码,但 application.yml 里会带上 password: 123456 ,这两者不一致会导致应用启动时报 ERR Client sent AUTH, but no password is set 或 NOAUTH Authentication required 。我的做法是:先不加密码跑通,最后再统一加固,避免一上来就被鉴权报错挡住。
2.3 确认 FFmpeg 是否可用,点播系统离不开它
视频点播系统的核心链路是「上传 → 转码 → 播放」。上传的原始视频可能是 2GB 的 4K 文件,但用户不一定需要那么大的码率,所以服务端通常会用 FFmpeg 生成多码率的 HLS(m3u8 + ts 分片)或 MP4 转码版本。这意味着后端代码里大概率有调用 FFmpeg 的逻辑,比如用 ProcessBuilder 或 subprocess 执行转码命令。
ffmpeg -version | head -n 1
ffprobe -version | head -n 1
ffmpeg -version 输出的第一行是编译版本和配置参数, ffprobe 是配套的媒体探测工具,用于读取视频时长、分辨率、编码格式。转码时如果 FFmpeg 路径不对或没安装,日志里通常会出现 Cannot run program "ffmpeg": error=2, No such file or directory ,这是整个点播系统最容易被忽略的环境依赖。多数项目会在配置项里写死 ffmpeg.path ,你可以先用 which ffmpeg 拿到实际路径,再看配置文件里是否需要改。
提示:下载源码包时记得看压缩包内是否带
ffmpeg目录或bin目录。有些「完美版」会把 Windows 和 Linux 两个版本的 FFmpeg 都打进去,用的时候要根据操作系统选对,别用 Windows 的 exe 在 Linux 上跑。
3. 后端启动与接口层改造:端口、跨域、JWT 与 token 刷新
3.1 Spring Boot 项目的核心配置项与启动命令
多数视频点播系统后端用 Spring Boot,核心配置集中在 src/main/resources/application.yml (或 application.properties )。我一般会用 grep 快速定位需要改的项,而不是用编辑器逐个翻:
grep -nE "port:|url:|username:|password:|ffmpeg:|upload" server/src/main/resources/application.yml
这个命令把端口、数据库连接串、账号密码、FFmpeg 路径、上传目录一次性打出来。视频点播系统里最容易漏改的是 upload.path 或 file.upload-dir ,它决定视频传到哪个目录。如果这个目录不存在,后端启动时不报错,但上传接口一调用就返回 500。所以先手动建好目录并确认权限:
mkdir -p /data/vod/uploads
chown -R $(whoami) /data/vod/uploads
chown -R 把目录归属权交给当前用户,避免启动进程用非 root 身份时没有写入权限。如果你用的不是 Spring Boot,比如后端是 Django,那就是在 settings.py 里改 MEDIA_ROOT 和 DATABASES 配置,逻辑完全一样:让后端进程知道视频文件往哪写、数据库连哪个实例。
接下来启动后端。用 Maven 的项目在 server 目录下执行:
cd server
mvn spring-boot:run
mvn spring-boot:run 会先下载依赖再启动应用,首次执行可能要几分钟。启动成功后日志里会有 Tomcat started on port 8080 或类似字样。如果用的是打包好了的 jar,直接 java -jar target/vod-server.jar --server.port=8080 即可,但要注意 --server.port 是命令行参数,优先级高于 application.yml 里的配置。
3.2 跨域配置与请求路径规则
前后端分离后,前端跑在 http://localhost:9528 ,后端跑在 http://localhost:8080 ,浏览器会禁止前端直接跨域调用后端接口。用 Spring Boot 的话,常见做法是写一个全局配置类:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
addMapping("/api/**") 限制了只有 /api 开头的路径才允许跨域访问,而不是把整个后端全部放开。 allowedOriginPatterns("*") 配合 allowCredentials(true) 是前后端分离项目里常见的配置组合,因为带 Cookie 或 Authorization 头的跨域请求不允许使用单一的 * ,Spring Boot 里要用 allowedOriginPatterns 才能同时满足「任意来源」和「携带凭证」。这里要提醒一点:如果你用的是 Nginx 做反向代理(后面第 5 章会讲),跨域配置在代理层做更合适,后端这层保持默认即可,两层都配反而可能出现重复响应头的告警。很多开源项目自带跨域配置,你只需要确认它放行的路径前缀和前端调用的一致就行。
3.3 登录态与 token 刷新机制:点播场景的特殊性
视频点播系统和普通管理系统在鉴权上的最大区别是「长时间播放」。用户可能挂着页面看一整部电影,如果 token 有效期只有 30 分钟,看到一半接口突然返回 401,播放进度就断了。我见过的开源点播项目里,常见的方案是:登录时同时发放 accessToken (短时效,15~30 分钟)和 refreshToken (长时效,7~30 天),前端在请求拦截器里统一处理刷 token 逻辑,而不是让用户重新登录。
后端 JWT 拦截器里的核心判断逻辑通常是这样的:
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (jwtUtil.isTokenExpired(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("{\"code\":401,\"msg\":\"token_expired\"}");
return;
}
}
substring(7) 是把 Bearer 这 7 个字符去掉,拿到纯 token。 jwtUtil.isTokenExpired 内部解析 token 的过期时间戳并和当前时间比较。这里有一个细节:如果你的前端拿到的响应是 {"code":401} 这种业务状态码,HTTP 状态码却是 200,那前端拦截器可能只在 HTTP 层判断,就会漏掉这个 401 分支。所以要统一约定:业务态和 HTTP 态要一致,或者前端两个地方都判断。
表:点播系统前后端分离下的典型鉴权字段
| 字段 | 存放位置 | 有效时长 | 说明 |
|---|---|---|---|
| accessToken | 内存或 localStorage | 15~30 分钟 | 每次接口请求携带 |
| refreshToken | HttpOnly Cookie 或 localStorage | 7~30 天 | 仅在刷新 token 时使用 |
| userInfo | Vuex / Pinia | 随会话 | 存放头像、昵称、会员等级 |
token 刷新有一个安全边界要讲清楚: refreshToken 如果放在 localStorage ,一旦被 XSS 脚本读取,账号基本就丢了。更稳的做法是放在 HttpOnly Cookie 里,但这样做跨域配置又要调,因为 Cookie 跨域需要 SameSite=None; Secure 。在本地演示阶段我一般先放 localStorage ,方便调试,生产环境再上 Cookie 方案。这个取舍在 README 里往往不会写,需要自己根据部署场景定。
3.4 后端启动失败的常见日志与排查路径
| 症状 | 日志关键字 | 排查方向 |
|---|---|---|
| 启动即退出 | APPLICATION FAILED TO START | 端口被占用,或数据库连接串错误 |
| 连不上数据库 | Communications link failure | 检查 MySQL 是否启动、账号密码、 useSSL 参数 |
| Redis 报错 | NOAUTH Authentication required | application.yml 里密码和 Redis 实际配置不一致 |
| 上传报 500 | Failed to create directory 或 Permission denied | 检查上传目录是否存在及其写权限 |
| FFmpeg 找不到 | Cannot run program "ffmpeg" | which ffmpeg 确认路径已写进配置 |
数据库连接失败是最多的,和服不「完美版」没关系,纯粹是环境差异。Spring Boot 2.x 之后的 MySQL 连接串通常要带 serverTimezone=Asia/Shanghai ,因为新版 MySQL 驱动默认用 UTC,不带时区和本机时间差 8 小时,这会导致视频的「发布时间」字段全错位,如果你发现首页视频列表和详情页时间显示不一致,先查这里。
4. 前端工程化改造:代理、请求拦截与视频上传组件
4.1 本地联调的代理配置,解决浏览器跨域拦截
前端开发时,Vue 项目跑在自己的 dev server 上,端口通常是 9528 或 8081,而后端 API 在 8080。两种解决方式里,改后端 CORS 我们已经在 3.2 里讲了;另一种是让 dev server 把 /api 前缀的请求转发给后端,浏览器看到的还是同源请求。
以 Vue 的 vue.config.js 为例:
module.exports = {
devServer: {
port: 9528,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '/api' }
}
}
}
}
proxy 里的 '/api' 是匹配规则,前端发往 /api/user/login 的请求会被代理转发到 http://localhost:8080/api/user/login 。 changeOrigin: true 会让请求头里的 Host 字段改成目标地址,有些严格校验 Host 的后端接口不设置这个就会返回 403。 pathRewrite 的作用是路径替换,这里写的 ^/api 换成 /api 等同于保留原路径;如果你的后端接口本来就不带 /api 前缀,就改成 pathRewrite: { '^/api': '' } 。很多开源项目的后端 Controller 用的 @RequestMapping("/api/v1") 之类的前缀,先去后端看几行代码确认,再决定 pathRewrite 怎么配,别照抄网上的配置。
4.2 Axios 请求拦截:自动带 token,遇到 401 刷 token 重试
前端所有请求都是走封装的 axios 实例发出的。视频点播系统里,视频列表、播放地址、上传凭证这些接口都需要登录态,所以每发一次请求都要把 accessToken 塞进请求头。这个逻辑一般集中在 src/utils/request.js 或 src/api/request.ts :
import axios from 'axios'
const service = axios.create({
baseURL: '/api',
timeout: 15000
})
service.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
service.interceptors.response.use(
(response) => response.data,
async (error) => {
const { response } = error
if (response && response.status === 401) {
const refreshToken = localStorage.getItem('refreshToken')
if (refreshToken) {
const res = await axios.post('/api/auth/refresh', { refreshToken })
localStorage.setItem('accessToken', res.data.accessToken)
// 重放原请求
const config = error.config
config.headers['Authorization'] = `Bearer ${res.data.accessToken}`
return service(config)
}
}
return Promise.reject(error)
}
)
service.interceptors.request.use 在请求发出前拦截,把 token 放进 Authorization 头。如果登录成功但 refreshToken 也过期了, response.status === 401 这个分支里的 refreshToken 要么是空,要么后端返回 401,这时就应该跳转登录页而不是继续重试。一个容易踩的坑是:401 重试时如果并发了好几个请求,比如页面同时加载视频列表和用户信息,两个请求都发现 token 过期,就会触发两次刷新 token 的请求,后一次刷新会把前面刷新出来的 token 顶掉。优化做法是维护一个 isRefreshing 标志和订阅队列,刷新期间把新请求挂起,token 刷新完再统一放行。这个逻辑不复杂,但效果很明显,播放入口页的接口特别多,值得单独写一遍。
4.3 视频上传组件:分片上传与表单字段约定
视频文件动辄几百 MB 甚至几个 GB,直接用 multipart/form-data 一把梭上传,失败就要重来,体验很差。成熟的开源点播系统几乎都做了分片上传。前端按 5MB~10MB 切块,每块独立上传,后端收到后按 chunkIndex 顺序合并。前端最简实现如下:
const CHUNK_SIZE = 5 * 1024 * 1024 // 5MB 一个分片
function uploadVideo(file, videoId) {
const chunkCount = Math.ceil(file.size / CHUNK_SIZE)
const tasks = []
for (let i = 0; i < chunkCount; i++) {
const formData = new FormData()
formData.append('file', file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE))
formData.append('videoId', videoId)
formData.append('chunkIndex', i)
formData.append('chunkCount', chunkCount)
tasks.push(
axios.post('/api/video/upload/chunk', formData, {
timeout: 0, // 大分片上传不要设超时
headers: { 'Content-Type': 'multipart/form-data' }
})
)
}
return Promise.all(tasks)
}
file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) 在不复制整个文件的前提下读取对应字节段。 formData.append 的字段名要和后端 MultipartFile 参数名严格一致,比如后端是 @RequestParam("file") MultipartFile file ,那前端就必须叫 file ,不能叫别的。 timeout: 0 是关键,5MB 在上行带宽只有 1Mbps 的家宽上可能要传半分钟,默认的 15 秒超时会导致分片传不完直接报错。分片上传的另一个隐藏作用是「断点续传」:上传失败的片只需要单独重传,已经成功的片不用再传,前提是后端提供了查询哪些分片已上传的接口。
4.4 播放器与 HLS 播放地址的对接
上传完成经过转码后,后端返回给前端的通常不是一个 .mp4 直链,而是一个 .m3u8 播放列表地址。播放器组件在页面里的核心逻辑是拿到播放地址后设置给 video 标签,大多数开源项目用的是 video.js 或 plyr 。以 video.js 为例:
const player = videojs('my-video', {
controls: true,
sources: [{
src: videoPlayUrl, // 形如 https://your-domain/live/xxxx/index.m3u8
type: 'application/x-mpegURL'
}]
})
sources 里填的 src 是后端动态生成的播放地址, type 必须指定为 application/x-mpegURL ,video.js 才能识别并自动拉起 HLS 的解码器。有些系统为了做播放权限,会在 videoPlayUrl 后面拼 token 参数或签名串,比如 index.m3u8?sign=abc123&expires=1699999999 。如果你把播放地址打印到 console 里调试时发现带了一串参数,那是正常的,不要手动删掉,否则后端 CDN 或拦截器会返回 403。HLS 相比 MP4 的优势是支持多码率自适应,弱网下自动降清晰度,这是点播系统该有的体验,「完美版」里没做的话可以自己扩展。
5. 用 Nginx 把前后端整合成能直接演示的完整点播系统
5.1 构建前端并产出静态文件
开发联调跑通后,最终要给人演示,不能一直开着 npm run dev 。前端项目执行 npm run build ,产物会输出到 dist 目录,里面就是 index.html 和一堆带 hash 的 js/css 文件。这是前后端分离项目部署时最重要的一步:前端最终是一个纯静态目录,后端是独立的 API 服务,两者通过 Nginx 统一对外。
cd frontend
npm run build
ls -la dist/
dist 目录下如果有 static 或 assets 子目录,说明构建时把资源按类型归好了。拿 dist 整体放到服务器路径下,比如 /data/www/vod-front ,后面 Nginx 让它直接对外服务即可。这里有个小细节:构建时 baseURL 如果写的是 /api (相对路径),那么 dist 里的 js 不管部署在哪个域名下都能正确请求 API;如果写死了 http://localhost:8080 ,那部署到别的机器上就会请求到不存在的地址。所以检查前端代码里 axios 的 baseURL 一定不要写死带域名的全路径。
5.2 Nginx 配置:静态页面 + API 反向代理 + 视频目录映射
这是最终让「前后端分离」变成「用户无感」的关键一环。用户在浏览器里访问 http://your-server ,看到的是前端页面;页面里的请求 /api/xxx 经过 Nginx 转发到后端的 8080 端口;视频文件则通过另一个 location 直接读取本地 /data/vod/uploads 目录。这样整个系统对外只暴露一个域名,天然避免了跨域问题,因为浏览器看所有资源都是同域的。
server {
listen 80;
server_name your-domain.com;
client_max_body_size 0;
# 前端静态资源
location / {
root /data/www/vod-front;
try_files $uri $uri/ /index.html;
}
# 后端 API 反向代理
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 视频文件直出
location /uploads/ {
alias /data/vod/uploads/;
add_header Cache-Control "public, max-age=86400";
types {
video/mp4 mp4;
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
}
}
client_max_body_size 0 表示不限制请求体大小,分片上传时每个分片 5MB,如果 Nginx 默认的 1m 没有改掉,分片传到一半会被直接断开,返回 413。 try_files $uri $uri/ /index.html 是 Vue Router 用 history 模式的刚需配置:前端路由可能长这样 /video/detail/100 ,服务器上不存在这个路径,Nginx 需要把所有路径都指向 index.html ,由前端路由自己解析。 alias /data/vod/uploads/ 的路径末尾要带斜杠,不带的话访问 /uploads/abc.mp4 会拼成 /data/vod/uploadsabc.mp4 ,直接 404。
types 块里声明了 m3u8 和 ts 的类型,浏览器才能正确识别为 HLS 流媒体而不是当成下载文件。 Cache-Control 只给视频文件加了一天缓存,因为视频文件上传后不会频繁变动,但更新封面或重新转码时又需要尽快生效,一天是比较折中的方案。
5.3 部署完成后的验证清单
nginx -t 检查配置语法无误后, systemctl reload nginx 重新加载,然后按这个清单验证:
curl -I http://your-server/ # 预期返回 200,Content-Type 是 text/html
curl -I http://your-server/api/video/list # 预期返回 200,说明代理生效
curl -I http://your-server/uploads/demo.mp4 # 预期返回 200,视频文件能直出
curl -I 只请求响应头,不下载整个文件,验证效率高。 /api/video/list 这个接口如果要求登录,返回 401 反而是正常的,说明请求确实到达了后端,没被 Nginx 拦下。如果上传和播放都正常了,还剩一件事要做:检查系统里默认的管理员账号。大多数开源视频点播系统的初始账号是 admin/admin123 或 admin/123456 ,这类弱口令在公网环境下等同于裸奔。上线前改掉管理员密码、停掉注册接口(如果不需要对外开放)都比堆业务功能更紧急。最后去 application.yml 或管理后台设置里确认转码后的 HLS 分片有多长时间:分片越短,拖动进度条时需要等待的加载时间越短,但分片数量变多、存储开销变大。要调整就三处一起配合分成段时长,转码拼接逻辑里改一个 segment_time 参数,再重新转一遍视频,拖动播放入口验证效果。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)