Search code examples
python-3.xpython-asyncioquart

Quart infinite/indefinite streaming response


I am trying to create a server (loosely) based on an old blog post to stream video with Quart.

To stream video to a client, it seems all I should need to do is have a route that returns a generator of frames. However, actually doing this results in a constant repeated message of socket.send() raised exception, and shows a broken image on the client. After that, the server does not appear to respond to further requests.

Using more inspiration from the original post, I tried returning a Response (using return Response(generator, mimetype="multipart/x-mixed-replace; boundary=frame").) This does actually display video on the client, but as soon as they disconnect (close the tab, navigate to another page, etc) the server begins spamming socket.send() raised exception again and does not respond to further requests.

My code is below.

# in app.py
from camera_opencv import Camera
import os
from quart import (
    Quart,
    render_template,
    Response,
    send_from_directory,
)

app = Quart(__name__)

async def gen(c: Camera):
    for frame in c.frames():
        # d_frame = cv_processing.draw_debugs_jpegs(c.get_frame()[1])
        yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame[0] + b"\r\n")


c_gen = gen(Camera(0))


@app.route("/video_feed")
async def feed():
    """Streaming route (img src)"""
    # return c_gen
    return Response(c_gen, mimetype="multipart/x-mixed-replace; boundary=frame")

# in camera_opencv.py
from asyncio import Event
import cv2

class Camera:
    last_frame = []

    def __init__(self, source: int):
        self.video_source = source
        self.cv2_cam = cv2.VideoCapture(self.video_source)
        self.event = Event()

    def set_video_source(self, source):
        self.video_source = source
        self.cv2_cam = cv2.VideoCapture(self.video_source)

    async def get_frame(self):
        await self.event.wait()
        self.event.clear()
        return Camera.last_frame

    def frames(self):
        if not self.cv2_cam.isOpened():
            raise RuntimeError("Could not start camera.")

        while True:
            # read current frame
            _, img = self.cv2_cam.read()

            # encode as a jpeg image and return it
            Camera.last_frame = [cv2.imencode(".jpg", img)[1].tobytes(), img]
            self.event.set()
            yield Camera.last_frame
        self.cv2_cam.release()

Solution

  • This was originally an issue with Quart itself.

    After a round of bugfixes to both Quart and Hypercorn, the code as posted functions as intended (as of 2018-11-13.)