Return File/Streaming response from online video URL in FastAPI
Asked Answered
S

2

2

I am using FastAPI to return a video response from googlevideo.com. This is the code I am using:

@app.get(params.api_video_route)
async def get_api_video(url=None):

  def iter():
     req = urllib.request.Request(url)

     with urllib.request.urlopen(req) as resp:
         yield from io.BytesIO(resp.read())


  return StreamingResponse(iter(), media_type="video/mp4")

but this is not working

I want this Nodejs to be converted into Python FastAPI:

app.get("/download-video", function(req, res) { 
 http.get(decodeURIComponent(req.query.url), function(response) { 
   res.setHeader("Content-Length", response.headers["content-length"]); 
   if (response.statusCode >= 400)         
     res.status(500).send("Error");                     
     response.on("data", function(chunk) { res.write(chunk); }); 
     response.on("end", function() { res.end(); }); }); });
Sachasachem answered 8/3, 2022 at 13:23 Comment(5)
What isn't working? What do you expect to happen? Do you get any error messages? What kind of response did you get? Does the request return any data at all if you watch it in a debugger (or even print the response?)Gondi
@Gondi it is not returning any response and the API keeps loading foreverSachasachem
app.get("/download-video", function(req, res) { http.get(decodeURIComponent(req.query.url), function(response) { res.setHeader("Content-Length", response.headers["content-length"]); if (response.statusCode >= 400) res.status(500).send("Error"); response.on("data", function(chunk) { res.write(chunk); }); response.on("end", function() { res.end(); }); }); }); This is the nodejs code which I am converting in python fastapiSachasachem
Have you checked if your call to resp.read() gets any data at all? Does it get called? Does urlopen succeeed?Gondi
@Gondi Yeah it is returning bytes but I want it in mp4/video format and it takes alot of timeSachasachem
H
1

The quick solution would be to replace yield from io.BytesIO(resp.read()) with the one below (see FastAPI documentation - StreamingResponse for more details).

 yield from resp

However, instead of using urllib.request and resp.read() (which would read the entire file contents into memory, hence the reason for taking too long to respond), I would suggest using the HTTPX library, which, in contrast to urllib and requests libraries, provides async support as well, which is more suitable in an async environment such as FastAPI's. Also, it supports Streaming Responses (see async Streaming Responses too), and thus, you can avoid loading the entire response body into memory at once (especially, when dealing with large files). Below are provided examples in both synchronous and asynchronous ways on how to stream a video from a given URL.

Note: Both versions below would allow multiple clients to connect to the server and get the video stream without being blocked, as a normal def endpoint in FastAPI is run in an external threadpool that is then awaited, instead of being called directly (as it would block the server)—thus ensuring that FastAPI will still work asynchronously. Even if you defined the endpoint of the first example below with async def instead, it would still not block the server, as StreamingResponse will run the code (for sending the body chunks) in an external threadpool that is then awaited (have a look at this comment and the source code here), if the function for streaming the response body (i.e., iterfile() in the examples below) is a normal generator/iterator (as in the first example) and not an async one (as in the second example). However, if you had some other I/O or CPU blocking operations inside that endpoint, it would result in blocking the server, and hence, you should drop the async definition on that endpooint. The second example demonstrates how to implement the video streaming in an async def endpoint, which is useful when you have to call other async functions inside the endpoint that you have to await, as well as you thus save FastAPI from running the endpoint in an external threadpool. For more details on def vs async def, please have a look at this answer.

The below examples use iter_bytes() and aiter_bytes() methods, respectively, to get the response body in chunks. These functions, as described in the documentation links above and in the source code here, can handle gzip, deflate, and brotli encoded responses. One can alternatively use the iter_raw() method to get the raw response bytes, without applying content decoding (if is not needed). This method, in contrast to iter_bytes(), allows you to optionally define the chunk_size for streaming the response content, e.g., iter_raw(1024 * 1024). However, this doesn't mean that you read the body in chunks of that size from the server (that is serving the file) directly. If you had a closer look at the source code of iter_raw(), you would see that it just uses a ByteChunker that stores the byte contents into memory (using BytesIO() stream) and returns the content in fixed-size chunks, depending the chunk size you passed to the function (whereas raw_stream_bytes, as shown in the linked source code above, contains the actual byte chunk read from the stream).

Using HTTPX with def endpoint

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import httpx

app = FastAPI()

@app.get('/video')
def get_video(url: str):

    def iterfile():
        with httpx.stream("GET", url) as r:
            for chunk in r.iter_bytes():
                yield chunk

    return StreamingResponse(iterfile(), media_type="video/mp4")

Using HTTPX with async def endpoint

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import httpx

app = FastAPI()

@app.get('/video')
async def get_video(url: str):

    async def iterfile():
        async with httpx.AsyncClient() as client:
            async with client.stream("GET", url) as r:
                async for chunk in r.aiter_bytes():
                    yield chunk
                    
    return StreamingResponse(iterfile(), media_type="video/mp4")

You can use public videos provided here to test the above. Example:

http://127.0.0.1:8000/video?url=http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4

If you would like to return a custom Response or FileResponse instead—which I wouldn't really recommend in case you are dealing with large video files, as you should either read the entire contents into memory, or save the contents to a temporary file on disk that you later have to read again into memory, in order to send it back to the client—please have a look at this answer and this answer.

Horace answered 8/3, 2022 at 16:37 Comment(2)
How can I identify the media_type here as I'm dealing with different files. Can video or Image or some other format.Myers
Gentlemen I believe Content-Disposition is the header I was looking forMyers
N
1

I encountered similar issues but solved all. The main idea is to create a session with requests.Session(), and yield a chunk one by one, instead of getting all the content and yield it at once. This works very nicely without making any memory issue at all.

@app.get(params.api_video_route)
async def get_api_video(url=None):

  def iter():
     session = requests.Session()
     r = session.get(url, stream=True)
     r.raise_for_status()
     
     for chunk in r.iter_content(1024*1024):
         yield chunk

  return StreamingResponse(iter(), media_type="video/mp4") 

Nika answered 10/8, 2022 at 1:18 Comment(1)
Remember. It's very important to set up 'stream=True' when you connect to the target url via session. By setting up 'stream=True', requests is actually in a idling mode, which means it never retrieve any content at all yet until it starts yielding a chunk by activating iter_content(). In this sample I set up the chunk size 1MB(1024*1024) to perform better. The chunk size can be different in your system environment, but such a thing isn't the main issue you raise here.Nika

© 2022 - 2024 — McMap. All rights reserved.