繼上一篇介紹 StreamingResponse 的基礎應用後,今天我們要深入探討一個更實用的場景:如何實現支援 Range 請求的影片串流服務。這項技術能讓使用者在瀏覽器中直接播放影片,並且支援拖曳進度條跳到任意時間點播放,就像 YouTube 或 Netflix 一樣的體驗。
雖然跟 AI 比較沒有直接關係,只有部分 AI 應用會需要有播放影片功能,但還是想趁這個機會跟大家分享一下~ (畢竟之前也花了不少時間在處理這項需求XD)
Range 請求是 HTTP/1.1 規範中定義的一項重要功能,它允許客戶端請求資源的特定部分,而不是整個檔案。對於影片串流來說,這意味著:
當瀏覽器的 <video>
標籤請求影片時,它會在 HTTP 標頭中加入 Range
欄位,例如 Range: bytes=0-1023
,表示要求下載從第 0 位元組到第 1023 位元組的內容。
一般的 StreamingResponse 雖然能夠串流影片,但無法處理 Range 請求。這會導致:
先讓我們看一下直接傳送檔案的做法 (沒使用 Range 請求):
@app.get("/video")
def get_video():
def iterfile(file_path: str):
with open(file_path, mode="rb") as file_like:
while True:
chunk = file_like.read(1024 * 1024) # 1MB chunks
if not chunk:
break
yield chunk
video_path = "assets/record.mp4"
# 檢查檔案是否存在
if not os.path.exists(video_path):
return {"error": "Video file not found"}
return StreamingResponse(
iterfile(video_path),
media_type="video/mp4",
headers={"Content-Disposition": "inline; filename=record.mp4"}
)
以及一個簡單前端,使用 <video>
來串接影片 API (/video
):
@app.get("/demo")
async def get_demo():
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>Video Demo</title>
</head>
<body>
<div>
<h1>Video Streaming Demo</h1>
<video width="800" height="600" controls>
<source src="/video" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
</body>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
可以看到,影片是可以正常播放的,但是沒辦法隨意調整時間軸,一旦影片很長 (檔案很大),就會導致使用體驗極差。
讓我們先從一個簡單的範例開始,理解 Range 請求的基本概念:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
import os
app = FastAPI()
VIDEO_PATH = "sample.mp4"
@app.get("/video")
def get_video(request: Request):
range_header = request.headers.get("range")
if not range_header:
raise HTTPException(status_code=416, detail="Range header missing")
VIDEO_PATH = "assets/record.mp4"
# 解析 Range 標頭 (e.g. bytes=1000-)
start, end = range_header.replace("bytes=", "").split("-")
start = int(start)
file_size = os.path.getsize(VIDEO_PATH)
end = int(end) if end else file_size - 1
chunk_size = end - start + 1
def iterfile(start_pos, end_pos):
with open(VIDEO_PATH, "rb") as f:
f.seek(start_pos)
yield f.read(chunk_size)
headers = {
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(chunk_size),
}
return StreamingResponse(iterfile(start, end), media_type="video/mp4", headers=headers, status_code=206)
這個簡單的實作展示了 Range 請求的核心概念:解析 Range 標頭、計算檔案範圍、回傳 206 狀態碼。
可以看到,不只影片可以正常播放,也可以隨意調整時間軸,使用體驗好很多~
然而,在實際應用中,我們需要處理更多情況,例如:
bytes=0-499
、bytes=500-
、bytes=-500
等不同格式因此,實際應用中的程式碼會更長,有興趣的讀者可以參考這個回應的實作。
正確的狀態碼對瀏覽器的行為至關重要。Chrome 等現代瀏覽器在播放影片時會檢查是否支援 Range 請求,如果伺服器不回傳 206 狀態碼,可能會導致播放功能異常。
video/mp4
透過實作 Range 請求支援,我們的 FastAPI 影片串流服務可以有更好的使用者體驗 (更流暢地播放),而伺服器也能更有效率地處理頻寬和資源。這項技術不僅適用於影片,也可以應用在其他大型檔案的下載服務上,如 PDF 閱讀器、音訊播放器等。