1

Goal

I'm trying to stream in-memory images from a Python backend written in FastAPI to a HTML (Vue3) frontend. However, I cannot get the video element to display the stream of images like a video.

Minimal example attempt

As a minimal example, I create 2 images in memory (red and blue) on the server side, which should be displayed on the frontend alternating each second.

I created the following folder and HTML file: templates/index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Streaming JPEG</title>
</head>
<body>
    <h1>Streaming JPEG</h1>
    <p>Your IP address is {{ request.client.host }}.</p>
    <video src="http://127.0.0.1:8000/video-stream" width="256" height="256" autoplay></video> <!-- 0.0.0.0 -->
    <p>End video</p>
</body>
</html>

And I created the following backend file main.py:

import io
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from PIL import Image
import asyncio

app = FastAPI()

templates = Jinja2Templates(directory="templates")


@app.get("/", response_class=HTMLResponse)
async def read_item(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})


@app.get("/video-stream")
async def video_stream():
    # create a red and a blue image
    imgs = [
        Image.new('RGB', (256, 256), color='red'),
        Image.new('RGB', (256, 256), color='blue')  # .tobytes()
    ]
    img_bytes = list()
    for img in imgs:
        # Save the image to a BytesIO object as JPEG format
        with io.BytesIO() as output:
            img.save(output, format="JPEG")
            img_bytes.append(output.getvalue())

    # generator function that will generate JPEG-encoded frames
    async def generate_frames():
        yield b'--frame\r\n'
        while True:
            for idx in range(len(img_bytes)):
                print(f"idx: {idx}")
                yield b'Content-Type: image/jpeg\r\n\r\n' + img_bytes[idx] + b'\r\n--frame\r\n'
                await asyncio.sleep(1)

    return StreamingResponse(generate_frames(), media_type="multipart/x-mixed-replace;boundary=frame")


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)  # "0.0.0.0"

Which can be started with one of the following commands:

uvicorn main:app --reload
python main.py

Explanation

While searching online it seemed I had to append Content-Type to a byte version of an image before sending it through StreamingResponse, however it doesn't seem to work for me.

Problems

When navigating to http://127.0.0.1:8000/ I see the HTML template with a white area where the video is supposed to play alternating red / blue images.

Looking in my Firefox console (also tried with Chromium), I see the following 2 messages:

  • HTTP “Content-Type” of “multipart/x-mixed-replace” is not supported. Load of media resource http://127.0.0.1:8000/video-stream failed.
  • Cannot play media. No decoders for requested formats: multipart/x-mixed-replace

Which seems to indicate it's not properly encoded? Or am I doing something else wrong? Any help is appreciated!

NumesSanguis
  • 5,832
  • 6
  • 41
  • 76

0 Answers0