2

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()
c-x-berger
  • 991
  • 12
  • 30
  • 1
    Your code looks good, and I think this is a bug with Quart that is now fixed in master. See https://gitlab.com/pgjones/quart/issues/154. – pgjones Nov 06 '18 at 22:42
  • @pgjones Will that be pushed to PyPi soon? There aren't instructions on source installation in the repository (+ updating from pip is easier) – c-x-berger Nov 07 '18 at 14:32
  • 1
    I've just pushed 0.6.9, so it should be available now. – pgjones Nov 10 '18 at 10:02
  • Hmm. Updated Quart and hypercorn (`pip install -U Quart hypercorn`) and now starting `app.py` gives `RuntimeError: ASGI Scope type is unknown` (and rejects all connections, followed by a `TimeoutError`.) I'll open a GitLab issue later today. – c-x-berger Nov 12 '18 at 17:41
  • 1
    Sadly the Hypercorn 0.4.0 release introduced a new bug, which I've now fixed with Hypercorn 0.4.1. Could you upgrade and try it? I think then this should be good. – pgjones Nov 12 '18 at 20:02
  • Well, save the `ASGI Framework Lifespan error, continuing without Lifespan support` message on startup, everything seems to be in working order now! I'll post an answer summary soonish. – c-x-berger Nov 13 '18 at 15:53

1 Answers1

1

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.)

c-x-berger
  • 991
  • 12
  • 30