简介:一套基于Django框架开发的视频点播网站完整源码,面向计算机、数学、电子信息等专业学生,适合作为课程设计、期末大作业或毕业设计参考项目。项目已实现视频播放、收藏、后台管理等功能模块,代码结构清晰,可直接下载运行,同时留有二次开发空间,便于理解Django MTV架构、路由映射、模型操作与前后端交互逻辑。压缩包共178个文件,体积14.56MB,主要包含49个Python源码文件、47个HTML模板、40个编译后的pyc文件,以及JS、CSS、图片等静态资源,覆盖视图、模板、静态文件等完整站点组成,还包含少量演示视频与图标素材。已有125人学习下载,可作为同类项目起步参考。通过源码可学习用户登录、视频列表与播放页渲染、收藏功能实现、admin后台配置等关键知识,适合有一定Python基础、希望快速搭建功能完整视频站点的学习者参考借鉴。
1. 用 Django 搭视频点播站:播放链路比后台管理更值得拆
拿到“基于django框架搭建的视频点播网站源码”这类压缩包时,大部分人第一反应是解压、配环境、跑起来,但真正值得看的是播放链路:Django 负责业务编排,视频文件本身并不适合直接塞给模板渲染。一个能用于生产的视频点播站,至少要拆成数据模型、文件处理、播放分发、后台管理四层,而 Django 在第四层的 admin 和第二层的数据建模上优势明显,第三层要交给 ffmpeg 与 Nginx 补位。下面按这条主线,把播放、收藏、后台管理三个功能逐个落到可复现代码上,适合正在做内部培训系统、课程站或私有媒体库的 Django 开发者。
2. 视频模型与收藏关系:Django ORM 的核心建模
2.1 三张表:Video、Category、Favorite 的字段取舍
视频点播站的业务实体不算多,但字段设计直接影响后续转码和播放。常见的做法是拆出 Category、Video、Favorite 三张表,其中 Video 表要把“业务字段”和“文件字段”分开:title、summary、status 属于业务,video_file、cover_image、duration 属于文件。status 字段用 IntegerField 配合 choices,比直接用 CharField 更省空间,也方便在 Admin 里做筛选。
from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField(max_length=50, unique=True) sort = models.IntegerField(default=0) class Meta: ordering = ['sort', 'id'] def __str__(self): return self.name class Video(models.Model): STATUS_CHOICES = ( (0, '草稿'), (1, '转码中'), (2, '已上架'), (3, '已下架'), ) category = models.ForeignKey(Category, on_delete=models.PROTECT) title = models.CharField(max_length=120, db_index=True) summary = models.TextField(blank=True) video_file = models.FileField(upload_to='videos/%Y/%m/') cover_image = models.ImageField(upload_to='covers/%Y/%m/', blank=True) duration = models.IntegerField(default=0, help_text='秒') status = models.IntegerField(choices=STATUS_CHOICES, default=0, db_index=True) views_count = models.PositiveIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-created_at'] def __str__(self): return self.title class Favorite(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='favorites') created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'video')代码逻辑说明:category 用 PROTECT 防止误删分类把视频一起删掉;video_file 的 upload_to 按年月分目录,避免单目录文件过多;Favorite 的 unique_together 是收藏业务的关键,它把“重复收藏”从数据库层面挡掉,接口层就不用再做一次 exists 判断。
2.2 存储选型:本地 MEDIA_ROOT 与云存储的关键参数
models 里的 FileField 只保存路径,文件实际落盘位置由 MEDIA_ROOT 决定。开发阶段直接指向项目内 media 目录最简单,但生产环境建议用云存储或独立文件服务器,原因在表格里看得更清楚。
| 对比项 | 本地磁盘 MEDIA_ROOT | 云存储 OSS/COS/S3 |
|---|---|---|
| 读取速度 | 快,无网络开销 | 依赖地域节点,需配 CDN |
| 扩容 | 需手动加盘 | 按量付费,自动扩展 |
| 转码集成 | 本机 ffmpeg 直接读路径 | 需先下载或绑定文件目录 |
| 成本 | 磁盘固定成本 | 流量费用偏高 |
如果打算用云存储,FileField 要换成 django-storages 对应的 storage 后端,字段本身不用动。我一般会把 MEDIA_URL 和 MEDIA_ROOT 单独抽到 local_settings 或环境变量里,避免源码里写死路径。转码机与 Web 机分离时,本地磁盘方案要求共享存储(NFS 或对象存储),否则 ffmpeg 找不到源文件。
2.3 迁移与查询优化:select_related 与收藏状态集合
模型建好后先 makemigrations 再 migrate,这一步人人都会,但查询优化常被忽略。视频列表页如果直接Video.objects.all(),模板里每访问一次 category.name 就会多发一条 SQL,这就是教科书级的 N+1 问题。
from django.db.models import Prefetch def video_list(request): videos = Video.objects.select_related('category').filter(status=2) favorites = Favorite.objects.filter(user=request.user.id) if request.user.is_authenticated else Favorite.objects.none() fav_video_ids = set(favorites.values_list('video_id', flat=True)) return render(request, 'video/list.html', { 'videos': videos, 'fav_video_ids': fav_video_ids, })select_related 适用于 ForeignKey,会用一次 JOIN 把 category 带出来;收藏的查询用 values_list 取 video_id 集合,是为了在模板里快速判断“当前用户是否已收藏”,比循环里逐个判断省太多。注意 select_related 不适用于 ManyToMany 和反向外键,那些场景要换 Prefetch。
3. 播放链路:ffmpeg 转 HLS、流媒体视图与防盗链
3.1 为什么不能直接放 mp4:HLS 切片的价值
本地网络里直接点 mp4 确实能播,但公网场景问题很明显:单文件 mp4 无法做到“边下边播”的精细控制,拖动进度条要服务端支持 Range 请求;不同带宽用户只能共用一个码率。HLS 把视频切成 6-10 秒的 ts 分片,再生成 m3u8 索引,播放器按需拉片,配合多码率就能做自适应。Django 本身不做转码,ffmpeg 是这一层的主角。
常见的工作流是:后台上传原片 → 任务队列触发 ffmpeg → 输出 HLS 目录 → 更新 Video.status。很多人直接把 ffmpeg 放进视图同步跑,小文件还好,一个 2GB 的视频会把 worker 阻塞十几分钟,必须拆出去。没有 Celery 时,最简单的是丢给后台线程或直接 systemd 定时扫描。
3.2 ffmpeg 转码命令与码率档位
转码前先约定目录结构,我常用的约定是media/hls/{video_id}/index.m3u8,切完片之后把 video_file 更新成相对路径。核心命令如下:
ffmpeg -i input.mp4 \ -profile:v baseline -level 3.0 \ -hls_time 10 -hls_list_size 0 \ -hls_segment_filename "media/hls/12/seg_%04d.ts" \ -f hls media/hls/12/index.m3u8参数说明:-profile:v baseline是为了兼容旧手机和低端播放器;-hls_time 10控制每个分片时长,太短会增加请求数,太长拖动延迟变大;-hls_list_size 0表示在 m3u8 里保留所有分片,默认只保留最近几个分片,点播场景必须写成 0。码率档位按清晰度调整:
| 清晰度 | 视频码率 | 音频码率 | 建议 hls_time |
|---|---|---|---|
| 360P | 800k | 64k | 10 |
| 720P | 2500k | 128k | 10 |
| 1080P | 5000k | 192k | 8 |
转码完成后把 duration 也写上,ffprobe -show_format可以拿到精确时长。
ffprobe -v quiet -print_format json -show_format input.mp4 | jq .format.duration3.3 播放视图与 Range 请求:Django 的流式响应
播放页视图要同时做两件事:模板渲染播放器、提供 m3u8 和 ts 文件的流式响应。m3u8 文件很小,用普通 HttpResponse 即可;ts 分片和 mp4 这类媒体文件要支持断点续传,Django 3 内置的 FileResponse 已经实现了 Range 处理,直接用它比自己写 StreamingHttpResponse 更稳。
import os from django.http import FileResponse, Http404 from django.views.decorators.http import require_GET @require_GET def stream_video(request, video_id): video = get_object_or_404(Video, pk=video_id, status=2) file_path = video.video_file.path if not os.path.exists(file_path): raise Http404 response = FileResponse(open(file_path, 'rb'), content_type='video/mp4') response['Content-Disposition'] = 'inline' return response播放器模板里直接指向视图的 URL。要注意:video.video_file.path 依赖 MEDIA_ROOT 配置,云存储场景下 path 不可用,到那时要么先用脚本同步到本地,要么换重定向方案。
3.4 防盗链:时间戳签名 URL 的生成与校验
HLS 的 m3u8 和 ts 文件如果直接放在 media 目录下,别人拿到域名就能刷流量。常见的做法是在 URL 带上过期时间和签名,服务端先校验再决定是否放行。签名逻辑很简单:expire 时间戳 + 视频路径 + 密钥做 MD5。
import hashlib, time from django.conf import settings def sign_url(path, expire_secs=3600): expire = int(time.time()) + expire_secs sign = hashlib.md5(f"{path}{expire}{settings.MEDIA_SIGN_KEY}".encode()).hexdigest() return f"{path}?expire={expire}&sign={sign}"校验视图和播放视图合在一起:先验证 expire 是否过期、sign 是否匹配,再调用 FileResponse。注意签名过期时间不要设成 10 年,否则防盗链形同虚设;转码后的 HLS 分片建议也走同一套签名逻辑,否则播放器请求 ts 分片时会绕过校验。
4. 收藏接口与后台管理:Django Admin 的定制实践
4.1 收藏接口的幂等设计:POST 切换状态
前端按钮要做“收藏/取消收藏”切换,接口设计成 POST /api/favorite/{video_id}/toggle 最简单,返回当前状态。幂等性是这类接口的要点:快速连点两次,不能出现两条记录。数据库层的 unique_together 已经挡住重复,视图里再用 get_or_create 兜底。
from django.http import JsonResponse from django.views.decorators.http import require_POST from django.views.decorators.csrf import csrf_exempt from django.shortcuts import get_object_or_404 @require_POST def toggle_favorite(request, video_id): video = get_object_or_404(Video, pk=video_id) fav, created = Favorite.objects.get_or_create(user=request.user, video=video) if not created: fav.delete() return JsonResponse({'favorited': created, 'video_id': video_id})前端需要带 csrftoken,否则 Django 会拒绝 POST。简单的办法是在模板里加隐藏字段或者从 cookie 里用 JavaScript 取值,发 fetch 请求时加上 X-CSRFToken 头。上面的视图为了演示没写登录校验,真实项目里记得加 login_required。
4.2 后台管理:把 Admin 变成运营台,而不是默认列表
Django Admin 默认生成的表单很朴素,但对视频站点来说,真正要解决的是“运营同学快速找到视频并改状态”。list_display、list_filter、search_fields 这三个属性最常用:list_display 决定列显示,list_filter 让状态、分类在侧边栏直接筛,search_fields 对标题做模糊搜索。
from django.contrib import admin from .models import Video, Favorite class VideoAdmin(admin.ModelAdmin): list_display = ('title', 'category', 'status', 'views_count', 'created_at') list_filter = ('status', 'category') search_fields = ('title',) readonly_fields = ('views_count', 'created_at') actions = ['make_published'] @admin.action(description='上架选中视频') def make_published(self, request, queryset): queryset.update(status=2) admin.site.register(Video, VideoAdmin) admin.site.register(Favorite)readonly_fields 把容易误改的手工字段锁死,views_count 只允许通过代码累加;actions 是批量操作的人口,批量上架、下架在这种站点里比逐个编辑表单好使。收藏表注册进 Admin 的作用是排障:用户说“我收藏的视频不见了”,运营可以直接按用户查。
| 配置项 | 作用 | 适用场景 |
|---|---|---|
| list_display | 控制列表列 | 展示标题、状态、播放量 |
| list_filter | 侧边栏筛选 | 按状态、分类过滤 |
| search_fields | 模糊搜索 | 按标题找视频 |
| readonly_fields | 只读字段 | 播放量、创建时间 |
| actions | 批量操作 | 批量上下架 |
4.3 从 Admin 到转码任务入口
status 的四个状态里,转码中是异步任务写回的。Admin 列表只能看到结果,看不到过程。常见的改进是给 VideoAdmin 加一个自定义按钮,点击后手动触发转码任务;另一个增强是引入第三方后台美化主题,比如 simpleui 这类 django admin 界面美化方案,把状态标签做成高亮色块。这些都是锦上添花,核心还是把 status 流转规则定清楚:上传后置为 1,转码成功置为 2,ffmpeg 失败置为 1 并记录错误日志。
5. 部署到 Nginx:内部重定向、缓存参数与三个经典坑
5.1 X-Accel-Redirect:让 Nginx 读文件,别让 Django 读
生产环境把 MEDIA_ROOT 直接交给 Nginx 托管是最常见的做法,但防盗链会失效,因为静态资源的鉴权逻辑在 Django 里。折中方案是 Django 校验签名后返回 X-Accel-Redirect 头,Nginx 收到后自己读文件返回,流量不经过 Python 进程:
location /media/protected/ { internal; alias /data/var/media/; }Django 视图里,校验通过后设置response['X-Accel-Redirect'] = '/media/protected/' + rel_path,返回空响应即可。
提示:internal 目录不能和公开 media 目录重叠,否则用户可以直接绕过签名访问。
5.2 播放量缓存回写:防止数据库击穿
播放页读的次数远多于写的次数,views_count 每次都直接F('views_count')+1会频繁触发 UPDATE,量大了数据库压力明显。我常用的写法是:先放 Redis 累加,定时任务批量写回。
from django.core.cache import cache def record_view(video_id): key = f"video:views:{video_id}" try: cache.incr(key) except ValueError: cache.set(key, 1, timeout=3600)缓存要设置过期时间,防止冷视频的 key 永久占内存;列表页的“热门视频”建议缓存 60 秒,秒级过期会让 Redis 形同虚设。
5.3 三个经典坑和它们的观察点
坑一:上传 413。Nginx client_max_body_size 默认 1MB,传电影必挂,调到client_max_body_size 5g;,同时把 Django 的 DATA_UPLOAD_MAX_MEMORY_SIZE 也调大。
坑二:分片目录没有写权限。ffmpeg 用 nginx 用户跑会写不进 media/hls,表现为转码进程正常但目录是空的。检查目录属主比改代码更快。
坑三:收藏按钮快速双击,前端以为失败了。接口本身幂等,前端要做的只是请求合并或加 loading 状态。常见的验证手法是用 chrome network 面板模拟慢网络,观察是否发送了两个 POST。
最后的验证技巧:播放失败先看 Network 面板里 m3u8 请求返回码,401 是签名问题,404 是转码没完成,200 但播放器黑屏则优先怀疑 CORS 或分片 sec 参数。
本文还有配套的精品资源,点击获取