FastAPI实现206 Partial Content后,如何适配各浏览器Video组件?
问题描述
我用FastAPI实现了一个分段加载视频的流式接口,代码如下:
@router.get("/video/{name_video}") async def get_video(name_video: str, range: str = Header(None)): # bytes=0- start, end = range.replace("bytes=", "").split("-") start = int(start) end = int(start + PORTION_SIZE) with open(current_directory + name_video, "rb") as myfile: myfile.seek(start) data = myfile.read(end - start) size_video = str(os.path.getsize(current_directory + name_video)) headers = { 'Content-Range': f'bytes {str(start)}-{str(end)}/{size_video}', 'Accept-Ranges': 'bytes' } return Response(content=data, status_code=206, headers=headers, media_type="video/mp4")
前端用video组件直接调用:
<video width="1200" controls> <source src="http://127.0.0.1:8000/api/stream_video/video_tet.mp4" type="video/mp4"> </video>
但只有Firefox能正常播放,Chrome和Edge加载失败,需要通用实现方案、接口优化建议,也接受JS请求的实现方式。
通用实现与优化方案
一、接口核心问题修复
你的代码存在3个导致Chrome/Edge兼容失败的关键问题:
- Range头处理不严谨:当请求头仅提供起始值(如
bytes=1000-),split("-")会得到空的end值,直接转换会报错;且未判断计算出的end是否超过视频实际大小。 - 缺失Content-Length头:Chrome/Edge需要明确响应的字节长度,否则无法正确解析分段数据。
- 路径安全风险:直接拼接文件名可能引发路径遍历漏洞,需做安全校验。
优化后的接口代码:
import os from fastapi import APIRouter, Header, Response, HTTPException from starlette.status import HTTP_404_NOT_FOUND, HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE router = APIRouter() PORTION_SIZE = 1024 * 1024 # 1MB分段,可根据需求调整 VIDEO_DIR = "./videos/" # 视频存放目录,提前配置 @router.get("/video/{name_video}") async def get_video(name_video: str, range: str = Header(None)): # 安全校验:防止路径遍历攻击 safe_filename = os.path.basename(name_video) video_path = os.path.join(VIDEO_DIR, safe_filename) if not os.path.exists(video_path) or not os.path.isfile(video_path): raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Video not found") video_size = os.path.getsize(video_path) # 处理未指定Range的情况,返回完整视频(可选逻辑) if not range: with open(video_path, "rb") as f: return Response(content=f.read(), media_type="video/mp4", status_code=200) # 解析Range请求头 try: range_part = range.replace("bytes=", "") start_str, end_str = range_part.split("-") start = int(start_str) if start_str else 0 # 处理end的两种情况:未指定/超过视频大小 end = int(end_str) if end_str else (start + PORTION_SIZE - 1) end = min(end, video_size - 1) # 字节范围是闭区间,最大为总大小-1 except ValueError: raise HTTPException(status_code=HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE, detail="Invalid range") # 校验Range合法性 if start < 0 or start >= video_size or end < start: raise HTTPException(status_code=HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE, detail="Range not satisfiable") # 读取分段数据 with open(video_path, "rb") as f: f.seek(start) data = f.read(end - start + 1) # 闭区间,需+1才能取到end位置的字节 # 构建响应头 headers = { 'Content-Range': f'bytes {start}-{end}/{video_size}', 'Accept-Ranges': 'bytes', 'Content-Length': str(len(data)), # 必须返回本次响应的字节长度 'Content-Type': 'video/mp4' } return Response(content=data, status_code=206, headers=headers)
二、前端通用兼容方案
1. 原生video组件直接调用
修改后的接口已兼容Chrome/Edge,直接沿用原有video标签即可,注意两点:
- 确保视频为标准MP4格式(H.264编码),非标准编码可能导致浏览器解码失败。
- 跨域场景下需在FastAPI中配置CORS:
from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["*"], # 生产环境替换为具体域名 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
2. JS手动实现分段请求(可选)
如果需要更精细的加载控制,可使用Fetch API手动处理分段:
<video width="1200" controls id="myVideo"></video> <script> const video = document.getElementById('myVideo'); const videoUrl = 'http://127.0.0.1:8000/api/stream_video/video_tet.mp4'; let videoSize = 0; const SEGMENT_SIZE = 1024 * 1024; // 1MB分段 // 先获取视频总大小 fetch(videoUrl, { method: 'HEAD' }) .then(res => { videoSize = parseInt(res.headers.get('Content-Length')); loadSegment(0); }) .catch(err => console.error(err)); function loadSegment(start) { const end = Math.min(start + SEGMENT_SIZE - 1, videoSize - 1); fetch(videoUrl, { headers: { 'Range': `bytes=${start}-${end}` } }) .then(res => res.blob()) .then(blob => { const blobUrl = URL.createObjectURL(blob); video.src = blobUrl; // 监听播放进度,提前加载下一段 video.addEventListener('timeupdate', () => { if (video.buffered.length > 0) { const bufferedEnd = video.buffered.end(video.buffered.length - 1); if (bufferedEnd / video.duration > 0.8) { loadSegment(end + 1); } } }, { once: true }); }) .catch(err => console.error(err)); } </script>
三、额外优化建议
- 异步文件读取:用
aiofiles替代原生open,避免阻塞FastAPI事件循环,提升并发性能:
import aiofiles async with aiofiles.open(video_path, "rb") as f: await f.seek(start) data = await f.read(end - start + 1)
- 缓存控制:添加
Cache-Control头,允许浏览器缓存分段数据,减少重复请求:
headers['Cache-Control'] = 'public, max-age=3600'
- 多格式支持:根据文件后缀返回对应
Content-Type,比如video/webm、video/ogg等。
内容的提问来源于stack exchange,提问作者Luis Garcia
相关产品推荐
相关产品推荐

