You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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兼容失败的关键问题:

  1. Range头处理不严谨:当请求头仅提供起始值(如bytes=1000-),split("-")会得到空的end值,直接转换会报错;且未判断计算出的end是否超过视频实际大小。
  2. 缺失Content-Length头:Chrome/Edge需要明确响应的字节长度,否则无法正确解析分段数据。
  3. 路径安全风险:直接拼接文件名可能引发路径遍历漏洞,需做安全校验。

优化后的接口代码:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 12:46:15