Chrome浏览器Video标签拖动进度条请求错误字节范围,FASTAPI服务异常
解决FastAPI视频流在Chrome中拖动进度条卡顿/重置的问题
问题根源
你的代码存在两个关键问题,导致Chrome无法正确处理视频分段请求:
- Content-Range格式不符合HTTP规范:HTTP规定
Content-Range的区间是闭区间(start-end包含两端字节),原代码中end取值为start + CHUNK_SIZE,实际应该是start + CHUNK_SIZE - 1,否则返回的字节范围会超出文件实际索引。 - 未正确处理Chrome的range请求格式:Chrome拖动进度条时会发送
bytes=xxx-(仅指定起始位置,无结束位置)的请求,原代码直接分割后将空字符串转为整数会引发错误,导致响应无效,Chrome停止后续请求。
修复后的FastAPI后端代码
以下是修正后的核心代码,处理了所有合法的range请求,并严格遵循HTTP规范:
from fastapi import APIRouter, Header, Response, HTTPException from pathlib import Path import aiofiles video_router = APIRouter() @video_router.get("/video/file/{folder}/{name}") async def video_endpoint(folder: str, name: str, range: str = Header(None)): CHUNK_SIZE = 2 * 1024 * 1024 # 2MB 分段大小,可根据需求调整 video_path = Path(f"./media/{folder}/{name}") # 检查文件是否存在 if not video_path.exists(): raise HTTPException(status_code=404, detail="Video not found") video_size = video_path.stat().st_size # 处理无range头的请求(首次加载或直接请求完整视频) if not range: async with aiofiles.open(video_path, "rb") as f: data = await f.read() return Response(data, media_type="video/mp4") # 解析range请求 try: range_part = range.replace("bytes=", "") start_str, end_str = range_part.split("-", 1) start = int(start_str) if start_str.strip() else 0 # 处理Chrome发送的仅含起始位置的range请求 if not end_str.strip(): end = video_size - 1 # 文件最后一个字节的索引 else: end = int(end_str) end = min(end, video_size - 1) # 确保不超出文件范围 start = max(start, 0) # 确保起始位置不小于0 except ValueError: # range格式无效,返回416状态码 raise HTTPException(status_code=416, detail="Invalid range request") # 调整分段结束位置,避免超过设定的分段大小 end = min(end, start + CHUNK_SIZE - 1) # 异步读取文件分段(避免阻塞事件循环) async with aiofiles.open(video_path, "rb") as f: await f.seek(start) data = await f.read(end - start + 1) # 构造符合规范的响应头 headers = { 'Content-Range': f'bytes {start}-{end}/{video_size}', 'Accept-Ranges': 'bytes', 'Content-Length': str(end - start + 1) # 明确返回的字节数 } return Response(data, status_code=206, headers=headers, media_type="video/mp4")
额外优化:使用StreamingResponse流式返回(可选)
如果视频文件较大,使用流式返回可以减少内存占用,提升性能:
@video_router.get("/video/file/{folder}/{name}") async def video_stream(folder: str, name: str, range: str = Header(None)): CHUNK_SIZE = 2 * 1024 * 1024 video_path = Path(f"./media/{folder}/{name}") if not video_path.exists(): raise HTTPException(status_code=404, detail="Video not found") video_size = video_path.stat().st_size start = 0 end = video_size - 1 if range: try: range_part = range.replace("bytes=", "") start_str, end_str = range_part.split("-", 1) start = int(start_str) if start_str.strip() else 0 if end_str.strip(): end = int(end_str) end = min(end, video_size - 1) start = max(start, 0) except ValueError: raise HTTPException(status_code=416, detail="Invalid range request") # 异步生成器,流式返回数据 async def stream_generator(): async with aiofiles.open(video_path, "rb") as f: await f.seek(start) remaining_bytes = end - start + 1 while remaining_bytes > 0: chunk = await f.read(min(CHUNK_SIZE, remaining_bytes)) if not chunk: break yield chunk remaining_bytes -= len(chunk) headers = { 'Content-Range': f'bytes {start}-{end}/{video_size}', 'Accept-Ranges': 'bytes', 'Content-Length': str(end - start + 1) } return StreamingResponse(stream_generator(), status_code=206, headers=headers, media_type="video/mp4")
前端注意事项
- 跨域配置:如果前端和FastAPI服务不在同一域名下,必须在FastAPI中添加CORS中间件:
from fastapi.middleware.cors import CORSMiddleware app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["你的前端域名"], # 生产环境请勿使用"*" allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
- 原React的video标签无需修改,保持现有配置即可。
替代方案
如果上述修正后仍有问题,可以尝试以下方案:
- 使用
ffmpeg将视频转成HLS(HTTP Live Streaming)格式,FastAPI返回M3U8索引文件和TS分片,前端使用video.js或hls.js播放,这种方式对Chrome的兼容性更好,支持更灵活的进度控制。 - 直接使用FastAPI静态文件服务:将视频文件放在静态目录下,通过
StaticFiles挂载,FastAPI会自动处理206分段请求,无需手动实现:
from fastapi import FastAPI from fastapi.staticfiles import StaticFiles app = FastAPI() app.mount("/video", StaticFiles(directory="media"), name="video")
内容的提问来源于stack exchange,提问作者Kanha Rusia
相关产品推荐
相关产品推荐

