news 2026/9/12 2:17:58

Flask视频播放网站开发实战:数据库、Range流与Nginx部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flask视频播放网站开发实战:数据库、Range流与Nginx部署

简介:这是一份基于Flask框架的在线电影视频播放网站毕业设计源码,适合计算机相关专业学生用于毕设、课程设计或项目演示。网站前端采用HTML5与Bootstrap,后端使用Python3与Flask,数据库为MySQL,涵盖视频浏览、搜索筛选、登录注册、收藏评论等前台功能,后台支持对视频、用户、评论、轮播图、操作日志和管理员进行管理。压缩包共2000个文件,以JS、HTML、CSS等前端资源为主,另有Python源码、SQL数据库脚本及说明文档,整体大小21.02MB,结构清晰便于二次开发。资源附带了管理员与用户测试账号,代码已通过运行测试,答辩评审平均分达96分,已有550人学习下载。对于需要完成相似课题或学习Flask全栈开发的同学,这份源码能提供完整项目框架、配置思路与可直接运行的示例,具备较高参考价值。

1. Flask视频播放网站:别急着看代码,先想清楚视频怎么发出去

网上能搜到大量「Python Flask 电影网站源代码」的zip包,解压后结构基本一致:一个app.py、一个models.py、一个templates目录、一个sqlite数据库文件。文档说明写得比代码还长,但真正能拖动进度条、播放不卡的项目十个里挑不出两三个。这套东西的本质是毕业设计和数据库课程设计里的常客,核心难点从来不在增删改查,而是视频文件如何通过HTTP正确到达浏览器。Flask本身不擅长文件服务,但配合Range协议和Nginx就能把视频播放做得像模像样。这篇博文不负责帮你交作业,只帮你把这个zip里的每个模块落成自己能跑、能改、能上线的小型视频网站。

2. 数据库先行:电影信息、播放记录与用户体系如何落表

2.1 影视站的表结构,先分清主数据和行为数据

一个小型在线电影播放站的数据库远比想象中简单,主数据就三张:分类、电影、用户。行为数据是播放记录和评论。很多课程设计把字段堆进一张表里,导致后面做搜索、做分页、做播放记录时全糊在一起。

主数据的核心是电影表,字段里最容易忽略的是status。它控制着电影是否在前台展示,做上下架功能时不需要删行,改个数字就行。播放记录表要存「看到哪里」,用户下次进入时能从上次进度继续。评论表则绑定用户和电影两个外键。表与表之间的关系,决定了后面ORM建模时relationship的写法,也决定了查询时的性能。

2.2 Flask-SQLAlchemy模型定义,五张表的关系映射

# models.py from datetime import datetime from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy() class Category(db.Model): __tablename__ = 'category' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), unique=True, nullable=False) movies = db.relationship('Movie', backref='category', lazy='dynamic') class Movie(db.Model): __tablename__ = 'movie' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(128), nullable=False, index=True) # 标题建索引,搜索走索引 cover = db.Column(db.String(256)) video_path = db.Column(db.String(256), nullable=False) # 相对路径,不含域名 category_id = db.Column(db.Integer, db.ForeignKey('category.id')) summary = db.Column(db.Text) play_count = db.Column(db.Integer, default=0) status = db.Column(db.SmallInteger, default=1) # 1上架 0下架 created_at = db.Column(db.DateTime, default=datetime.now) class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) is_admin = db.Column(db.Boolean, default=False) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class PlayHistory(db.Model): __tablename__ = 'play_history' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) movie_id = db.Column(db.Integer, db.ForeignKey('movie.id')) progress = db.Column(db.Integer, default=0) # 播放进度,单位秒 updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)

这里用video_path而不是video_url,刻意区分一个概念:库里存相对路径,域名和前缀交给模板拼接。这样将来换存储目录、换CDN,不需要回改数据库。play_count字段在热门排行榜里要用到,它属于冗余计数,可以在播放视图里累加,也可以后台上架时初始化。lazy='dynamic'表示获取分类下的电影列表时返回一个查询对象,支持继续叠加过滤条件,而不是一次性把全部结果加载进内存。

2.3 从建库到初始化,数据库课程设计的三个命令

# 以 MySQL 为例,创建数据库 mysql -uroot -p -e "CREATE DATABASE movie_site DEFAULT CHARSET utf8mb4;" # 建表 python -c "from app import app, db; app.app_context().push(); db.create_all()" # 写入初始分类和测试电影 python seed.py

第一行建库时指定utf8mb4,是为了中文标题和简介不出乱码,这是数据库课程设计里最常扣分的点。第二行通过应用上下文建表,Flask 2.x必须在app_context()里执行create_all(),直接跑会报Working outside of application context。第三行的seed.py是初始化数据脚本,插入默认分类和几个测试电影,方便打开站点就有内容可看。数据库表结构建好后,接下来要解决的核心问题就是视频文件怎么发出去。

对比项SQLiteMySQL
初始化成本零配置,复制即用需要单独安装并建库
并发能力写并发差,单写多读读写并发稳定
备份部署备份一个文件即可需要mysqldump和权限管理
适合场景课程设计、单机演示多人访问的真实站点

3. 播放的核心:Flask如何把视频流送到浏览器

3.1 视频拖不动进度条,多半是Range请求没处理

浏览器里的<video>标签播放MP4时,不会一次性把整个文件拉完。它先发一个不带Range的请求探测文件大小,然后根据用户拖动进度条,用Range: bytes=1234-5678这种请求头向服务器要文件的某一段。服务器返回206 Partial Content和对应的数据段,播放器才能拖动跳转。

很多课程设计代码直接用send_file(path),文件小的时候一切正常,文件上到几百兆,播放器会卡在缓冲、进度条拖不动。所以视频播放视图要自己解析Range头。下面是完整实现:

# video_stream.py import os from flask import request, Response, abort, current_app def iter_file(path, start, end, chunk_size=65536): """按块读取文件区间 [start, end] 的数据""" with open(path, 'rb') as f: f.seek(start) remaining = end - start + 1 while remaining > 0: size = min(chunk_size, remaining) data = f.read(size) if not data: break yield data remaining -= len(data) def parse_range(range_header, file_size): """解析 Range 头,返回 (start, end),越界时限制到文件大小内""" if not range_header or not range_header.startswith('bytes='): return 0, file_size - 1 try: start_s, end_s = range_header[6:].split('-', 1) start = int(start_s) end = int(end_s) if end_s else file_size - 1 except (ValueError, KeyError): abort(416) if start >= file_size or end < start: abort(416) return start, min(end, file_size - 1) @app.route('/play/<int:movie_id>') def play_stream(movie_id): movie = Movie.query.get_or_404(movie_id) path = os.path.join(current_app.config['MEDIA_ROOT'], movie.video_path) if not os.path.exists(path): abort(404) file_size = os.path.getsize(path) range_header = request.headers.get('Range') start, end = parse_range(range_header, file_size) resp = Response(iter_file(path, start, end), status=206 if range_header else 200, content_type='video/mp4', direct_passthrough=True) resp.headers['Content-Range'] = f'bytes {start}-{end}/{file_size}' resp.headers['Accept-Ranges'] = 'bytes' resp.headers['Content-Length'] = str(end - start + 1) return resp

几个参数要说明:iter_filechunk_size=65536是64KB,这个值太小会让TCP小包过多,太大会占用内存和磁盘突发IO,64KB是视频流服务里的常见折中;direct_passthrough=True告诉Flask跳过默认的Etag和缓存逻辑,让响应变成裸文件流,避免每次请求都对整个文件做哈希计算。parse_rangeabort(416)对应Range Not Satisfiable,这是播放器拖动到文件尾端时需要的正确响应码,写错成404会导致播放器直接停止播放。文件路径用MEDIA_ROOT配置拼接,这要求config.py里必须有这个常量,否则os.path.join(None, path)会抛异常。

3.2 全片直出、HLS还是DASH,按场景选传输方案

Range方案适合MP4、WebM这类「可随机访问」的容器格式。Flask项目里最常见的坑是拿了RMVB或AVI格式硬往<video>里塞,浏览器直接不认。最常见的做法是统一转成H.264编码的MP4,一条ffmpeg命令搞定:

ffmpeg -i input.avi -c:v libx264 -c:a aac -movflags +faststart output.mp4

+faststart参数会把moov元数据块挪到文件头部,这样播放器不用等文件全部下载完就能开始播放,对MP4直出方案几乎是必加的参数。不转封装直接改扩展名是无效的,浏览器按容器格式解析,后缀名只影响服务器返回的Content-Type

方案服务端工作量进度条支持兼容性
MP4 + Range中,需要实现Range逻辑所有现代浏览器
HLS/m3u8低,返回静态文件即可靠播放器实现iOS原生支持,Web端需要hls.js
DASH需要dash.js,标准较复杂

HLS切片适合视频量大的场景,ffmpeg切片成m3u8加ts分片,天然支持码率自适应。代价是课程设计级别的前端要额外引hls.js,且切片文件数量多,管理略麻烦。大多数课程设计和中小型影视站,MP4加Range是性价比最高的方案。

3.3 防盗链:Referrer校验入门,签名Token才是正解

视频网站上线后第一个被薅的就是视频地址。用户拿<video>的src直接下载,或者别的站点把视频地址嵌到自己页面上播放。防外部站点盗用,最简单的是校验Referer头:

@app.route('/play/<int:movie_id>') def play_stream(movie_id): referer = request.headers.get('Referer', '') if not referer.startswith(request.host_url): abort(403) # 后续Range处理逻辑同上

这个方案对「拿地址浏览器直接下载」无效,因为浏览器直接访问地址时Referer为空。更可靠的是生成带有效期的签名URL,在电影详情页点击播放时拿到?token=xxx&expire=xxx的临时地址,服务端校验通过才允许进入流文件逻辑。签名生成代码放在模板路由里:

import hmac, hashlib, time def sign_video_url(movie_id): expire = int(time.time()) + 3600 * 6 # 6小时有效 msg = f'{movie_id}:{expire}'.encode() token = hmac.new(current_app.config['SECRET_KEY'], msg, hashlib.sha1).hexdigest() return f'/play/{movie_id}?expire={expire}&token={token}'

校按时重新按相同参数计算一遍HMAC,相等才放行。注意SECRET_KEY不能用默认的'dev'或空值,否则等于签名形同虚设。签名URL过期后播放器重新拉一次详情页就能刷新,对用户体验影响很小。

4. 页面与播放器:模板渲染、搜索分页和后台上线的联动

4.1 播放器选型:原生video标签配合续播接口

前台播放页用原生<video>标签搭底,需要弹幕、倍速、画中画的时候再引入DPlayer或video.js。原生标签结构如下:

<video id="player" controls preload="auto" width="100%" height="420"> <source src="{{ play_url }}" type="video/mp4"> </video> <script> const player = document.getElementById('player'); player.currentTime = {{ history_progress }}; // 续播:定位到上次进度 player.addEventListener('pause', () => { fetch('/api/progress', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ movie_id: {{ movie.id }}, progress: Math.floor(player.currentTime) }) }); }); </script>

preload="auto"让页面加载后就拉少量数据,便于用户快速看到第一帧。续播通过player.currentTime直接定位,把播放进度回写到PlayHistory表。注意浏览器自动播放政策下,带声音的视频不能自动启动播放,所以这里只定位进度不做play()调用。播放过程中定期保存进度比只在pause时保存更可靠,用户直接关标签页时pause事件不一定会触发。

4.2 搜索和分页:条件查询、排序、翻页一次写全

前台首页的搜索要按标题模糊查、按分类筛选、按播放数排序。Flask-SQLAlchemy的查询写法需要把过滤条件累积起来:

@app.route('/') def index(): page = request.args.get('page', 1, type=int) keyword = request.args.get('kw', '').strip() cid = request.args.get('cid', 0, type=int) query = Movie.query.filter(Movie.status == 1) if keyword: query = query.filter(Movie.title.like(f'%{keyword}%')) if cid: query = query.filter(Movie.category_id == cid) movies = query.order_by(Movie.play_count.desc()) \ .paginate(page=page, per_page=12, error_out=False) return render_template('index.html', movies=movies, pages=movies.pages, page=page, kw=keyword, cid=cid)

paginate是Flask-SQLAlchemy内置的分页对象,返回的movies.items是当前页数据,movies.pages是总页数,error_out=False防止页码越界时抛404。排序用play_count.desc()让热播电影靠前。搜索词kw和分类cid要回传到模板里,否则翻页后筛选条件会丢掉。分页链接生成时用url_for('index', page=p, kw=kw, cid=cid)带上原参数。

4.3 后台发布电影,大文件上传要避开Flask的内存陷阱

后台发布电影的字段和Movie表一一对应,但video_path不放文件上传的结果,而是放「服务器上已有的视频文件相对路径」。这样避免Flask接收大文件时把内存打爆。视频上传采用保存到临时目录加秒传映射的方式,文件名用时间戳加随机串,防止中文文件名导致路径解析错误:

# admin.py 上传功能的简化版 from werkzeug.utils import secure_filename import time, os, uuid @app.route('/admin/movie/add', methods=['POST']) def admin_add_movie(): if not current_user.is_authenticated or not current_user.is_admin: abort(403) file = request.files.get('video') if not file or not file.filename.endswith('.mp4'): flash('仅支持MP4文件') return redirect(request.referrer) # 按月划分子目录,避免单目录文件过多 sub_dir = time.strftime('%Y%m') os.makedirs(os.path.join(current_app.config['MEDIA_ROOT'], sub_dir), exist_ok=True) base = f"{int(time.time())}_{uuid.uuid4().hex[:6]}.mp4" avatar_path = os.path.join(sub_dir, base) file.save(os.path.join(current_app.config['MEDIA_ROOT'], avatar_path)) movie = Movie(title=request.form['title'], video_path=avatar_path, category_id=int(request.form['category_id']), summary=request.form['summary'], status=1) db.session.add(movie) db.session.commit() return redirect('/admin/movies')

文件名里时间戳加uuid4前6位,保证一个时间点内并发上传不撞名,也避免直接用原始文件名让secure_filename处理中文时被截断成空串。按月建子目录是防止文件系统单目录出现几万文件的寻址瓶颈。前端上传超时是另一个坑,Nginx的client_max_body_size默认1m,必须在server块里调大,同时proxy_read_timeout也要放宽到300秒以上,否则大视频传到一半就被断掉。

5. 权限与安全:登录、防刷与数据库增删改查的隐藏风险

5.1 登录态用session,配合装饰器控制前后台角色

前台用户和后台管理员共用一张User表,通过is_admin字段区分角色。Flask自带的session底层是签名Cookie,不占用服务端内存,课程设计规模下足够。登录逻辑里必须注意:session.permanent决定会话生命周期,前端用户要持久登录,后台要短会话。登录接口和权限装饰器拆开写:

from functools import wraps from flask import session, redirect, url_for, abort def login_required(view_func): @wraps(view_func) def wrapper(*args, **kwargs): if not session.get('user_id'): return redirect(url_for('login', next=request.url)) return view_func(*args, **kwargs) return wrapper def admin_required(view_func): @wraps(view_func) def wrapper(*args, **kwargs): if not session.get('is_admin'): abort(403) return view_func(*args, **kwargs) return wrapper

装饰器的实现要点:@wraps保留原视图的元信息,next=request.url让登录成功后跳回原地址,abort(403)返回403而不是重定向,避免后台地址被爆破尝试时混淆判断。session里的is_admin在登录成功时写入,退出登录时用session.clear()清空全部会话数据。

5.2 不只是防SQL注入:表单、模板与请求头的三个入口

Flask-SQLAlchemy的ORM参数绑定天然过滤SQL注入,但仍有三个容易忽略的入口。第一个是手写text()execute(sql)时直接拼接参数,正确写法是db.session.execute(text('SELECT * FROM movie WHERE title LIKE :kw'), {'kw': f'%{kw}%'})。第二个是模板里的XSS,Jinja2对{{ movie.summary }}默认转义HTML,但用|safe过滤器的地方要严格控制数据来源。第三个是重定向攻击,request.referrer用于后台跳转时不要直接透传。

数据库增删改查在后台的体现是标准的四个视图:列表、新增、编辑、删除。删除用db.session.delete(movie)提交后,还要把对应的视频文件从磁盘删除,否则数据库删了文件还在,慢慢占满磁盘。这个「两级删除」是课程设计里代码和文档都很少提到、但上线一定遇到的事。删除前先取movie.video_path拼出绝对路径,os.path.exists检查后再os.remove

5.3 播放量防刷:用一张计数表做幂等累加

play_count每次播放视图加一,刷新一次页面就多一次。常见的防刷方案是同一IP在60秒内对同一部电影只计一次数。直接依赖一张计数表加联合主键:

CREATE TABLE play_count_log ( movie_id INT NOT NULL, ip_hash CHAR(40) NOT NULL, first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (movie_id, ip_hash) );

在播放视图里先执行INSERT IGNORE,影响行数为1说明是第一次访问,再累加play_countip_hash不存原始IP,用SHA1做一次散列,既满足聚类统计又避免敏感信息明文落库。这个方案对多实例部署同样有效,瓶颈只在数据库单行插入的并发量。要防止同一用户换IP刷量,可以叠加session里的user_id生成校验值,双因子判断更稳。

6. 部署与排错:zip里的项目能跑起来,更要能上线

6.1 Gunicorn加Nginx,视频站点的最小上线配置

开发环境里flask run自带单进程服务,页面最多几十个并发视频请求,很容易卡死。上线的常见结构是Nginx当反向代理加静态文件服务,Gunicorn跑Flask应用。启动命令如下:

pip install gunicorn gunicorn -w 2 -b 127.0.0.1:8000 app:app --timeout 30

Nginx侧的配置有两段关键location:

location /media/ { alias /data/movie/media/; sendfile on; sendfile_max_chunk 1m; aio on; } location /play/ { proxy_pass http://127.0.0.1:8000; proxy_buffering off; }

sendfile_max_chunk 1m防止单次sendfile占住worker太久,aio on对视频文件走异步IO。proxy_buffering off/play/必须关闭,否则Nginx缓冲视频流导致Range响应被吞掉,进度条又拖不动。Gunicorn的-w 2只开两个worker,视频流IO密集,worker太多反而在GIL上互相争抢。--timeout 30防止某个慢请求把worker整个拖死,超过30秒直接被杀了重启。

6.2 一次Range请求出错的排查路线

视频播放中最常见的故障是「前几秒能放,拖动就转圈」。按这个顺序排查:先确认浏览器发出的请求头里有Range且Nginx没有剥离它;再确认Flask返回的Content-Range格式是bytes start-end/total;最后确认Content-Length和实际响应体字节数一致。一个隐蔽的坑是Response里设置了Content-Encoding: gzip,视频流一旦被压缩编码,播放器无法解析Range,必须在Nginx对视频location关闭gzip。用curl直接验证是最快的方式:

curl -I -H "Range: bytes=0-1023" http://127.0.0.1:8000/play/1

看到HTTP/1.1 206 Partial Content和正确的Content-Range就是Range链路正常。如果返回200,说明Flask响应没走流媒体分支,检查request.headers.get('Range')拿到的值是否为None,以及路径拼接是否正确。顺便确认MEDIA_ROOT配置在config.py里有值,别拿None拼进os.path.join

6.3 磁盘IO扛不住时,用X-Accel-Redirect把文件传输交给Nginx

单机视频站最大的瓶颈是Flask逐块读文件再转发。Nginx提供了X-Accel-Redirect机制,Flask校验权限通过后只返回一个特殊响应头,Nginx直接接管文件传输,Gunicorn worker瞬间释放。改造/play/视图的返回逻辑:

@app.route('/play/<int:movie_id>') def play_stream(movie_id): movie = Movie.query.get_or_404(movie_id) path = os.path.join(current_app.config['MEDIA_ROOT'], movie.video_path) if not os.path.exists(path): abort(404) resp = Response('', status=200) resp.headers['X-Accel-Redirect'] = f'/media/{movie.video_path}' resp.headers['Content-Type'] = 'video/mp4' return resp

Nginx收到这个响应头后,会从/media/内部location去取静态文件,整个过程对Flask来说是瞬时的。这个方案让Flask只做鉴权和计数,文件传输全部交给Nginx,CPU和内存占用立刻降下来。要注意X-Accel-Redirect指向的必须是Nginx可解析的内部路径,不能直接暴露文件系统绝对路径。配合前面的PlayHistory续播接口,单机撑住几百个并发视频请求没有问题。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 2:17:51

Node.js + AI开发实战:前端工程师快速上手大模型应用

经常有朋友私信问我类似的问题&#xff1a;“我不是算法工程师&#xff0c;也没系统学过Python&#xff0c;能不能做AI应用开发&#xff1f;”我的回答一直是&#xff1a;能&#xff0c;而且如果你本来就会一点前端或者后端&#xff0c;用Node.js接入AI这条路比想象中要顺得多。…

作者头像 李华
网站建设 2026/9/12 2:16:11

Apache Fesod替代EasyExcel:复杂Excel解析性能优化实战

1. 从EasyExcel切换到Apache Fesod&#xff1a;不是跟风&#xff0c;是被真实业务压出来的选择我第一次在生产环境里把EasyExcel换成Apache Fesod&#xff0c;不是因为看到什么技术雷达榜单&#xff0c;也不是听了某场分享会就热血上头——而是凌晨两点&#xff0c;运维同事发来…

作者头像 李华
网站建设 2026/9/12 2:14:30

STM32H750 LTDC驱动7寸RGB屏:时序参数与SDRAM显存配置全解析

简介&#xff1a;面向嵌入式开发者的STM32H750 LTDC驱动工程&#xff0c;支持7英寸1024600 RGB LCD屏&#xff0c;基于HAL库实现&#xff0c;并附带触摸屏驱动。工程覆盖LTDC控制器初始化、GPIO/时钟/DMA配置、触摸坐标解析等关键模块&#xff0c;源码结构清晰&#xff0c;便于…

作者头像 李华
网站建设 2026/9/12 2:14:06

Java魂斗罗游戏开发:帧同步渲染与实体状态机实现

简介&#xff1a;这是一份面向Java初学者与编程实践者的经典游戏复刻项目&#xff0c;基于Java SE平台实现魂斗罗核心玩法&#xff0c;聚焦面向对象设计、GUI绘图、事件响应、多线程控制及基础游戏逻辑构建。资源为ZIP压缩包&#xff0c;大小1.71MB&#xff0c;包含完整可运行源…

作者头像 李华
网站建设 2026/9/12 2:12:17

Kubernetes域名访问实践:从Service到Ingress的完整指南

做过线上服务的人&#xff0c;多半都经历过这么一件事&#xff1a;服务已经跑在Kubernetes里了&#xff0c;Deployment也正常&#xff0c;Pod也Ready&#xff0c;可别人要访问它&#xff0c;总不能每次都用IP加端口。尤其是对外提供服务&#xff0c;大家习惯的是输入一个域名就…

作者头像 李华