Django视频点播系统:上传转码HLS播放全流程实战

简介:这是一套面向计算机专业本科生的毕业设计级视频点播网站系统,基于Python 3.x与Django 3.x框架开发,完整覆盖用户管理、视频上传、分类检索、在线播放、评论互动等核心功能,适合作为毕业设计、课程设计或期末大作业项目,具备开箱即用特性,无需额外配置即可本地运行。资源包共129个文件,包含40个Python后端逻辑文件(含models、views、urls等模块)、48个HTML前端模板(如index.html、detail.html、profile.html等)、20个JavaScript交互脚本、6个PNG图标及1个JPG封面图,辅以CSS样式文件与基础配置文件,整体压缩包仅3.25MB,轻量且结构清晰。已有268人下载学习,资源附带配套教程文档与完整初始化数据,涵盖数据库迁移命令、管理员账号预置方案及常见部署问题说明,便于快速理解Django MTV架构实践与前后端协同逻辑。

1. 这不是又一个“Django博客模板”:它真能跑通视频点播全流程,从用户上传、转码、切片到HLS播放,连数据库里存的都是真实MP4元数据和播放统计

你搜“Django毕业设计”,刷出来的90%是带后台管理的图书借阅或学生成绩系统——界面漂亮,但没碰过文件上传瓶颈、没处理过FFmpeg进程卡死、没在Nginx里配过location /media/的缓存头、更没让Django ORM真正扛住并发点播请求。而这个“基于Python+Django的视频点播网站系统”,标题里那个.zip包里塞进的,是一套可离线部署、含真实视频样本、带完整转码流水线、且所有模型字段都按点播场景定制的工程。它不教你怎么写admin.py,而是直接给你models.py里定义了VideoFile(含duration_sec,bitrate_kbps,resolution,hls_status等12个业务字段)、PlaybackLog(记录IP、UA、seek位置、卡顿秒数)和TranscodeTask(状态机驱动,支持重试与失败告警)。适合两类人:一是大四学生赶毕设答辩,需要三天内搭出能演示“上传→自动转HLS→网页播放→看播放日志”的闭环;二是刚转Web开发的Python工程师,想用真实业务倒逼自己搞懂Django文件处理边界、异步任务调度、以及为什么FileField不能直接存视频流。它不承诺“零配置上线”,但承诺每一步踩的坑,都在教程PDF第17页写了报错截图和ps aux | grep ffmpeg排查命令。


2. 从解压到首页可访问:5步走通本地开发环境搭建,重点在Python版本锁死与Media路径硬编码规避

这个.zip包的结构很典型:根目录下是vodsite/(Django项目)、media/(存放原始视频和HLS切片)、transcode/(FFmpeg脚本与配置)、docs/(PDF教程)和data/(SQLite3数据库备份+CSV样本数据)。别急着python manage.py runserver——第一步必须确认Python环境。教程明确要求Python 3.8.10(不是3.9+,因FFmpeg绑定库ffmpeg-python在3.9上存在avcodec_open2符号冲突),且Django版本锁死在3.2.23(LTS版,兼容django-crispy-forms和django-redis旧插件)。我一般会新建虚拟环境并强制指定:

# 创建隔离环境(关键:指定Python小版本) pyenv install 3.8.10 pyenv virtualenv 3.8.10 vod-env pyenv activate vod-env # 安装指定Django(注意:不是最新版!) pip install Django==3.2.23 pip install -r requirements.txt # 包含ffmpeg-python==0.2.0, django-redis==5.2.0等

提示:requirements.txt里Pillow==8.3.2是故意降级的——新版Pillow在Ubuntu 20.04上编译libjpeg时会因系统库版本不匹配报错,这是教程PDF第3页就预警的“玄学依赖”。

第二步,修改settings.py里的媒体路径。很多新手在这里翻车:他们把MEDIA_ROOT写成绝对路径如/home/user/vodsite/media/,结果Django启动后找不到/media/xxx.mp4。正确做法是用BASE_DIR动态拼接,并确保MEDIA_URL以/media/结尾(少斜杠会导致前端请求404):

# vodsite/settings.py import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent # 注意:向上三级才是.zip解压根目录 MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 指向解压后的/media/文件夹 MEDIA_URL = '/media/' # 必须以/开头和结尾

第三步,初始化数据库。包里data/db.sqlite3是已填充的示例库(含3个测试视频、2个用户、15条播放日志),但直接替换db.sqlite3会因Django迁移记录不一致导致migrate失败。我的血泪经验是:先删掉默认的空库,再复制示例库,最后执行python manage.py migrate --fake-initial:

rm vodsite/db.sqlite3 cp data/db.sqlite3 vodsite/ python manage.py migrate --fake-initial # 关键:跳过初始迁移,因示例库已含表结构

第四步,收集静态文件。Django默认不自动服务静态资源,而点播站的播放器JS、CSS必须加载。执行:

python manage.py collectstatic --noinput

这会把static/下所有文件拷到STATIC_ROOT(教程设为vodsite/staticfiles/),后续Nginx需代理此路径。

第五步,启动服务并验证。运行:

python manage.py runserver 0.0.0.0:8000

打开浏览器访问http://127.0.0.1:8000,应看到首页轮播图和“最新上传”视频列表;访问http://127.0.0.1:8000/admin,用教程PDF第5页写的默认账号(admin/vod123456)登录,能看到VideoFile模型里已存在sample_720p.mp4记录,且hls_status字段为completed——说明HLS切片已就绪。


3. 视频上传→自动转码→HLS分片:三阶段流水线如何用Django信号+Celery+FFmpeg串联

点播系统的核心不是页面,而是文件从HTTP POST进入,到.m3u8文件生成并可被HTML5<video>加载的全链路。这个项目没用Django Channels做实时通知,而是用更稳的“信号触发+异步任务”模式:用户上传视频后,Django的post_save信号触发Celery任务,Celery Worker调用FFmpeg执行转码与切片。整个流程在vodsite/video/tasks.py里定义,关键逻辑如下:

3.1 上传后触发转码任务:VideoFile模型的save()方法埋点

VideoFile模型继承自models.Model,其save()方法被重写,当status从uploaded变为processing时,发送Celery任务:

# vodsite/video/models.py from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from .tasks import start_transcode_task class VideoFile(models.Model): STATUS_CHOICES = [ ('uploaded', '已上传'), ('processing', '转码中'), ('completed', '转码完成'), ('failed', '转码失败'), ] title = models.CharField(max_length=200) file = models.FileField(upload_to='raw/') # 原始MP4存于此 status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='uploaded') def save(self, *args, **kwargs): # 仅当status从uploaded变为processing时触发任务 if self.pk and self.status == 'processing': old_obj = VideoFile.objects.get(pk=self.pk) if old_obj.status == 'uploaded': start_transcode_task.delay(self.id) # 异步调用 super().save(*args, **kwargs)

参数说明:start_transcode_task.delay(self.id)中的self.id是数据库主键,Celery任务通过它查出VideoFile实例,读取file.path获取原始视频路径。delay()确保任务不阻塞HTTP响应,用户上传后页面立即返回“已提交,转码中”。

3.2 Celery Worker执行FFmpeg命令:转码参数与错误捕获

tasks.py里的start_transcode_task函数,本质是拼接并执行一条FFmpeg命令。它不直接调用subprocess.run(),而是用ffmpeg-python库封装,好处是能捕获stderr流并写入Django日志:

# vodsite/video/tasks.py import ffmpeg import logging from celery import shared_task from .models import VideoFile logger = logging.getLogger(__name__) @shared_task def start_transcode_task(video_id): try: video = VideoFile.objects.get(id=video_id) input_path = video.file.path output_dir = f"media/hls/{video_id}/" # HLS切片输出目录 # 执行转码:H.264 + AAC,分辨率自适应,切片长度4秒 stream = ffmpeg.input(input_path) stream = ffmpeg.output( stream, f"{output_dir}index.m3u8", codec_v='libx264', preset='medium', crf=23, vf='scale=-2:720', # 自动适配720p高度,保持宽高比 codec_a='aac', ac=2, ar=44100, hls_time=4, hls_list_size=0, hls_segment_filename=f"{output_dir}segment_%03d.ts", f='hls' ) ffmpeg.run(stream, overwrite_output=True, capture_stdout=True, capture_stderr=True) # 成功后更新状态 video.status = 'completed' video.hls_playlist = f"hls/{video_id}/index.m3u8" video.save() logger.info(f"Transcode success for video {video_id}") except Exception as e: video.status = 'failed' video.error_log = str(e)[:500] # 截断过长错误 video.save() logger.error(f"Transcode failed for video {video_id}: {e}")

参数说明:hls_time=4决定每个.ts切片时长为4秒,这是平衡加载速度与CDN缓存效率的常见值;vf='scale=-2:720'中-2表示宽度自动计算(保持比例),720是目标高度;hls_list_size=0禁用m3u8文件只保留最近N个切片的功能,确保完整播放列表。

3.3 前端播放器集成:HLS.js如何加载Django生成的.m3u8

Django本身不提供HLS播放器,前端用的是开源hls.js(包里static/js/player.js已封装)。关键在于<video>标签的src不直接指向.m3u8(原生HTML5不支持),而是由JS加载:

<!-- templates/video/detail.html --> <video id="video-player" controls width="100%" height="auto"> <source src="" type="application/x-mpegURL"> </video> <script> const video = document.getElementById('video-player'); const videoSrc = "{{ video.hls_playlist|default:'' }}"; // 从Django模板传入 if (Hls.isSupported() && videoSrc) { const hls = new Hls(); hls.loadSource(`/media/${videoSrc}`); // 注意:/media/前缀必须加 hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play(); // 加载完成后自动播放 }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // Safari原生支持 video.src = `/media/${videoSrc}`; video.addEventListener('loadedmetadata', () => { video.play(); }); } </script>

注意:/media/${videoSrc}拼接后是类似/media/hls/123/index.m3u8的URL,这要求Nginx或Django开发服务器能正确路由/media/路径。若用runserver,需在settings.py中添加urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)。


4. 避坑:5个让毕设答辩当场卡住的真实问题,附现象、原因与一行命令修复

这个项目最常被答辩老师挑刺的,不是代码逻辑,而是环境差异导致的“在我电脑上能跑,换台机器就404/500”。以下是我在3届毕设指导中总结的5个高频翻车点,每条都对应教程PDF里没明说但实际存在的陷阱:

4.1 现象:上传视频后页面卡在“转码中”,Admin里status始终是processing,Celery Worker日志无输出

原因:Celery未启动,或启动时未指定正确的配置模块。教程PDF只写了celery -A vodsite worker -l info,但没强调必须在vodsite/目录下执行(即Django项目根目录),否则-A vodsite找不到celery.py。
解决:终端cd到vodsite/目录,再执行:

celery -A vodsite worker -l info --pool=solo # --pool=solo解决Windows下eventlet不兼容问题

4.2 现象:HLS播放器显示“Network Error”,浏览器开发者工具Network标签页看到index.m3u8返回404

原因:MEDIA_URL配置错误。常见错误是写成MEDIA_URL = 'media/'(缺开头斜杠)或MEDIA_URL = '/media'(缺结尾斜杠),导致前端请求/media/hls/123/index.m3u8变成/mediahls/123/index.m3u8。
解决:检查settings.py,确保MEDIA_URL = '/media/'(两端都有斜杠),并重启Django服务。

4.3 现象:FFmpeg报错Unable to find a suitable output format for 'xxx/index.m3u8'

原因:ffmpeg-python版本与系统FFmpeg二进制不匹配。包里requirements.txt指定ffmpeg-python==0.2.0,但它依赖系统ffmpeg命令行工具。Ubuntu 18.04默认ffmpeg太老(3.x),不支持HLS v7。
解决:升级系统FFmpeg(Ubuntu):

sudo apt update && sudo apt install ffmpeg # Ubuntu 20.04+自带4.x+ # 或手动编译:https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu

4.4 现象:Admin后台上传视频时,选择文件后点击“保存”报错RequestEntityTooLarge

原因:Django开发服务器默认限制POST大小为2.5MB,而视频文件远超此值。这不是代码bug,是服务器配置缺失。
解决:在settings.py顶部添加:

# vodsite/settings.py DATA_UPLOAD_MAX_MEMORY_SIZE = 26214400 # 25MB FILE_UPLOAD_MAX_MEMORY_SIZE = 26214400 # 同上

4.5 现象:播放页面空白,控制台报错Uncaught ReferenceError: Hls is not defined

原因:hls.js未正确加载。包里static/js/hls.min.js存在,但base.html中引用路径写成<script src="{% static 'js/hls.min.js' %}"></script>,而实际文件在static/hls/hls.min.js(教程PDF第12页截图有误)。
解决:修改templates/base.html,将引用改为:

<script src="{% static 'hls/hls.min.js' %}"></script>

5. 数据库设计与播放统计:为什么PlaybackLog模型不用Django内置logentry,以及如何用Raw SQL查“卡顿率”

毕业设计最容易被问“你这个系统怎么体现工作量?”,光有CRUD不够,得拿出业务数据证明你真做了点播场景的深度。这个项目的PlaybackLog模型就是答案——它不是简单记录“谁看了什么”,而是抓取播放器上报的逐秒卡顿事件,字段设计直指点播核心指标:

字段名类型说明为什么不用logentry
videoForeignKey关联VideoFilelogentry无外键关联能力
ip_addressGenericIPAddressField记录客户端IP(用于地域分析)logentry只存管理员操作IP
user_agentTextField解析设备类型(移动端/PC)logentry不存UA
seek_position_secFloatField用户拖拽到的时间点(秒)logentry无业务上下文字段
buffering_duration_secFloatField单次卡顿持续时间(秒)logentry不记录性能指标
total_played_secFloatField本次播放总时长(秒)logentry不记录行为时长

提示:buffering_duration_sec > 0即为一次卡顿事件。卡顿率 = 卡顿次数 / 总播放次数,但更科学的是“卡顿时长占比”:SUM(buffering_duration_sec) / SUM(total_played_sec)。

5.1 用Raw SQL查“TOP 5卡顿率最高视频”

Django ORM对聚合计算支持有限,尤其涉及分组后二次计算(如卡顿率=卡顿时长/总时长)。这时直接写Raw SQL更清晰:

# vodsite/video/views.py from django.db import connection from django.http import JsonResponse def get_top_stutter_videos(request): with connection.cursor() as cursor: cursor.execute(""" SELECT v.title, COUNT(*) as play_count, SUM(CASE WHEN p.buffering_duration_sec > 0 THEN 1 ELSE 0 END) as stutter_count, ROUND(SUM(p.buffering_duration_sec) * 100.0 / NULLIF(SUM(p.total_played_sec), 0), 2) as stutter_rate_pct FROM video_playbacklog p JOIN video_videofile v ON p.video_id = v.id GROUP BY v.id, v.title ORDER BY stutter_rate_pct DESC LIMIT 5 """) rows = cursor.fetchall() data = [ { "title": row[0], "play_count": row[1], "stutter_count": row[2], "stutter_rate_pct": row[3] } for row in rows ] return JsonResponse(data, safe=False)

访问/api/top-stutter/即可返回JSON,前端用ECharts画柱状图。注意NULLIF(SUM(p.total_played_sec), 0)防止除零错误——这是生产环境必须加的防御性写法。

5.2 在Admin中嵌入播放统计图表:用django-admin-chart轻量集成

不想写前端?Django Admin支持内嵌图表。安装django-admin-chart(已包含在requirements.txt),在admin.py中注册:

# vodsite/video/admin.py from django.contrib import admin from .models import VideoFile, PlaybackLog from admin_chart import AdminChartMixin @admin.register(VideoFile) class VideoFileAdmin(AdminChartMixin, admin.ModelAdmin): list_display = ['title', 'duration_sec', 'hls_status'] chart_config = { 'type': 'bar', 'data': { 'labels': ['720p', '1080p', '4K'], 'datasets': [{ 'label': '视频数量', 'data': [ VideoFile.objects.filter(resolution='1280x720').count(), VideoFile.objects.filter(resolution='1920x1080').count(), VideoFile.objects.filter(resolution='3840x2160').count(), ] }] } }

效果:Admin列表页底部自动出现分辨率分布柱状图。虽然简陋,但答辩时点开就能说“我做了数据可视化”。


6. 毕设答辩加分技巧:3个让老师眼前一亮的“小而实”改造,附代码与效果对比

答辩不是比谁代码行数多,而是比谁真懂业务痛点并动手解决了。下面三个改造,每个不超过20行代码,但都能在5分钟内演示出“这学生确实琢磨过点播场景”:

6.1 改造1:给上传表单加“分辨率预检”,避免用户上传模糊视频

用户上传低清视频(如360p)却标称“高清”,影响体验。我们在前端加JS检测,上传前读取视频元数据:

<!-- templates/video/upload.html --> <input type="file" id="video-file" accept="video/*"> <div id="resolution-info"></div> <script> document.getElementById('video-file').onchange = function(e) { const file = e.target.files[0]; if (!file) return; const url = URL.createObjectURL(file); const video = document.createElement('video'); video.onloadedmetadata = function() { const res = `${video.videoWidth}x${video.videoHeight}`; document.getElementById('resolution-info').innerText = `检测分辨率:${res}(建议720p及以上)`; URL.revokeObjectURL(url); }; video.src = url; }; </script>

效果:用户选文件瞬间显示分辨率,比“上传完再报错”体验好太多。老师会问“你怎么想到这个?”,答:“点播站第一印象是画质,不能让用户传完才发现糊”。

6.2 改造2:Admin中批量导出播放日志为Excel,用django-import-export

答辩常被问“数据怎么导出分析?”。django-import-export已预装,只需在admin.py加两行:

# vodsite/video/admin.py from import_export.admin import ExportActionModelAdmin from .resources import PlaybackLogResource @admin.register(PlaybackLog) class PlaybackLogAdmin(ExportActionModelAdmin): # 继承ExportActionModelAdmin resource_class = PlaybackLogResource # 定义导出字段 list_display = ['video', 'ip_address', 'buffering_duration_sec']

resources.py定义导出范围:

# vodsite/video/resources.py from import_export import resources from .models import PlaybackLog class PlaybackLogResource(resources.ModelResource): class Meta: model = PlaybackLog fields = ('video__title', 'ip_address', 'user_agent', 'buffering_duration_sec', 'created_at')

效果:Admin列表页多出“导出”按钮,点开选Excel格式,一键下载带表头的CSV/Excel。老师要数据?3秒搞定。

6.3 改造3:用Redis缓存热门视频封面,降低数据库压力

首页“最新上传”列表每次请求都查DB,其实封面图(VideoFile.cover_image)变化极少。加Redis缓存:

# vodsite/video/views.py from django.core.cache import cache from django.views.generic import ListView class HomePageView(ListView): model = VideoFile template_name = 'home.html' context_object_name = 'videos' def get_queryset(self): cache_key = 'homepage_videos' videos = cache.get(cache_key) if videos is None: videos = VideoFile.objects.filter(status='completed').order_by('-uploaded_at')[:8] cache.set(cache_key, videos, 300) # 缓存5分钟 return videos

效果:首页加载从300ms降到50ms,redis-cli monitor能看到GET/SET命令。老师问“怎么优化性能?”,这就是教科书级回答。

我带过的毕设学生,凡是在答辩前花半天做完这三项改造的,90%拿到优秀。不是因为代码多炫,而是每一处都戳中点播系统的实际痒点——分辨率、数据导出、缓存。希望帮到你。

本文还有配套的精品资源,点击获取