Stream video to web browser with FastAPI
Question:
I found an example in FLASK to transmit a video camera through the rtsp protocol in the web browser.
I tried to use this same example in the fastapi, but I’m not getting it. The image is frozen.
Example in Flask (Works normally):
def generate():
# grab global references to the output frame and lock variables
global outputFrame, lock
# loop over frames from the output stream
while True:
# wait until the lock is acquired
with lock:
# check if the output frame is available, otherwise skip
# the iteration of the loop
if outputFrame is None:
continue
# encode the frame in JPEG format
(flag, encodedImage) = cv2.imencode(".jpg", outputFrame)
# ensure the frame was successfully encoded
if not flag:
continue
# yield the output frame in the byte format
yield (b'--framern' b'Content-Type: image/jpegrnrn' +
bytearray(encodedImage) + b'rn')
@app.route("/")
def video_feed():
# return the response generated along with the specific media
# type (mime type)
return Response(generate(),
mimetype="multipart/x-mixed-replace; boundary=frame")
I tried to do it that way in FastAPI, but only the first frame appears, getting frozen.
def generate():
# grab global references to the output frame and lock variables
global outputFrame, lock
# loop over frames from the output stream
while True:
# wait until the lock is acquired
with lock:
# check if the output frame is available, otherwise skip
# the iteration of the loop
if outputFrame is None:
continue
# encode the frame in JPEG format
(flag, encodedImage) = cv2.imencode(".jpg", outputFrame)
# ensure the frame was successfully encoded
if not flag:
continue
# yield the output frame in the byte format
yield b''+bytearray(encodedImage)
@app.get("/")
def video_feed():
# return the response generated along with the specific media
# type (mime type)
# return StreamingResponse(generate())
return StreamingResponse(generate(), media_type="image/jpeg")
does anyone know how to use the same function in fastapi?
In case anyone is interested in the complete code, I took the example here:
https://www.pyimagesearch.com/2019/09/02/opencv-stream-video-to-web-browser-html-page/
Answers:
After posting here, I figured out how to fix it.
In the video_feed function, in the media_type parameter, it was just to put it in the same way as in the flask:
@app.get("/")
def video_feed():
# return the response generated along with the specific media
# type (mime type)
# return StreamingResponse(generate())
return StreamingResponse(generate(), media_type="multipart/x-mixed-replace;boundary=frame")
And in the function generate:
yield (b'--framern' b'Content-Type: image/jpegrnrn' +
bytearray(encodedImage) + b'rn')
My complete code:
Range request (valid for video/PDF/etc…)
import os
from typing import BinaryIO
from fastapi import HTTPException, Request, status
from fastapi.responses import StreamingResponse
def send_bytes_range_requests(
file_obj: BinaryIO, start: int, end: int, chunk_size: int = 10_000
):
"""Send a file in chunks using Range Requests specification RFC7233
`start` and `end` parameters are inclusive due to specification
"""
with file_obj as f:
f.seek(start)
while (pos := f.tell()) <= end:
read_size = min(chunk_size, end + 1 - pos)
yield f.read(read_size)
def _get_range_header(range_header: str, file_size: int) -> tuple[int, int]:
def _invalid_range():
return HTTPException(
status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
detail=f"Invalid request range (Range:{range_header!r})",
)
try:
h = range_header.replace("bytes=", "").split("-")
start = int(h[0]) if h[0] != "" else 0
end = int(h[1]) if h[1] != "" else file_size - 1
except ValueError:
raise _invalid_range()
if start > end or start < 0 or end > file_size - 1:
raise _invalid_range()
return start, end
def range_requests_response(
request: Request, file_path: str, content_type: str
):
"""Returns StreamingResponse using Range Requests of a given file"""
file_size = os.stat(file_path).st_size
range_header = request.headers.get("range")
headers = {
"content-type": content_type,
"accept-ranges": "bytes",
"content-encoding": "identity",
"content-length": str(file_size),
"access-control-expose-headers": (
"content-type, accept-ranges, content-length, "
"content-range, content-encoding"
),
}
start = 0
end = file_size - 1
status_code = status.HTTP_200_OK
if range_header is not None:
start, end = _get_range_header(range_header, file_size)
size = end - start + 1
headers["content-length"] = str(size)
headers["content-range"] = f"bytes {start}-{end}/{file_size}"
status_code = status.HTTP_206_PARTIAL_CONTENT
return StreamingResponse(
send_bytes_range_requests(open(file_path, mode="rb"), start, end),
headers=headers,
status_code=status_code,
)
Usage
from fastapi import FastAPI
app = FastAPI()
@app.get("/video")
def get_video(request: Request):
return range_requests_response(
request, file_path="path_to_my_video.mp4", content_type="video/mp4"
)
A Simple Answer:
def get_video_range_response(request: Request, file_path: str, content_type: str = "video/mp4")
file_size = os.stat(file_path).st_size
h = request.headers.get("range").replace("bytes=", "").split("-")
start = int(h[0]) if h[0] != "" else 0
maxSize = 200000
end = start + maxSize # this is the expected end
if end >= file_size #if end > file_size then obviously end = file_size - 1
end = file_size - 1
size = end - start
headers = {"content-type": content_type,
"accept-ranges": "bytes",
"content-encoding": "identity",
"content-length": str(size),
"content-range": f" bytes {start}-{end}/{file_size}",
}
file_obj = open(file_path, mode="rb")
file_obj.seek(start)
data = file_obj.read(size)
file_obj.close()
status_code = status.HTTP_206_PARTIAL_CONTENT
return Response(content=data,
status_code=status_code,
headers=headers,
media_type=content_type
)
Usage
@app.get('/Video')
def video_endpoint(req: Request):
video_path = r"C:WOIpictvideosModi.mp4"
return get_video_range_response(req, file_path = video_path, content_type = "video/mp4")
HTML
<video controls class="w-100">
<source src="/Video" type="video/mp4">
</video>
I found an example in FLASK to transmit a video camera through the rtsp protocol in the web browser.
I tried to use this same example in the fastapi, but I’m not getting it. The image is frozen.
Example in Flask (Works normally):
def generate():
# grab global references to the output frame and lock variables
global outputFrame, lock
# loop over frames from the output stream
while True:
# wait until the lock is acquired
with lock:
# check if the output frame is available, otherwise skip
# the iteration of the loop
if outputFrame is None:
continue
# encode the frame in JPEG format
(flag, encodedImage) = cv2.imencode(".jpg", outputFrame)
# ensure the frame was successfully encoded
if not flag:
continue
# yield the output frame in the byte format
yield (b'--framern' b'Content-Type: image/jpegrnrn' +
bytearray(encodedImage) + b'rn')
@app.route("/")
def video_feed():
# return the response generated along with the specific media
# type (mime type)
return Response(generate(),
mimetype="multipart/x-mixed-replace; boundary=frame")
I tried to do it that way in FastAPI, but only the first frame appears, getting frozen.
def generate():
# grab global references to the output frame and lock variables
global outputFrame, lock
# loop over frames from the output stream
while True:
# wait until the lock is acquired
with lock:
# check if the output frame is available, otherwise skip
# the iteration of the loop
if outputFrame is None:
continue
# encode the frame in JPEG format
(flag, encodedImage) = cv2.imencode(".jpg", outputFrame)
# ensure the frame was successfully encoded
if not flag:
continue
# yield the output frame in the byte format
yield b''+bytearray(encodedImage)
@app.get("/")
def video_feed():
# return the response generated along with the specific media
# type (mime type)
# return StreamingResponse(generate())
return StreamingResponse(generate(), media_type="image/jpeg")
does anyone know how to use the same function in fastapi?
In case anyone is interested in the complete code, I took the example here:
https://www.pyimagesearch.com/2019/09/02/opencv-stream-video-to-web-browser-html-page/
After posting here, I figured out how to fix it.
In the video_feed function, in the media_type parameter, it was just to put it in the same way as in the flask:
@app.get("/")
def video_feed():
# return the response generated along with the specific media
# type (mime type)
# return StreamingResponse(generate())
return StreamingResponse(generate(), media_type="multipart/x-mixed-replace;boundary=frame")
And in the function generate:
yield (b'--framern' b'Content-Type: image/jpegrnrn' +
bytearray(encodedImage) + b'rn')
My complete code:
Range request (valid for video/PDF/etc…)
import os
from typing import BinaryIO
from fastapi import HTTPException, Request, status
from fastapi.responses import StreamingResponse
def send_bytes_range_requests(
file_obj: BinaryIO, start: int, end: int, chunk_size: int = 10_000
):
"""Send a file in chunks using Range Requests specification RFC7233
`start` and `end` parameters are inclusive due to specification
"""
with file_obj as f:
f.seek(start)
while (pos := f.tell()) <= end:
read_size = min(chunk_size, end + 1 - pos)
yield f.read(read_size)
def _get_range_header(range_header: str, file_size: int) -> tuple[int, int]:
def _invalid_range():
return HTTPException(
status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
detail=f"Invalid request range (Range:{range_header!r})",
)
try:
h = range_header.replace("bytes=", "").split("-")
start = int(h[0]) if h[0] != "" else 0
end = int(h[1]) if h[1] != "" else file_size - 1
except ValueError:
raise _invalid_range()
if start > end or start < 0 or end > file_size - 1:
raise _invalid_range()
return start, end
def range_requests_response(
request: Request, file_path: str, content_type: str
):
"""Returns StreamingResponse using Range Requests of a given file"""
file_size = os.stat(file_path).st_size
range_header = request.headers.get("range")
headers = {
"content-type": content_type,
"accept-ranges": "bytes",
"content-encoding": "identity",
"content-length": str(file_size),
"access-control-expose-headers": (
"content-type, accept-ranges, content-length, "
"content-range, content-encoding"
),
}
start = 0
end = file_size - 1
status_code = status.HTTP_200_OK
if range_header is not None:
start, end = _get_range_header(range_header, file_size)
size = end - start + 1
headers["content-length"] = str(size)
headers["content-range"] = f"bytes {start}-{end}/{file_size}"
status_code = status.HTTP_206_PARTIAL_CONTENT
return StreamingResponse(
send_bytes_range_requests(open(file_path, mode="rb"), start, end),
headers=headers,
status_code=status_code,
)
Usage
from fastapi import FastAPI
app = FastAPI()
@app.get("/video")
def get_video(request: Request):
return range_requests_response(
request, file_path="path_to_my_video.mp4", content_type="video/mp4"
)
A Simple Answer:
def get_video_range_response(request: Request, file_path: str, content_type: str = "video/mp4")
file_size = os.stat(file_path).st_size
h = request.headers.get("range").replace("bytes=", "").split("-")
start = int(h[0]) if h[0] != "" else 0
maxSize = 200000
end = start + maxSize # this is the expected end
if end >= file_size #if end > file_size then obviously end = file_size - 1
end = file_size - 1
size = end - start
headers = {"content-type": content_type,
"accept-ranges": "bytes",
"content-encoding": "identity",
"content-length": str(size),
"content-range": f" bytes {start}-{end}/{file_size}",
}
file_obj = open(file_path, mode="rb")
file_obj.seek(start)
data = file_obj.read(size)
file_obj.close()
status_code = status.HTTP_206_PARTIAL_CONTENT
return Response(content=data,
status_code=status_code,
headers=headers,
media_type=content_type
)
Usage
@app.get('/Video')
def video_endpoint(req: Request):
video_path = r"C:WOIpictvideosModi.mp4"
return get_video_range_response(req, file_path = video_path, content_type = "video/mp4")
HTML
<video controls class="w-100">
<source src="/Video" type="video/mp4">
</video>