Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use "206 Partial Content" response in a Video component for use in any browser?

I'm using the Fast API framework, I'm trying to use it as a stream type response to visualize the videos by loading parts.

@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")

It happens that it responds with blob data type and I try to use it in the video component as is:

<video width="1200" controls>
  <source src="http://127.0.0.1:8000/api/stream_video/video_tet.mp4" type="video/mp4">
</video>

I have tried to use it in browsers like Google Chrome and Edge and they do not work, the curious thing is that with Mozilla FireFox if it works. if you have to use request using js and then insert it there would be no problem, the question is that I do not know how to do it.

I would like to know in general how it would work this way and if there is a charitable soul that optimizes the response using this procedure I would be very happy.

Thank you very much for your help.

like image 533
Luis Garcia Avatar asked Sep 01 '25 10:09

Luis Garcia


1 Answers

It is better to use StreamingResponse to make sure all browsers properly support the video stream you're creating. A very similar question has been asked and answered in the FastAPI Github issue tracker here.

There's an excellent answer posted by the user angel-langdon which I'll copy here for reference. I've used it myself (slightly modifies as I use sshfs as data source) in my own project and it works great.

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"
    )
like image 89
waza-ari Avatar answered Sep 04 '25 01:09

waza-ari