Django视频点播系统实战:大文件上传、HLS转码与动态鉴权

发布时间:2026/10/7 10:58:00
Django视频点播系统实战:大文件上传、HLS转码与动态鉴权 简介这是一套基于Python与Django框架实现的视频点播网站系统完整源码专为本科毕业设计及Web全栈入门实践者打造可直接运行部署覆盖在线教育、短视频分享等典型业务场景。资源共128个文件包含40个核心Python后端逻辑文件含models、views、urls及admin配置、48个HTML模板如index.html、detail.html、profile.html等支撑多级页面渲染、20个JS交互脚本含dropload.js等增强用户体验、6个PNG/JPG图标资源及5个CSS样式文件含semantic.custom.css、admin.css等整体压缩包仅3.23MB轻量易上手。已有244人学习下载适合快速掌握Django MVC架构、用户认证、视频管理、评论互动与后台Admin定制等关键能力。读者可直接运行项目深入理解视频列表展示、播放详情页动态渲染、评论提交与审核、个人中心功能扩展等完整链路并参考清晰的目录结构与模块化代码组织方式开展二次开发。1. 这不是又一个“Django博客模板”它真能扛住1080P视频上传、转码、分片播放和用户鉴权——毕设级系统里最常被低估的硬骨头很多同学搜“pythonDjango视频点播网站 毕设”点开就看到个带首页、登录页、视频列表页的静态页面后台连个Video.objects.all()都没写全更别说上传后自动转码、HLS切片、防盗链、播放进度同步这些真实场景刚需。这不是毕设“能跑就行”而是毕设“要像生产环境一样稳”。我去年帮3个本科生调这个项目90%卡在视频文件落地后无法播放——不是前端video标签写错了是 Django 的FileField默认不支持大文件流式上传Nginx 没配client_max_body_sizeFFmpeg 转码命令漏了-c:v libx264 -crf 23 -preset fast这几个关键参数连media/目录权限都忘了chown www-data:www-data。本篇不讲“Django怎么创建app”只拆解从用户点击“上传”按钮开始到浏览器里拖动进度条流畅播放1080P视频为止每一步谁在干活、为什么必须这么干、哪一步错了整个链路就断。适合正在写毕设、已卡在视频模块两周以上、查文档查到怀疑人生的同学——你缺的不是教程是有人把settings.py里MEDIA_ROOT和STATIC_ROOT的物理路径边界、views.py里UploadView的chunked upload实现逻辑、ffmpeg命令里-sc_threshold 0对关键帧对齐的影响全给你钉死在代码行上。2. 用 Django 构建视频点播服务的最小可行链路从文件上传到可播放 URL 的四层穿透2.1 为什么不能直接request.FILES[video]——大文件上传必须绕过 Django 默认表单机制Django 默认的request.FILES会把整个文件读进内存再存入TemporaryUploadedFile上传一个500MB的MP4Python进程内存瞬间飙到1.2GB服务器直接 OOM。毕设部署在学生云主机1核2G上这根本不可行。正确做法是前端分片上传 后端合并。我们不用第三方库如 django-file-form用原生django.core.files.uploadedfile.InMemoryUploadedFiledjango.core.files.storage.FileSystemStorage组合实现可控分片# views.py from django.http import JsonResponse from django.core.files.storage import FileSystemStorage from django.conf import settings import os import hashlib def upload_chunk(request): if request.method ! POST: return JsonResponse({error: Method not allowed}, status405) chunk request.FILES.get(chunk) filename request.POST.get(filename) chunk_index int(request.POST.get(chunkIndex)) total_chunks int(request.POST.get(totalChunks)) # 用文件名索引生成唯一临时路径避免并发冲突 temp_dir os.path.join(settings.MEDIA_ROOT, temp_uploads) os.makedirs(temp_dir, exist_okTrue) chunk_path os.path.join(temp_dir, f{filename}.part{chunk_index}) with open(chunk_path, wb) as destination: for chunk_part in chunk.chunks(): destination.write(chunk_part) # 检查是否所有分片已接收 received_chunks [f for f in os.listdir(temp_dir) if f.startswith(filename) and f.endswith(.part)] if len(received_chunks) total_chunks: # 合并分片 final_path os.path.join(settings.MEDIA_ROOT, videos, filename) os.makedirs(os.path.dirname(final_path), exist_okTrue) with open(final_path, wb) as final_file: for i in range(total_chunks): part_path os.path.join(temp_dir, f{filename}.part{i}) with open(part_path, rb) as part_file: final_file.write(part_file.read()) os.remove(part_path) # 立即清理 os.rmdir(temp_dir) # 清空临时目录 return JsonResponse({status: success, url: f/media/videos/{filename}}) return JsonResponse({status: uploading})注意这段代码里os.makedirs(..., exist_okTrue)是血泪经验——很多同学漏掉exist_okTrue第一次上传成功第二次因目录已存在报FileExistsErroros.remove(part_path)必须在write()后立即执行否则磁盘空间撑爆final_path的路径拼接必须用os.path.joinWindows下反斜杠会直接让nginx404。2.2 视频转码不是“装个FFmpeg就行”Django 后台任务必须与 Web 请求解耦Django 视图函数是同步阻塞的如果在upload_chunk返回前执行subprocess.run([ffmpeg, -i, input_path, ...])用户上传完要等3分钟1080P转码耗时期间整个 Django 进程卡死其他请求全部排队。毕设答辩现场演示时老师点上传按钮后盯着空白页30秒——这就是翻车现场。解耦方案只有两个可靠选择Celery 或 Django-Q。Celery 配置复杂需 Redis/RabbitMQ对毕设过于重Django-Q 更轻量且自带数据库队列一行pip install django-q即可启动# 安装后在 settings.py 中添加 INSTALLED_APPS [django_q] Q_CLUSTER { name: DjangORM, workers: 4, recycle: 500, timeout: 60, compress: True, save_limit: 250, queue_limit: 500, cpu_affinity: 1, label: Django Q, redis: { host: 127.0.0.1, port: 6379, db: 0, } }然后定义转码任务# tasks.py from django_q.tasks import async_task from django.core.files.storage import default_storage import subprocess import os from django.conf import settings def transcode_video(video_path): 转码核心逻辑输入原始MP4输出HLS切片 m3u8索引 input_full_path os.path.join(settings.MEDIA_ROOT, video_path) output_dir os.path.join(settings.MEDIA_ROOT, hls, os.path.splitext(video_path)[0]) os.makedirs(output_dir, exist_okTrue) # 关键参数说明 # -c:v libx264强制H.264编码兼容性最好 # -crf 23质量-体积平衡点低于20文件过大高于28画质崩坏 # -preset fast转码速度与压缩率折中veryfast太糙slow在毕设机上太慢 # -sc_threshold 0关闭场景切换检测确保关键帧严格按-g 25对齐否则HLS拖动卡顿 # -hls_time 10每10秒切一个TS片段太短增加HTTP请求数太长拖动延迟高 cmd [ ffmpeg, -i, input_full_path, -c:v, libx264, -crf, 23, -preset, fast, -sc_threshold, 0, -c:a, aac, -b:a, 128k, -hls_time, 10, -hls_list_size, 0, -hls_segment_filename, f{output_dir}/%05d.ts, f{output_dir}/index.m3u8 ] try: result subprocess.run(cmd, capture_outputTrue, textTrue, timeout600) # 10分钟超时 if result.returncode ! 0: raise Exception(fFFmpeg failed: {result.stderr}) return fhls/{os.path.splitext(video_path)[0]}/index.m3u8 except subprocess.TimeoutExpired: raise Exception(Transcoding timeout) except Exception as e: raise Exception(fTranscoding error: {str(e)}) # 在 upload_chunk 成功合并后触发 # async_task(myapp.tasks.transcode_video, fvideos/{filename})参数深挖-crf 23不是玄学——CRFConstant Rate Factor值越小画质越好但体积越大23是YouTube推荐的“视觉无损”起点-hls_time 10是平衡点小于5秒1080P视频每分钟产生12个TS文件Nginx日志爆炸大于15秒用户拖动进度条后要等15秒才加载新片段。这些数字必须实测不是抄来的。2.3 播放器不是video src...就完事HLS 必须用 hls.js且 Nginx 要配 MIME 类型Django 自带的开发服务器runserver不支持.m3u8和.ts文件的正确 MIME 类型浏览器直接下载文件而非播放。很多同学以为“本地能播上线就404”其实是 Nginx 没配# /etc/nginx/sites-available/your-site location /media/hls/ { alias /var/www/yourproject/media/hls/; add_header Content-Type application/vnd.apple.mpegurl; add_header Cache-Control no-cache; # 必须允许跨域否则 hls.js 加载失败 add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, OPTIONS; add_header Access-Control-Allow-Headers Range; } location /media/videos/ { alias /var/www/yourproject/media/videos/; add_header Content-Type video/mp4; }前端播放器必须用hls.js原生video不支持 HLS!-- template.html -- script srchttps://cdn.jsdelivr.net/npm/hls.jslatest/script video idvideo-player controls/video script const video document.getElementById(video-player); const videoSrc {{ hls_url }}; // 后端传入的 /media/hls/xxx/index.m3u8 if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(videoSrc); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () { video.play(); }); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 video.src videoSrc; video.addEventListener(loadedmetadata, () { video.play(); }); } /script提示add_header Access-Control-Allow-Headers Range是关键——HLS 分片加载依赖 HTTP Range 请求漏掉这行Chrome 控制台会报Failed to load resource: net::ERR_FAILED但错误信息完全不提 CORS。3. 用户鉴权与防盗链毕设最容易被忽略的“安全补丁”却决定你能否通过答辩3.1 Django 用户系统不能只靠login_required视频资源必须动态签名 URLlogin_required只能保护视图函数但/media/hls/xxx/index.m3u8是静态文件路径用户登录后拿到这个 URL可以发给任何人甚至分享到论坛——你的毕设视频就裸奔了。真实方案是所有媒体 URL 都带有时效签名过期即 403。# utils.py import hmac import time from django.conf import settings from urllib.parse import urlencode def generate_signed_url(path, expires_in3600): 生成带签名的媒体URL有效期1小时 expire int(time.time()) expires_in # 签名 HMAC-SHA256(密钥, path expire) signature hmac.new( settings.SECRET_KEY.encode(), f{path}{expire}.encode(), sha256 ).hexdigest()[:16] # 截取16位够用且URL短 return f{path}?expires{expire}signature{signature} # views.py 中返回播放URL时 def get_video_play_url(request, video_id): video Video.objects.get(idvideo_id) signed_url generate_signed_url(f/media/hls/{video.hls_folder}/index.m3u8) return JsonResponse({play_url: signed_url})Nginx 层验证签名这才是真正的防盗链# nginx 配置段 location /media/hls/ { alias /var/www/yourproject/media/hls/; # 提取 URL 参数 set $expires $arg_expires; set $signature $arg_signature; # 验证签名 if ($expires ) { return 403; } if ($signature ) { return 403; } if ($expires $time_iso8601) { return 403; } # 重新计算签名比对使用 nginx 的 hmac_sha256 set $to_sign $uri$expires; set $expected_signature ; # 注意此处需用 nginx 的 lua 模块或编译时启用 --with-http_secure_link_module # 毕设简化版用 Django 中间件做校验见下文 }避坑Nginx 原生secure_link模块需要编译支持学生机通常没装。毕设务实方案是用 Django 中间件拦截在urls.py中把/media/路由指向自定义视图而不是django.views.static.serve# urls.py from django.urls import re_path from . import views urlpatterns [ re_path(r^media/(?Ppath.*)$, views.signed_media_view), ]# views.py from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponse from django.conf import settings import hmac import time import os def signed_media_view(request, path): # 解析 URL 参数 expires request.GET.get(expires) signature request.GET.get(signature) if not all([expires, signature]): return HttpResponseForbidden() try: expires int(expires) except ValueError: return HttpResponseForbidden() if time.time() expires: return HttpResponseForbidden() # 重新计算签名 expected hmac.new( settings.SECRET_KEY.encode(), f/media/{path}{expires}.encode(), sha256 ).hexdigest()[:16] if not hmac.compare_digest(signature, expected): return HttpResponseForbidden() # 签名有效返回文件 file_path os.path.join(settings.MEDIA_ROOT, path) if not os.path.exists(file_path): return HttpResponseNotFound() # 根据扩展名设置 Content-Type content_type_map { .m3u8: application/vnd.apple.mpegurl, .ts: video/MP2T, .mp4: video/mp4, } ext os.path.splitext(file_path)[1].lower() content_type content_type_map.get(ext, application/octet-stream) with open(file_path, rb) as f: response HttpResponse(f.read(), content_typecontent_type) response[Content-Disposition] finline; filename{os.path.basename(file_path)} return response3.2 播放进度同步不是“存个数据库字段”必须用 WebSocket 防止多端冲突用户在手机上看了一半回家用电脑继续看——毕设系统得记住进度。很多人用VideoProgress.objects.update_or_create(useruser, videovideo, defaults{progress: seconds})但问题来了手机端每5秒存一次电脑端也每5秒存一次最后数据库里存的是哪个正确方案是用 Django Channels WebSocket 实时同步pip install channels redis# consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer from asgiref.sync import sync_to_async from .models import VideoProgress class ProgressConsumer(AsyncWebsocketConsumer): async def connect(self): self.user self.scope[user] if self.user.is_authenticated: await self.accept() else: await self.close() async def receive(self, text_data): data json.loads(text_data) video_id data[video_id] progress data[progress] # 异步写入数据库 await sync_to_async(self.save_progress)(video_id, progress) def save_progress(self, video_id, progress): VideoProgress.objects.update_or_create( userself.user, video_idvideo_id, defaults{progress: progress, updated_at: timezone.now()} )前端用 WebSocket 发送进度// 播放器监听时间更新 video.addEventListener(timeupdate, () { if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ video_id: {{ video.id }}, progress: Math.floor(video.currentTime) })); } });注意sync_to_async是必须的——Channels 的 Consumer 是异步的Django ORM 默认是同步的不包一层会报SynchronousOnlyOperation错误。这是 Django Channels 新手第一大坑。4. 部署上线前必做的五项硬核检查避开毕设答辩当天的“黑色五分钟”4.1 检查点一MEDIA_ROOT和STATIC_ROOT的物理路径绝对不能重叠很多同学在settings.py里写MEDIA_ROOT os.path.join(BASE_DIR, media) STATIC_ROOT os.path.join(BASE_DIR, media) # ❌ 错误结果python manage.py collectstatic把所有 CSS/JS 打包进media/而 Nginx 配置location /media/时用户访问/media/admin/css/base.css会直接下载 CSS 文件因为 Nginx 按alias返回了原始文件而不是渲染管理后台。正确配置# settings.py MEDIA_ROOT os.path.join(BASE_DIR, media) # 存用户上传的视频、图片 STATIC_ROOT os.path.join(BASE_DIR, staticfiles) # 存 collectstatic 合并后的静态文件 STATIC_URL /static/ MEDIA_URL /media/Nginx 配置分离location /static/ { alias /var/www/yourproject/staticfiles/; } location /media/ { alias /var/www/yourproject/media/; }4.2 检查点二DEBUGFalse下ALLOWED_HOSTS必须精确匹配域名/IP本地DEBUGTrue时一切正常一改DEBUGFalseDjango 直接返回DisallowedHost错误页。原因ALLOWED_HOSTS [*]在生产环境被禁止安全漏洞必须写死# settings.py ALLOWED_HOSTS [your-domain.com, www.your-domain.com, 123.45.67.89] # 云服务器IP # ❌ 不能写 [*] 或 [.your-domain.com]点号开头表示子域名通配但需配合 DNS4.3 检查点三数据库迁移必须在部署前完成且makemigrations不能有 unapplied migration答辩现场python manage.py migrate报错no module named xxx是因为INSTALLED_APPS里写了不存在的 app 名。检查命令python manage.py showmigrations # 查看哪些 migration 未应用 python manage.py makemigrations --dry-run # 检查是否会生成新 migration python manage.py migrate --plan # 查看将执行哪些 SQL4.4 检查点四ffmpeg版本必须 ≥ 4.2否则-hls_segment_filename参数不识别Ubuntu 18.04 自带ffmpeg 3.4运行转码命令报错Unrecognized option hls_segment_filename。升级命令sudo apt remove ffmpeg sudo add-apt-repository ppa:jonathonf/ffmpeg-4 sudo apt update sudo apt install ffmpeg ffmpeg -version # 确认输出 4.2.x 或更高4.5 检查点五media/目录权限必须为www-data:www-data且755sudo chown -R www-data:www-data /var/www/yourproject/media sudo chmod -R 755 /var/www/yourproject/media # 特别注意上传的视频文件权限应为 644目录为 755 # 如果是 600Nginx 无法读取返回 4035. 毕设答辩前最后一关用真实数据压测验证三个核心指标是否达标5.1 压测目标不是“能跑”而是“能扛住答辩现场突发流量”答辩当天老师、同学十几人同时点开你的网站每人上传一个200MB视频——这就是真实压力。用locust模拟# locustfile.py from locust import HttpUser, task, between import random class VideoUser(HttpUser): wait_time between(1, 3) task def upload_video(self): # 模拟上传分片 filename ftest_{random.randint(1000,9999)}.mp4 chunk_data b\x00 * 1024 * 1024 # 1MB 分片 # 第一步发送分片 self.client.post(/upload/chunk/, data{ filename: filename, chunkIndex: 0, totalChunks: 1, }, files{chunk: (chunk.bin, chunk_data)}) # 第二步触发转码实际项目中这步由后端自动触发 # self.client.post(/transcode/, json{video_id: 1})运行压测locust -f locustfile.py --host http://your-domain.com --users 10 --spawn-rate 2达标线学生云主机 1核2G10并发用户上传平均响应时间 800msNginxactive connections稳定在 15 以下超过20开始排队htop观察 Python 进程内存 1.1GB5.2 验证播放体验的三个硬指标首帧、拖动、卡顿率用 Chrome DevTools 的 Network 面板抓取 HLS 加载过程指标达标标准不达标表现排查方向首帧时间 3.5s页面加载后3秒黑屏检查index.m3u8是否被 Nginx 正确返回Content-Type、hls.js 是否加载成功、MANIFEST_PARSED事件是否触发拖动响应 1.2s拖动进度条后1秒以上才开始加载新TS检查 FFmpeg-hls_time 10是否生效、Nginx 是否缓存了.m3u8加add_header Cache-Control no-cache、TS文件是否真实存在卡顿率 5%连续播放10分钟内卡顿次数 ≤ 3次播放中频繁缓冲检查ffmpeg是否用了-preset fast太慢导致TS生成延迟、网络带宽是否足够学生机下行带宽通常 ≥ 50Mbps、hls.js的maxBufferLength是否设为 305.3 答辩演示话术把技术难点转化成“我解决了什么问题”不要说“我用了 Django 和 FFmpeg”。要说“老师好这个系统最核心的突破是解决了大文件上传与实时播放的矛盾。传统方案上传500MB视频要等10分钟我的分片上传后台转码用户点上传后3秒内就能看到‘上传成功’后台自动转码2分钟后就能播放。而且每个视频链接带1小时时效签名防止盗链——这是我在urls.py里重写了媒体路由用 HMAC 签名时间戳双重校验实现的。”最后我把整个项目的requirements.txt、nginx.conf样例、ffmpeg参数速查表整理成了一个压缩包里面包含所有可直接运行的代码含 SQLite 数据库初始数据你解压后pip install -r requirements.txtpython manage.py migratepython manage.py createsuperuser再python manage.py runserver就能跑起来。我当年毕设答辩前夜就是靠反复跑这三遍压测、逐行核对 Nginx 日志、把ffmpeg命令粘贴到终端里手动执行十次才敢第二天上台。希望这篇笔记能帮你省下那宝贵的24小时。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询