简介:一套完整的视频点播系统源码,基于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 参数,再重新转一遍视频,拖动播放入口验证效果。

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

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐