Software development and beyond

Streaming video with FastAPI

We will briefly discuss various options of streaming video over the internet and see a simple streaming video example using FastAPI that will show the video in the browser using the HTML5 video tag.

Video streaming

There are three main options when streaming videos online:

Today I will show you a simple video streaming example using the HTTP protocol, since we don't have to implement anything on the frontend ourselves. The server-side code is pretty simple too. It is also not really tied to FastAPI, so it can be easily ported to Django or other framework.

The code for this article can be found in the stribny/fastapi-video repo.

Streaming video over HTTP

Let's start with the frontend part, which is going to be just a <video> tag:

<!DOCTYPE html>
<html>
<head>
<title>FastAPI video streaming</title>
</head>
<body>
<video width="1200" controls muted="muted">
<source src="http://localhost:8000/video" type="video/mp4" />
</video>
</body>
</html>

The only necessary thing is to specify the path to the video file or stream and describe the video format. In this case the video will be loaded from the /video URL as video/mp4. Multiple <source> tags can be used as well to offer multiple streams with different file formats and video encodings.

Optionally we can specify various attributes to show/hide the controls in the video, specify the element's width or decide if the video playback should start muted.

As far as frontend is concerned, we are done.

Video streaming using the video tag

Streaming video with FastAPI

There is a simple mechanism that allows browsers to ask for a specific part of the video stream. When requesting the data for the video tag, browsers send an HTTP header called range that specify the requested range in number of bytes, in the format bytes=1024000,2048000. We can then leverage this information to send only this specific part of the video file. If the browser needs to load another part of the stream, it will ask for it automatically until the whole stream is consumed.

The response needs to have a couple of headers as well for this to work. The size of the data being sent in the response is indicated with Content-Length, the overall size of the file or stream with Content-Range, the content type with Content-Type and finally Accept-Ranges header that tells the browser that byte ranges are supported.

The implementation code is fairly simple once we know what to do:

from pathlib import Path
from fastapi import FastAPI
from fastapi import Request, Response
from fastapi import Header
from fastapi.templating import Jinja2Templates


app = FastAPI()
templates = Jinja2Templates(directory="templates")
CHUNK_SIZE = 1024*1024
video_path = Path("video.mp4")


@app.get("/")
async def read_root(request: Request):
return templates.TemplateResponse("index.htm", context={"request": request})


@app.get("/video")
async def video_endpoint(range: str = Header(None)):
start, end = range.replace("bytes=", "").split("-")
start = int(start)
end = int(end) if end else start + CHUNK_SIZE
with open(video_path, "rb") as video:
video.seek(start)
data = video.read(end - start)
filesize = str(video_path.stat().st_size)
headers = {
'Content-Range': f'bytes {str(start)}-{str(end)}/{filesize}',
'Accept-Ranges': 'bytes'
}
return Response(data, status_code=206, headers=headers, media_type="video/mp4")

I defined two routes. The main route / serves the HTML template with the video tag, while the /video route streams the video.

The first step when handling the stream request is to parse the range header to get the start and end bytes so that we know which part of data to send. The part of the file or stream is also called chunk. I am using a chunk size of 1 MB as the default. Since the browser will send the first request with the range of bytes=0,, it is up to us how much data we will send. The browser will then repeat the file size with the following requests.

Once we know what to send, we load the video file with standard Python's open() function (rb indicates that we want to read binary data), point the iterator to the starting byte with video.seek(start) and read only the bytes we need with data = video.read(end - start).

Finally, we just need to construct a standard Response object with all the required headers. The status code 206 is like 200, but for partial content. The data themselves are passed simply as bytes. Two of the headers, Content-Length and Content-Type will be filled automatically by FastAPI.

If we run the server and open the browser, we will see the video being loaded in 1 MB chunks:

Video streaming result

Last updated on 16.3.2021.

fastapi python