-
-
Notifications
You must be signed in to change notification settings - Fork 6.1k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Using StreamingResponse to stream videos with seeking capabilities #1240
Comments
My bad I think i managed to solve it.
headers = {
"Accept-Ranges": "bytes",
"Content-Length": str(sz),
"Content-Range": F"bytes {asked}-{sz-1}/{sz}",
}
response = StreamingResponse(streaming_file(file_path, CS, asked), headers=headers, Not sure if there is anything else to add, but it seems like it's working here. |
Could you provide a full code example on how you achieved this? I need to implement the exact same thing (serving a folder of videos that can be streamed). |
I currently have the same issue with Chrome, did not really dug in that sense to be honest. (I'm serving files to be opened with VLC, so browser do not really matter for me) |
Thanks for reporting back @0xAcid . You should probably be able to create a subclass of |
Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues. |
I thought since I would probably not be the only one who ended up here after some Gooling, that it might be nice to provide a bit more elaborate example of how I made this work for me based on the great tips above. The snippet below shows how to implement this for any bytes-like stream. BYTES_PER_RESPONSE = 100000
def chunk_generator_from_stream(stream, chunk_size, start, size):
bytes_read = 0
stream.seek(start)
while bytes_read < size:
bytes_to_read = min(chunk_size,
size - bytes_read)
yield stream.read(bytes_to_read)
bytes_read = bytes_read + bytes_to_read
stream.close()
@router.get("...")
def stream(req: Request):
asked = req.headers.get("Range")
stream, total_size = get_file_and_total_size(...)
start_byte_requested = int(asked.split("=")[-1][:-1])
end_byte_planned = min(start_byte_requested + BYTES_PER_RESPONSE, total_size)
chunk_generator = chunk_generator_from_stream(
stream,
chunk_size=10000,
start=start_byte_requested,
size=BYTES_PER_RESPONSE
)
return StreamingResponse(
chunk_generator,
headers={
"Accept-Ranges": "bytes",
"Content-Range": f"bytes {start_byte_requested}-{end_byte_planned}/{total_size}",
"Content-Type": "..."
},
status_code=206
) |
Just to build on @FlorisHoogenboom' s answer. The "Content-Range" specifies a start byte and end byte included. The video was not streaming on Chrome first. Modifying this line:
by
works as expected. |
I adjust the code from FlorisHoogenboom a bit. This version work fine for me, both browser and VLC stream work. `
` |
I'm stuck trying to modify the above code to read the bytes to image using opencv, the implementation in https://stackoverflow.com/questions/65971081/stream-video-to-web-browser-with-fastapi simple does not work for me. Can someone help me out. |
That's a question that has also kept wandering my mind for a while but I don't have an answer yet. Big part of the solution above working of course depends on the encoding of the bytestream. If you might have opencv output in the right encoding you could just use a |
Reusable implementation (Video/PDF/etc...)import os
from typing import BinaryIO
from fastapi import FastAPI, 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,
)
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"
) |
this is quite helpful. I also found that the FileResponse doesnt work as good as other web framework, since its downloading speed is quite low comparing to flask's send_file function. |
I want to add a note to the above solution: For some reason the browser appears to try to download the whole file before starting to play it and even then I am not able to seek through the video. It starts with no range header -> so the server naturally sends back the entire thing (which takes a whole while for large files) I fixed this by removing the "content-encoding": "identity". (Even though the browser asks for "Accept-Encoding": "identity" specifically when using the range header in Firefox) And if someone can explain why Firefox does that please enlighten me. PPS: For those using a python version <3.8 make sure to resolve the walrus operator (in the send_bytes_range_requests function) properly as else you might get an LocalProtocolError("Too much data for declared Content-Length") error. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
First check
Description
I was wondering if it was possible using fastapi to use "StreamingResponse" not only to stream a video, but to be able to seek (with byte-range i guess ?)
Additional context
I am trying to reimplement a similar software to peerflix (https://github.com/mafintosh/peerflix), which basically downloads a content, and stream it then. It uses nodejs, which I would like to avoid, and i was wondering if it was possible to do such thing with fastapi. (some people achieved it with other technology : https://github.com/romanvm/kodi.yatp )
The text was updated successfully, but these errors were encountered: