☰
Django视频点播系统实战:大文件上传、HLS转码与动态鉴权
2026/10/7 10:57:40 网站建设 项目流程

简介:这是一套基于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视频上传、转码、分片播放和用户鉴权——毕设级系统里最常被低估的硬骨头

很多同学搜“python+Django视频点播网站 毕设”,点开就看到个带首页、登录页、视频列表页的静态页面,后台连个Video.objects.all()都没写全,更别说上传后自动转码、HLS切片、防盗链、播放进度同步这些真实场景刚需。这不是毕设“能跑就行”,而是毕设“要像生产环境一样稳”。我去年帮3个本科生调这个项目,90%卡在视频文件落地后无法播放——不是前端<video>标签写错了,是 Django 的FileField默认不支持大文件流式上传,Nginx 没配client_max_body_size,FFmpeg 转码命令漏了-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的MP4,Python进程内存瞬间飙到1.2GB,服务器直接 OOM。毕设部署在学生云主机(1核2G)上,这根本不可行。
正确做法是前端分片上传 + 后端合并。我们不用第三方库(如 django-file-form),用原生django.core.files.uploadedfile.InMemoryUploadedFile+django.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'}, status=405) 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_ok=True) 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_ok=True) 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_ok=True)是血泪经验——很多同学漏掉exist_ok=True,第一次上传成功,第二次因目录已存在报FileExistsError;os.remove(part_path)必须在write()后立即执行,否则磁盘空间撑爆;final_path的路径拼接必须用os.path.join,Windows下反斜杠会直接让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_ok=True) # 关键参数说明: # -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_output=True, text=True, timeout=600) # 10分钟超时 if result.returncode != 0: raise Exception(f"FFmpeg failed: {result.stderr}") return f'hls/{os.path.splitext(video_path)[0]}/index.m3u8' except subprocess.TimeoutExpired: raise Exception("Transcoding timeout") except Exception as e: raise Exception(f"Transcoding error: {str(e)}") # 在 upload_chunk 成功合并后触发: # async_task('myapp.tasks.transcode_video', f'videos/{filename}')

参数深挖:-crf 23不是玄学——CRF(Constant 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 src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <video id="video-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:视频资源必须动态签名 URL

@login_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_in=3600): """生成带签名的媒体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(id=video_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/(?P<path>.*)$', 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_type=content_type) response["Content-Disposition"] = f'inline; filename="{os.path.basename(file_path)}"' return response

3.2 播放进度同步不是“存个数据库字段”:必须用 WebSocket 防止多端冲突

用户在手机上看了一半,回家用电脑继续看——毕设系统得记住进度。很多人用VideoProgress.objects.update_or_create(user=user, video=video, 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( user=self.user, video_id=video_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 检查点二:DEBUG=False下ALLOWED_HOSTS必须精确匹配域名/IP

本地DEBUG=True时一切正常,一改DEBUG=False,Django 直接返回DisallowedHost错误页。原因:ALLOWED_HOSTS = ['*']在生产环境被禁止(安全漏洞),必须写死:

# settings.py ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com', '123.45.67.89'] # 云服务器IP # ❌ 不能写 ['*'] 或 ['.your-domain.com'](点号开头表示子域名通配,但需配合 DNS)

4.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 # 查看将执行哪些 SQL

4.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,且755

sudo chown -R www-data:www-data /var/www/yourproject/media sudo chmod -R 755 /var/www/yourproject/media # 特别注意:上传的视频文件权限应为 644,目录为 755 # 如果是 600,Nginx 无法读取,返回 403

5. 毕设答辩前最后一关:用真实数据压测,验证三个核心指标是否达标

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 = f"test_{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核2G):

  • 10并发用户上传,平均响应时间 < 800ms
  • Nginxactive connections稳定在 15 以下(超过20开始排队)
  • htop观察 Python 进程内存 < 1.1GB

5.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是否设为 30

5.3 答辩演示话术:把技术难点转化成“我解决了什么问题”

不要说:“我用了 Django 和 FFmpeg”。要说:

“老师好,这个系统最核心的突破是解决了大文件上传与实时播放的矛盾。传统方案上传500MB视频要等10分钟,我的分片上传+后台转码,用户点上传后3秒内就能看到‘上传成功’,后台自动转码,2分钟后就能播放。而且每个视频链接带1小时时效签名,防止盗链——这是我在urls.py里重写了媒体路由,用 HMAC 签名+时间戳双重校验实现的。”

最后,我把整个项目的requirements.txt、nginx.conf样例、ffmpeg参数速查表整理成了一个压缩包,里面包含所有可直接运行的代码(含 SQLite 数据库初始数据),你解压后pip install -r requirements.txt,python manage.py migrate,python manage.py createsuperuser,再python manage.py runserver就能跑起来。
我当年毕设答辩前夜,就是靠反复跑这三遍压测、逐行核对 Nginx 日志、把ffmpeg命令粘贴到终端里手动执行十次,才敢第二天上台。希望这篇笔记,能帮你省下那宝贵的24小时。
希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询