How to block less while streaming video from tornado

Viewed 178

I want to know how to make my Tornado video streaming code block less so that I don't impact other handers in my web app.

I have some legacy code that I use in Tornado to stream incoming jpegs and show them as video on an html page. My handler looks like this. It's worked well enough for a few years, but lately I've noticed that it is actually a big resource hog. Whenever something calls this handler all other requests slow down dramatically.

class VideoAPI(tornado.web.RequestHandler):
    '''
    Serves a MJPEG of the images posted from the vehicle.
    '''

    @tornado.gen.coroutine
    def get(self):
        host = self.get_argument("host")
        port = int(self.get_argument("port"))
        ioloop = tornado.ioloop.IOLoop.current()
        self.set_header("Content-type", "multipart/x-mixed-replace;boundary=--frame")
        self.served_image_timestamp = time.time()
        my_boundary = "--frame"
        for frame in live_video_stream(host,port=port):
            interval = .1
            if self.served_image_timestamp + interval < time.time():
                img = cv2.imencode('.jpg', frame)[1].tostring()
                self.write(my_boundary)
                self.write("Content-type: image/jpeg\r\n")
                self.write("Content-length: %s\r\n\r\n" % len(img))
                # Serve the image
                self.write(img)
                self.served_image_timestamp = time.time()
                yield tornado.gen.Task(self.flush)
            else:
                yield tornado.gen.Task(ioloop.add_timeout, ioloop.time() + interval)

Elsewhere in my app I have code that's somewhat computationally expensive that I can run without hogging the CPU too much. It follows this pattern:

class Example(tornado.web.RequestHandler):

    executor = ThreadPoolExecutor(3)

    @tornado.concurrent.run_on_executor
    def do_some_stuff(self):
        """Do something that's not asyncio-friendly, and computationally slow"""
        return something

    @tornado.gen.coroutine
    def get(self):
        result = yield self.do_some_stuff()
        self.write(result)

Assuming this second example is the most efficient pattern for streaming video, how can I convert the video handler to look more like this? I inherited the video handler code from someone else, and I don't really know how to replicate the loop part. For example, should I leave the loop in the function that runs on the executor? Should I keep it in the @tornado.gen.coroutine decorated method? I should do something entirely different?

1 Answers

You need to identify the expensive parts of the code and move them to other threads; leave the rest of the code in the coroutine (which is required for making Tornado calls like self.write). For example, my guess is that the main problem is the line

img = cv2.imencode('.jpg', frame)[1].tostring()

To move this to another thread (using more modern tornado idioms than the run_on_executor decorator), put it in a function and run it like this:

def expensive_fn():
    return cv2.imencode('.jpg', frame)[1].tostring()

img = yield ioloop.run_in_executor(None, expensive_fn)

If the problem is for frame in live_video_stream(host,port=port) then you'll need to do a more extensive refactoring to move away from the generator pattern so it can be iterated in a separate thread.

Related