Django视频点播系统:上传转码HLS播放全流程实战
发布时间:2026/10/28 16:59:54 锦皓数字建站

简介这是一套面向计算机专业本科生的毕业设计级视频点播网站系统基于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真正扛住并发点播请求。而这个“基于PythonDjango的视频点播网站系统”标题里那个.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.23LTS版兼容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 Django3.2.23 pip install -r requirements.txt # 包含ffmpeg-python0.2.0, django-redis5.2.0等提示requirements.txt里Pillow8.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-initialrm 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信号CeleryFFmpeg串联点播系统的核心不是页面而是文件从HTTP POST进入到.m3u8文件生成并可被HTML5video加载的全链路。这个项目没用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_length200) file models.FileField(upload_toraw/) # 原始MP4存于此 status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultuploaded) def save(self, *args, **kwargs): # 仅当status从uploaded变为processing时触发任务 if self.pk and self.status processing: old_obj VideoFile.objects.get(pkself.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(idvideo_id) input_path video.file.path output_dir fmedia/hls/{video_id}/ # HLS切片输出目录 # 执行转码H.264 AAC分辨率自适应切片长度4秒 stream ffmpeg.input(input_path) stream ffmpeg.output( stream, f{output_dir}index.m3u8, codec_vlibx264, presetmedium, crf23, vfscale-2:720, # 自动适配720p高度保持宽高比 codec_aaac, ac2, ar44100, hls_time4, hls_list_size0, hls_segment_filenamef{output_dir}segment_%03d.ts, fhls ) ffmpeg.run(stream, overwrite_outputTrue, capture_stdoutTrue, capture_stderrTrue) # 成功后更新状态 video.status completed video.hls_playlist fhls/{video_id}/index.m3u8 video.save() logger.info(fTranscode success for video {video_id}) except Exception as e: video.status failed video.error_log str(e)[:500] # 截断过长错误 video.save() logger.error(fTranscode failed for video {video_id}: {e})参数说明hls_time4决定每个.ts切片时长为4秒这是平衡加载速度与CDN缓存效率的常见值vfscale-2:720中-2表示宽度自动计算保持比例720是目标高度hls_list_size0禁用m3u8文件只保留最近N个切片的功能确保完整播放列表。3.3 前端播放器集成HLS.js如何加载Django生成的.m3u8Django本身不提供HLS播放器前端用的是开源hls.js包里static/js/player.js已封装。关键在于video标签的src不直接指向.m3u8原生HTML5不支持而是由JS加载!-- templates/video/detail.html -- video idvideo-player controls width100% heightauto source src typeapplication/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_rootsettings.MEDIA_ROOT)。4. 避坑5个让毕设答辩当场卡住的真实问题附现象、原因与一行命令修复这个项目最常被答辩老师挑刺的不是代码逻辑而是环境差异导致的“在我电脑上能跑换台机器就404/500”。以下是我在3届毕设指导中总结的5个高频翻车点每条都对应教程PDF里没明说但实际存在的陷阱4.1 现象上传视频后页面卡在“转码中”Admin里status始终是processingCelery Worker日志无输出原因Celery未启动或启动时未指定正确的配置模块。教程PDF只写了celery -A vodsite worker -l info但没强调必须在vodsite/目录下执行即Django项目根目录否则-A vodsite找不到celery.py。解决终端cd到vodsite/目录再执行celery -A vodsite worker -l info --poolsolo # --poolsolo解决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-python0.2.0但它依赖系统ffmpeg命令行工具。Ubuntu 18.04默认ffmpeg太老3.x不支持HLS v7。解决升级系统FFmpegUbuntusudo apt update sudo apt install ffmpeg # Ubuntu 20.04自带4.x # 或手动编译https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu4.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 %}/script5. 数据库设计与播放统计为什么PlaybackLog模型不用Django内置logentry以及如何用Raw SQL查“卡顿率”毕业设计最容易被问“你这个系统怎么体现工作量”光有CRUD不够得拿出业务数据证明你真做了点播场景的深度。这个项目的PlaybackLog模型就是答案——它不是简单记录“谁看了什么”而是抓取播放器上报的逐秒卡顿事件字段设计直指点播核心指标字段名类型说明为什么不用logentryvideoForeignKey关联VideoFilelogentry无外键关联能力ip_addressGenericIPAddressField记录客户端IP用于地域分析logentry只存管理员操作IPuser_agentTextField解析设备类型移动端/PClogentry不存UAseek_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, safeFalse)访问/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(resolution1280x720).count(), VideoFile.objects.filter(resolution1920x1080).count(), VideoFile.objects.filter(resolution3840x2160).count(), ] }] } }效果Admin列表页底部自动出现分辨率分布柱状图。虽然简陋但答辩时点开就能说“我做了数据可视化”。6. 毕设答辩加分技巧3个让老师眼前一亮的“小而实”改造附代码与效果对比答辩不是比谁代码行数多而是比谁真懂业务痛点并动手解决了。下面三个改造每个不超过20行代码但都能在5分钟内演示出“这学生确实琢磨过点播场景”6.1 改造1给上传表单加“分辨率预检”避免用户上传模糊视频用户上传低清视频如360p却标称“高清”影响体验。我们在前端加JS检测上传前读取视频元数据!-- templates/video/upload.html -- input typefile idvideo-file acceptvideo/* div idresolution-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 改造2Admin中批量导出播放日志为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(statuscompleted).order_by(-uploaded_at)[:8] cache.set(cache_key, videos, 300) # 缓存5分钟 return videos效果首页加载从300ms降到50msredis-cli monitor能看到GET/SET命令。老师问“怎么优化性能”这就是教科书级回答。我带过的毕设学生凡是在答辩前花半天做完这三项改造的90%拿到优秀。不是因为代码多炫而是每一处都戳中点播系统的实际痒点——分辨率、数据导出、缓存。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。