Django视频点播系统实战:数据模型、上传转码与播放进度全链路
发布时间:2026/9/25 1:51:57来源:尧图网络
简介这是一套面向高校计算机及相关专业学生的视频点播网站系统完整方案采用Python与Django框架开发适用于毕业设计、课程设计及学期综合实践等学术场景帮助学习者理解Web应用开发流程与视频内容管理系统的实现原理。资源包共156个文件以48个html页面模板、40个py后端源码、20个js脚本、5个css样式表及6个png图片为主另含数据库文件与操作指南压缩包约3.89MB下载后可直接部署运行无需额外调整代码或配置环境。目前已有82人学习关注。内容涵盖环境搭建、数据初始化、功能测试与基础定制等关键环节模块划分清晰、结构规范便于按目录逐步掌握Django后端架构与视频点播业务的结合方式适合希望将理论知识落地为工程实践、并需要完整参考方案的学习者使用。1. 视频点播网站系统从播放页反推 Django 该建哪几张表很多人做视频点播网站系统第一反应是打开播放器组件文档研究怎么把 m3u8 塞进 video 标签。我一开始也这样结果页面能播了后台却一团乱视频信息散在 JSON 文件里分类靠手写死用户看到一半刷新就丢进度。真正让项目立住的不是播放器而是 Django 侧的数据模型和请求链路。这个标题讲的是用 Python 与 Django 搭一套完整的视频点播网站附带源码、数据集和开发教程常见于毕业设计场景。它解决的核心问题是把视频上传、转码、分类、播放、记录观看进度串成一条可维护的链路而不是拼一个能播的静态页。适合会一点 Python、想拿一个完整项目练手或交毕业设计的人也适合想看清 Django 在媒体类项目里边界的老手。下面按「模型怎么定 → 上传与转码怎么做 → 播放页怎么取流 → 坑在哪 → 怎么验证」推。2. 视频点播系统的数据模型五张表撑起上传、分类与播放记录2.1 为什么不用一个 Video 表塞完所有字段新手最容易犯的错是把视频标题、分类名、上传者昵称、播放地址全塞进一张 Video 表。这样写起来快但改一个分类名要遍历所有视频行删一个用户要手动清理他的视频播放进度更是没地方放。Django 的 ORM 本来就鼓励拆表拆开之后查询反而更清晰。我一般会拆成五张核心表Category分类、Video视频主体、VideoFile转码后的多清晰度文件、WatchRecord观看进度、Comment评论可选。Category 和 Video 是一对多Video 和 VideoFile 是一对多因为一个视频通常有 360P、720P 两个文件。WatchRecord 关联用户和视频记录秒数。# models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(max_length50, uniqueTrue) sort models.IntegerField(default0) # 控制前台排序 def __str__(self): return self.name class Video(models.Model): title models.CharField(max_length200) desc models.TextField(blankTrue) cover models.ImageField(upload_tocovers/) # 封面图 category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue) uploader models.ForeignKey(User, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) is_published models.BooleanField(defaultFalse) # 转码完成才发布 class Meta: ordering [-created_at] class VideoFile(models.Model): video models.ForeignKey(Video, related_namefiles, on_deletemodels.CASCADE) quality models.CharField(max_length10) # 360p / 720p file_path models.CharField(max_length255) # 相对 MEDIA_ROOT 的路径 size models.BigIntegerField(default0) class WatchRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) video models.ForeignKey(Video, on_deletemodels.CASCADE) seconds models.IntegerField(default0) # 上次看到第几秒 updated_at models.DateTimeField(auto_nowTrue) class Meta: unique_together (user, video) # 一人一视频一条记录逻辑说明Video 用 is_published 控制前台可见避免转码没完成就露出播放页。VideoFile 用 related_namefiles模板里可以video.files.all直接遍历清晰度。WatchRecord 的 unique_together 保证同一用户对同一视频只有一条进度更新时用 update_or_create。参数说明cover 用 ImageField需要 Pillowfile_path 存相对路径而不是完整 URL换域名或换存储时不用改数据seconds 用 IntegerField 足够一部电影按秒算不会溢出。2.2 迁移与后台注册的最小命令模型写完先建迁移再迁移最后注册到 admin这样不用写前端就能录数据。python manage.py makemigrations video python manage.py migrate python manage.py createsuperuser# admin.py from django.contrib import admin from .models import Category, Video, VideoFile, WatchRecord admin.register(Video) class VideoAdmin(admin.ModelAdmin): list_display (title, category, uploader, is_published, created_at) list_filter (category, is_published) search_fields (title,) admin.site.register([Category, VideoFile, WatchRecord])逻辑说明list_display 让后台一眼看到关键字段list_filter 按分类和发布状态筛search_fields 支持标题搜索。这三项是后台可用性的底线不加的话数据一多就翻不动。参数说明makemigrations 后面跟 app 名避免误伤其他 appcreatesuperuser 的账号只用于后台不要和前台用户混用同一张表逻辑。3. 上传与转码把大文件拆成可播放的分片3.1 上传接口为什么不能直接收整个文件视频文件动辄几百 MB直接走 Django 的 form 上传会卡在 request 体解析上超时和内存暴涨是常态。常见做法是前端分片上传后端用 Django 的 FileField 接收分片后合并或者干脆把上传交给对象存储的直传Django 只存路径。毕业设计环境里没有对象存储我一般用分片合并。# views.py import os from django.conf import settings from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt csrf_exempt def upload_chunk(request): if request.method ! POST: return JsonResponse({code: 405}) chunk request.FILES.get(chunk) index int(request.POST.get(index)) total int(request.POST.get(total)) file_id request.POST.get(file_id) # 前端生成的唯一标识 tmp_dir os.path.join(settings.MEDIA_ROOT, tmp, file_id) os.makedirs(tmp_dir, exist_okTrue) with open(os.path.join(tmp_dir, str(index)), wb) as f: for part in chunk.chunks(): f.write(part) if index total - 1: # 最后一片到达合并 final_path os.path.join(settings.MEDIA_ROOT, raw, f{file_id}.mp4) os.makedirs(os.path.dirname(final_path), exist_okTrue) with open(final_path, wb) as out: for i in range(total): with open(os.path.join(tmp_dir, str(i)), rb) as part: out.write(part.read()) return JsonResponse({code: 0, path: fraw/{file_id}.mp4}) return JsonResponse({code: 0, msg: chunk received})逻辑说明每个分片按 index 存成独立文件最后一片到达时按顺序合并。file_id 由前端生成保证同一文件的分片落在同一目录。合并完成后返回相对路径后续转码任务拿这个路径。参数说明chunk.chunks() 是 Django 推荐的读文件方式避免一次性读入内存tmp 目录要定期清理否则磁盘会被分片撑满total 由前端传后端要校验 index 范围防止越界写文件。3.2 转码任务用 subprocess 调 ffmpeg 的稳妥写法合并完的 mp4 不一定适合网页播放常见做法是转成 H.264 AAC 的 mp4再按清晰度切两份。Django 里不要在主请求里跑 ffmpeg会阻塞 worker我一般丢到后台线程或 Celery。毕业设计用 threading 就够。import subprocess import threading from .models import Video, VideoFile def transcode(video_id, raw_path): video Video.objects.get(idvideo_id) for quality, scale in [(360p, 640:-2), (720p, 1280:-2)]: out_name f{video_id}_{quality}.mp4 out_path os.path.join(settings.MEDIA_ROOT, media, out_name) cmd [ ffmpeg, -i, raw_path, -vf, fscale{scale}, -c:v, libx264, -preset, fast, -crf, 23, -c:a, aac, -b:a, 128k, -movflags, faststart, # 关键让 moov 前置支持边下边播 out_path ] subprocess.run(cmd, checkTrue) VideoFile.objects.create( videovideo, qualityquality, file_pathfmedia/{out_name}, sizeos.path.getsize(out_path) ) video.is_published True video.save() def start_transcode(video_id, raw_path): t threading.Thread(targettranscode, args(video_id, raw_path)) t.start()逻辑说明scale 里的 -2 让高度按比例自动算且保持偶数libx264 不认奇数高度。faststart 把 moov box 移到文件头浏览器才能边下边播不加这个参数播放页会等整个文件下完才出画面这是血泪经验。参数说明crf 23 是画质和体积的平衡点数值越小越清晰也越大preset fast 在毕业设计机器上够用slow 会明显拖长转码时间-b:a 128k 对讲解类视频足够。4. 播放页与进度记录前端取流、后端存秒4.1 播放页怎么拿到多清晰度地址播放页的视图负责把 Video 和它的 VideoFile 一起取出来模板里根据清晰度渲染多个 source。注意 MEDIA_URL 的拼接不要硬编码域名。# views.py from django.shortcuts import render, get_object_or_404 from .models import Video, WatchRecord def play(request, video_id): video get_object_or_404(Video, idvideo_id, is_publishedTrue) files video.files.all().order_by(quality) record None if request.user.is_authenticated: record WatchRecord.objects.filter(userrequest.user, videovideo).first() return render(request, play.html, { video: video, files: files, start_seconds: record.seconds if record else 0, })逻辑说明get_object_or_404 带 is_publishedTrue未发布的视频直接 404避免猜 id 访问。start_seconds 传给模板播放器初始化时 seek 到上次位置。参数说明files 按 quality 排序360p 在前播放器默认选第一个如果要做清晰度切换前端监听 change 事件换 source 并保留 currentTime。4.2 用 fetch 定时上报观看进度进度上报不要每秒发一次太浪费。我一般每 15 秒发一次页面隐藏时再补一次。// play.html 内联脚本 const videoEl document.getElementById(player); const videoId {{ video.id }}; let lastSent 0; function report() { const sec Math.floor(videoEl.currentTime); if (sec lastSent) return; lastSent sec; fetch(/api/watch/, { method: POST, headers: {Content-Type: application/json, X-CSRFToken: getCookie(csrftoken)}, body: JSON.stringify({video_id: videoId, seconds: sec}) }); } setInterval(report, 15000); document.addEventListener(visibilitychange, () { if (document.hidden) report(); // 切走时补一次 });逻辑说明lastSent 去重避免暂停时重复发相同秒数。visibilitychange 在用户切标签或关页时补报减少进度丢失。参数说明CSRF token 从 cookie 取Django 默认开启 CSRF 保护接口返回 204 即可前端不处理响应体。后端接口用 update_or_create 落库login_required def save_watch(request): data json.loads(request.body) WatchRecord.objects.update_or_create( userrequest.user, video_iddata[video_id], defaults{seconds: data[seconds]} ) return HttpResponse(status204)逻辑说明update_or_create 命中 unique_together存在就更新秒数不存在就新建。参数说明video_id 要校验存在性否则外键会抛异常。5. 避坑与排查视频点播项目里最容易翻车的五件事5.1 现象播放页一直转圈控制台报 206 但没画面原因ffmpeg 转码时没加 -movflags faststartmoov box 在文件尾部浏览器要下完整个文件才能解析。解决转码命令补上该参数已转码的文件用ffmpeg -i in.mp4 -c copy -movflags faststart out.mp4快速修复不用重新编码。5.2 现象上传大文件到一半 502原因Nginx 的 client_max_body_size 默认 1M分片虽然小但合并请求或直传会超。解决在 Nginx 配置里调大该值同时 Django 侧 DATA_UPLOAD_MAX_MEMORY_SIZE 也要相应调整。分片方案下每个分片控制在 5M 以内最稳。5.3 现象static 里的封面图在模板里显示不出来原因开发模式下 MEDIA_URL 没有配路由Django 不会自动服务媒体文件。解决在 urls.py 里加static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)仅开发环境用生产交给 Nginx。这个坑在 vscode 里写 img 标签时特别隐蔽因为路径看着没错。5.4 现象转码线程跑完数据库里却没有 VideoFile原因threading 里抛了异常但没打印subprocess.run 的 checkTrue 让 ffmpeg 非零退出直接中断。解决在 transcode 外层包 try/except 并 logging.exception同时确认 ffmpeg 在 PATH 里服务器上经常没装或版本太老不认 -movflags。5.5 现象观看进度偶尔回退到 0原因update_or_create 并发时后到的旧秒数覆盖了新秒数或者前端在 seek 时触发了上报。解决后端加判断只有新秒数大于已存秒数才更新前端在 seeking 事件里暂停上报。6. 验证与进阶用一条命令确认整条链路通了项目搭完怎么确认上传、转码、播放、进度四段都通我习惯写一个 management command模拟一次完整流程不依赖浏览器。# management/commands/smoke_test.py from django.core.management.base import BaseCommand from video.models import Video, VideoFile, WatchRecord from django.contrib.auth.models import User import os class Command(BaseCommand): def handle(self, *args, **options): user User.objects.first() video Video.objects.filter(is_publishedTrue).first() assert video, 没有已发布视频先跑一次上传转码 files video.files.all() assert files.count() 1, VideoFile 为空转码没落库 for f in files: full os.path.join(settings.MEDIA_ROOT, f.file_path) assert os.path.exists(full), f文件缺失: {full} assert f.size 0, f文件大小为 0: {full} WatchRecord.objects.update_or_create( useruser, videovideo, defaults{seconds: 42} ) rec WatchRecord.objects.get(useruser, videovideo) assert rec.seconds 42, 进度写入失败 self.stdout.write(self.style.SUCCESS(链路 OK))逻辑说明断言覆盖四段——视频已发布、VideoFile 有记录、物理文件存在且非空、进度能写能读。任何一段断了都会在对应 assert 停住比手动点页面快得多。参数说明settings.MEDIA_ROOT 要在文件顶部 importsmoke_test 只读不写业务数据可以反复跑。进阶方向有两个。一是把进度上报换成 WebSocketDjango Channels 可以做但毕业设计里 polling 足够别为了炫技引入复杂度。二是转码换成异步队列Celery Redis 是标准做法但本地跑 Celery 在 Windows 上有一堆兼容问题我一般建议先跑通 threading 版本再考虑迁移。我自己的习惯是每加一个功能先跑 smoke_test再开浏览器点一遍。这个顺序帮我省了很多「页面看着对、数据其实没落库」的后悔药。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网