Django+Vue视频点播系统:HLS切片与播放器工程实践
简介:这是一套面向计算机专业本科生的毕业设计级视频点播网站实战项目,基于Python+Django后端与Vue前端构建,专为大作业、毕设开发及全栈能力训练设计。资源包含完整可运行源码、全部数据库文件及配套论文参考文档,已通过导师评审并获98分高分,代码经本地编译与多轮调试验证,开箱即用。压缩包共462个文件,涵盖44个核心Python后端模块(含models.py、urls.py等)、40个Vue组件页面、24个MP4视频样本及大量静态资源(JPEG/JPG/PNG/SVG/WEBP图像、TS流媒体切片、LESS样式与JSON配置),整体30.85MB,结构清晰、模块职责分明,便于学习MVC架构、前后端分离部署与视频点播业务逻辑。目前已有187人下载学习,附带.bak备份文件与.gitignore等工程规范文件,有助于理解开发迭代过程与项目工程化实践。
1. 这不是又一个“前后端分离模板”,而是一套能真实承载视频点播业务的 Django+Vue 工程骨架
你下载到手的「毕业设计-基于python+Django+vue的视频点播网站系统源码+全部数据」,表面看是学生作业,实则暗含一套完整视频服务链路的最小可行实现:Django 不只做 API,它要处理视频上传分片、HLS 切片触发、数据库元数据强一致性校验;Vue 不只是播放器容器,它需对接后端鉴权接口、动态加载 m3u8 清单、响应式适配不同分辨率流、处理播放失败重试与错误上报。这套源码的价值不在“能跑起来”,而在它把视频点播中容易被忽略的工程细节——比如 upload_to 路径与 Nginx 静态路由的映射关系、Vue 中 video.js 与 hls.js 的混合加载策略、Django FileField 在大文件上传时的内存溢出防护——全部固化为可调试、可替换、可审计的代码模块。适合两类人:一是正在用 Django 做音视频类毕设的学生,需要避开 django-ckeditor 那种通用组件在视频场景下的坑;二是中小团队想快速验证点播 MVP,不希望从零写 FFmpeg 调用或重造播放器轮子。它不承诺高并发,但保证每一步操作都有对应日志、每个 API 都带状态码说明、每份数据都经 django.core.validators 校验。
2. Django 后端:从视频上传到 HLS 切片的闭环控制逻辑
视频点播系统的后端核心不是 CRUD,而是对媒体文件生命周期的精准干预。本源码中 Django 的设计跳出了传统 CMS 模式,采用“上传即处理”策略:用户上传 MP4 文件后,系统不直接存入 media 目录,而是先落盘至临时区,再由 Celery 异步任务调用 FFmpeg 执行 HLS 切片,并将生成的 .m3u8 和 .ts 文件按约定路径组织,最后更新数据库中的 Video 模型字段。这种设计避免了前端播放器请求未就绪资源导致的 404,也规避了手动切片带来的路径管理混乱。
2.1 视频模型定义与存储路径强约束
models.py 中 Video 类的关键字段设计直指点播场景:
# models.py
from django.db import models
from django.core.validators import FileExtensionValidator
from django.conf import settings
import os
class Video(models.Model):
title = models.CharField(max_length=200, verbose_name="标题")
description = models.TextField(verbose_name="描述", blank=True)
# 使用自定义 upload_to,确保所有视频文件存入 videos/ 子目录
video_file = models.FileField(
upload_to='videos/original/',
validators=[FileExtensionValidator(allowed_extensions=['mp4', 'avi', 'mov'])],
verbose_name="原始视频文件"
)
# HLS 切片根目录,与 Nginx 静态路由严格对应
hls_path = models.CharField(
max_length=255,
verbose_name="HLS 目录路径",
help_text="如 videos/hls/{uuid}/index.m3u8,用于前端播放器拼接 URL"
)
duration = models.DurationField(verbose_name="时长", null=True, blank=True)
status = models.CharField(
max_length=20,
choices=[('pending', '待处理'), ('processing', '处理中'), ('ready', '就绪'), ('failed', '失败')],
default='pending',
verbose_name="处理状态"
)
created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
# 自动填充 hls_path 字段,格式固定为 videos/hls/{uuid}/index.m3u8
if not self.hls_path and self.pk:
self.hls_path = f"videos/hls/{self.pk}/index.m3u8"
super().save(*args, **kwargs)
注意 :
upload_to='videos/original/'并非随意设定。它与settings.MEDIA_ROOT共同构成物理路径:MEDIA_ROOT/videos/original/xxx.mp4。而hls_path字段值videos/hls/{pk}/index.m3u8是相对路径,最终被拼接到settings.MEDIA_URL(如/media/)后形成前端可访问的 URL:/media/videos/hls/123/index.m3u8。这个路径必须与 Nginx 的location /media/配置完全匹配,否则播放器无法加载.m3u8。
2.2 异步切片任务:Celery + FFmpeg 的可靠执行链
切片任务封装在 tasks.py 中,使用 subprocess.run 调用 FFmpeg,而非依赖 Python FFmpeg 封装库(如 ffmpeg-python ),原因在于其对错误码捕获更直接、对大文件内存占用更可控:
# tasks.py
from celery import shared_task
import subprocess
import os
from django.conf import settings
from .models import Video
@shared_task(bind=True, max_retries=3)
def process_video_hls(self, video_id):
try:
video = Video.objects.get(id=video_id)
if video.status != 'pending':
return {'status': 'skipped', 'reason': 'not pending'}
# 构建原始文件绝对路径
original_path = os.path.join(settings.MEDIA_ROOT, video.video_file.name)
# 构建 HLS 输出目录(自动创建)
hls_dir = os.path.join(settings.MEDIA_ROOT, 'videos', 'hls', str(video_id))
os.makedirs(hls_dir, exist_ok=True)
# FFmpeg HLS 切片命令(关键参数说明见下文)
cmd = [
'ffmpeg',
'-i', original_path,
'-codec: copy', # 复制音视频流,不重新编码,提速
'-start_number', '0',
'-hls_time', '10', # 每个 .ts 片段时长 10 秒
'-hls_list_size', '0', # 保留全部 .ts 片段,不滚动删除
'-hls_segment_filename', f'{hls_dir}/%05d.ts',
'-f', 'hls',
f'{hls_dir}/index.m3u8'
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) # 10 分钟超时
if result.returncode != 0:
raise Exception(f"FFmpeg failed: {result.stderr}")
# 更新数据库状态
video.status = 'ready'
video.hls_path = f"videos/hls/{video_id}/index.m3u8"
video.save()
return {'status': 'success', 'hls_url': f"/media/{video.hls_path}"}
except Exception as exc:
# 重试机制:失败后 60 秒后重试,最多 3 次
raise self.retry(exc=exc, countdown=60, max_retries=3)
FFmpeg 关键参数解析表
| 参数 | 值 | 作用 | 点播场景必要性 |
|---|---|---|---|
-codec: copy | — | 流复制,跳过解码/编码 | 避免 CPU 过载,保障上传后快速就绪 |
-hls_time | 10 | 单个 .ts 片段时长(秒) | 影响首屏加载时间与 CDN 缓存粒度,10 秒是平衡点 |
-hls_list_size | 0 | .m3u8 中保留的 .ts 条目数 | 0 表示无限保留,适合点播(非直播) |
-hls_segment_filename | %05d.ts | .ts 文件名格式 | 确保数字序号左补零(如 00001.ts ),避免播放器解析失败 |
提示 :
subprocess.run的timeout=600是硬性保护。若视频长达 2 小时,FFmpeg 可能因 I/O 或 CPU 限制造成超时。生产环境应监控celery worker日志中的Task timed out,并考虑增加--ulimit限制或拆分超长视频。
3. Vue 前端:m3u8 播放器集成与播放状态精细化控制
Vue 部分并非简单引入 video.js ,而是构建了一套与 Django 后端状态深度耦合的播放器组件。它不仅要展示视频,更要感知 Video.status 的变化(如从 processing 到 ready ),并在播放失败时主动触发重试逻辑,而非静默报错。
3.1 播放器组件: VideoPlayer.vue 的核心逻辑
该组件使用 hls.js 作为底层播放引擎(因 video.js 的 HLS 插件本质也是封装 hls.js ),并封装了状态监听、错误处理、加载提示三层能力:
<!-- VideoPlayer.vue -->
<template>
<div class="video-player">
<video ref="videoEl" class="video-js vjs-default-skin" controls></video>
<div v-if="loading" class="loading-overlay">正在加载视频...</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
</template>
<script>
import Hls from 'hls.js'
export default {
name: 'VideoPlayer',
props: {
videoId: {
type: Number,
required: true
}
},
data() {
return {
hls: null,
loading: false,
error: '',
videoUrl: ''
}
},
watch: {
videoId: {
handler(newId) {
this.loadVideo(newId)
},
immediate: true
}
},
methods: {
async loadVideo(id) {
this.loading = true
this.error = ''
this.destroyHls()
try {
// 步骤1:向 Django API 查询视频状态与 HLS URL
const res = await this.$http.get(`/api/videos/${id}/`)
const video = res.data
if (video.status !== 'ready') {
throw new Error(`视频尚未就绪,当前状态:${video.status}`)
}
this.videoUrl = `${this.$http.defaults.baseURL}media/${video.hls_path}`
// 步骤2:初始化 hls.js 实例
if (Hls.isSupported()) {
this.hls = new Hls({
capLevelToPlayerSize: true, // 根据播放器尺寸自动选择清晰度
maxBufferLength: 30, // 最大缓冲时长(秒),避免卡顿
enableWorker: true // 启用 Web Worker,减轻主线程压力
})
this.hls.loadSource(this.videoUrl)
this.hls.attachMedia(this.$refs.videoEl)
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.loading = false
this.$refs.videoEl.play() // 自动播放
})
} else if (this.$refs.videoEl.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生支持,直接设置 src
this.$refs.videoEl.src = this.videoUrl
this.$refs.videoEl.addEventListener('loadedmetadata', () => {
this.loading = false
this.$refs.videoEl.play()
})
}
} catch (err) {
this.loading = false
this.error = `加载失败:${err.message || '未知错误'}`
// 步骤3:失败后 5 秒自动重试(仅限网络错误,非状态错误)
if (err.message.includes('network') || err.message.includes('timeout')) {
setTimeout(() => this.loadVideo(id), 5000)
}
}
},
destroyHls() {
if (this.hls) {
this.hls.destroy()
this.hls = null
}
}
},
beforeUnmount() {
this.destroyHls()
}
}
</script>
播放器关键行为说明
- 状态驱动加载 :
loadVideo()首先调用/api/videos/{id}/获取status字段。若为pending或processing,立即抛出错误,前端显示“视频处理中,请稍候”,而非盲目请求.m3u8导致 404。 - 双引擎兼容 :优先使用
hls.js(Chrome/Firefox),降级到 Safari 原生application/vnd.apple.mpegurl支持,确保全平台可用。 - 智能缓冲与重试 :
maxBufferLength: 30防止低网速下过度缓冲;网络错误(非业务状态错误)触发 5 秒后自动重试,提升弱网体验。
3.2 视频列表页:与 Django Admin 的数据同步策略
列表页 ( VideoList.vue ) 不直接渲染 Video 模型全部字段,而是通过 axios 请求 /api/videos/?page=1 获取分页数据。其关键在于与 Django Admin 的 list_display 保持一致:
// api/video.js
export function getVideoList(params = {}) {
// params 包含 page, page_size, search 等,与 Django REST Framework 的 PageNumberPagination 对齐
return axios.get('/api/videos/', { params })
}
// VideoList.vue 中的请求
async fetchVideos() {
try {
const res = await getVideoList({ page: this.currentPage })
this.videos = res.data.results
this.total = res.data.count
} catch (err) {
this.$message.error('获取视频列表失败')
}
}
Django 后端 views.py 中对应的 API 视图:
# views.py
from rest_framework import generics, pagination
from .models import Video
from .serializers import VideoSerializer
class VideoPagination(pagination.PageNumberPagination):
page_size = 12
page_size_query_param = 'page_size'
max_page_size = 100
class VideoListView(generics.ListAPIView):
queryset = Video.objects.filter(status='ready').order_by('-created_at')
serializer_class = VideoSerializer
pagination_class = VideoPagination
# 支持按标题搜索
filter_backends = [SearchFilter]
search_fields = ['title', 'description']
提示 :
queryset中的filter(status='ready')是硬性过滤。它确保列表页只展示已成功切片的视频,避免用户点击“处理中”的条目导致播放器空转。这与 Django Admin 的list_filter = ['status']形成前后端一致的数据视图。
4. 全栈联调:Nginx 静态路由、跨域与部署参数调优
本地开发时,Vue 通过 npm run serve 启动在 http://localhost:8080 ,Django 运行在 http://localhost:8000 ,二者默认跨域。生产部署则需 Nginx 统一反向代理,将 /api/ 转发至 Django,将 /media/ 映射到 MEDIA_ROOT ,并将 Vue 打包后的静态文件直接由 Nginx 服务。这一环节的配置错误是“源码能跑,上线就 404”的主因。
4.1 Nginx 核心配置:静态文件与 API 的精确分流
以下配置片段来自 nginx.conf ,重点在于 location 块的优先级与路径匹配:
# nginx.conf
server {
listen 80;
server_name your-domain.com;
# 1. 优先匹配 /media/,直接返回静态文件(Django 不参与)
location /media/ {
alias /var/www/myproject/media/; # 必须以 / 结尾!
expires 1h;
add_header Cache-Control "public, immutable";
}
# 2. 匹配 /static/,服务 Vue 打包后的 JS/CSS
location /static/ {
alias /var/www/myproject/staticfiles/; # 与 Django collectstatic 目录一致
expires 1h;
}
# 3. 所有 /api/ 开头的请求,反向代理到 Django
location /api/ {
proxy_pass http://127.0.0.1:8000/;
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;
# 关键:传递原始请求路径,Django 的 URL 路由才能匹配
proxy_redirect off;
}
# 4. 兜底:所有其他请求(/、/about 等)返回 Vue 的 index.html
location / {
root /var/www/myproject/dist; # Vue build 后的输出目录
try_files $uri $uri/ /index.html;
}
}
配置要点解析
| 配置项 | 值 | 为什么必须如此 |
|---|---|---|
location /media/ 的 alias | /var/www/myproject/media/ | alias 会 完全替换 匹配路径, /media/videos/hls/123/index.m3u8 → /var/www/myproject/media/videos/hls/123/index.m3u8 。若误用 root ,路径会变成 /var/www/myproject/media//media/videos/hls/123/index.m3u8 ,多一层 /media 导致 404。 |
proxy_pass 末尾的 / | http://127.0.0.1:8000/ | 有 / 表示截断 location 匹配部分。 /api/videos/ → http://127.0.0.1:8000/videos/ 。若无 / ,则变为 http://127.0.0.1:8000/api/videos/ ,Django URL 无法匹配。 |
try_files $uri $uri/ /index.html | — | Vue Router 的 history 模式要求:当用户直接访问 /video/123 时,Nginx 需返回 index.html ,由 Vue Router 解析路径。此行确保 SPA 路由正确。 |
4.2 Django 生产环境关键设置
settings.py 中的 DEBUG=False 会禁用 Django 的静态文件服务,因此 MEDIA_URL 和 STATIC_URL 必须与 Nginx 的 location 完全一致:
# settings.py (生产环境)
DEBUG = False
ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com']
# 静态文件(CSS/JS)由 Nginx 服务,Django 只负责收集
STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/myproject/staticfiles/' # collectstatic 目标目录
# 媒体文件(视频、图片)由 Nginx 服务
MEDIA_URL = '/media/'
MEDIA_ROOT = '/var/www/myproject/media/' # 与 Nginx alias 路径一致
# 安全相关(生产必备)
SECURE_HSTS_SECONDS = 31536000
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
注意 :
MEDIA_ROOT的路径/var/www/myproject/media/必须与 Nginxlocation /media/的alias值 完全相同 (包括结尾斜杠)。任何差异都会导致.m3u8文件 404,播放器黑屏。
5. 排查视频无法播放的 5 个关键检查点
当 Vue 播放器显示黑屏或报错 Failed to load resource 时,不要急于重装依赖或修改代码。按以下顺序逐项验证,90% 的问题可定位:
5.1 检查点 1:Nginx 是否真正生效并重载
运行 sudo nginx -t 验证配置语法,再执行 sudo systemctl reload nginx 。常见错误是修改了 nginx.conf 但忘记 reload ,导致旧配置仍在运行。验证方法: curl -I http://your-domain.com/media/videos/hls/123/index.m3u8 ,应返回 200 OK 及 Content-Type: application/vnd.apple.mpegurl 。若返回 404 ,说明 Nginx 未正确映射 /media/ 。
5.2 检查点 2:Django 数据库中 Video.hls_path 的值是否合法
登录 Django Admin 或执行 python manage.py shell :
>>> from myapp.models import Video
>>> v = Video.objects.get(id=123)
>>> v.hls_path
'videos/hls/123/index.m3u8' # ✅ 正确:相对路径
>>> v.status
'ready' # ✅ 必须为 ready
若 hls_path 为空或包含绝对路径(如 /var/www/... ),说明 save() 方法未被调用或逻辑有误。
5.3 检查点 3:FFmpeg 切片文件是否真实生成
SSH 登录服务器,检查物理路径:
# 进入 MEDIA_ROOT
cd /var/www/myproject/media/
# 查看 videos/hls/123/ 目录是否存在且有文件
ls -la videos/hls/123/
# 应看到:index.m3u8, 00000.ts, 00001.ts, ...
# 若目录为空或不存在,说明 Celery 任务未执行或 FFmpeg 失败
5.4 检查点 4:浏览器开发者工具 Network 标签页中的请求链
打开播放页面,F12 → Network → Filter m3u8 :
- 第一个请求:
/media/videos/hls/123/index.m3u8→ 应为200,Response 内容为文本,包含#EXTM3U和#EXTINF行。 - 后续请求:
/media/videos/hls/123/00000.ts→ 应为200,Response 为二进制数据。 - 若
.m3u8返回200但.ts返回404,检查index.m3u8文件内容中的00000.ts路径是否与 Nginxalias下的真实文件名一致(注意大小写、扩展名)。
5.5 检查点 5:Django 日志中的 Celery 任务状态
查看 Celery worker 日志(通常在 /var/log/celery/worker.log ):
- 搜索
process_video_hls关键字。 - 若看到
Task process_video_hls[xxx] succeeded,说明切片成功。 - 若看到
Task process_video_hls[xxx] raised exception,后面紧跟FFmpeg failed: ...,则复制该错误信息到服务器执行相同 FFmpeg 命令,复现并解决环境问题(如缺少 libx264 编码器)。
终极技巧 :在 Django Shell 中手动触发切片任务,绕过 Celery,快速验证 FFmpeg 环境:
>>> from myapp.tasks import process_video_hls >>> process_video_hls(123) # 直接同步执行,错误会立刻抛出此方法能瞬间区分问题是出在 FFmpeg 环境、Django 逻辑,还是 Celery 配置。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)