# HG changeset patch # User Drew Perttula # Date 2014-06-05 05:54:18 # Node ID bb92c50438edd0612b8d6e633c26c39ab89f11b1 # Parent 1f877950ad287bf26d360836ee8c650f5dfe6526 picamserve now has /pics that streams jpegs at 10fps Ignore-this: 25d54686d020a02046c79c4c87a8868a diff --git a/bin/picamserve b/bin/picamserve --- a/bin/picamserve +++ b/bin/picamserve @@ -2,9 +2,10 @@ from __future__ import division from run_local import log import sys;sys.path.append('/usr/lib/python2.7/dist-packages/') -import io, logging, traceback +import io, logging, traceback, time import cyclone.web -from twisted.internet import reactor +from twisted.internet import reactor, threads +from twisted.internet.defer import inlineCallbacks from light9 import prof try: @@ -13,12 +14,15 @@ try: except ImportError: class cameraCls(object): def __enter__(self): return self - def __exit__(self): pass + def __exit__(self, *a): pass def capture(self, out, *a, **kw): out.write(open('yuv.demo').read()) + def capture_continuous(self, *a, **kw): + for i in range(1000): + time.sleep(1) + yield i -@prof.logTime -def getFrame(c, arg): +def setCameraParams(c, arg): res = int(arg('res', 480)) c.resolution = { 480: (640, 480), @@ -31,6 +35,9 @@ def getFrame(c, arg): c.brightness = int(arg('brightness', 50)) c.awb_gains = (float(arg('redgain', 1)), float(arg('bluegain', 1))) + c.ISO = int(arg('iso', 250)) + +def setupCrop(c, arg): c.crop = (float(arg('x', 0)), float(arg('y', 0)), float(arg('w', 1)), float(arg('h', 1))) rw = rh = int(arg('resize', 100)) @@ -44,11 +51,16 @@ def getFrame(c, arg): else: # height is the constraint rw = int(scl2 * c.crop[2] * c.resolution[0]) - c.ISO = int(arg('iso', 250)) + return rw, rh + +@prof.logTime +def getFrame(c, arg): + setCameraParams(c, arg) + resize = setupCrop(c, arg) out = io.BytesIO('w') - prof.logTime(c.capture)(out, 'jpeg', use_video_port=True, resize=(rw, rh)) + prof.logTime(c.capture)(out, 'jpeg', use_video_port=True, resize=resize) return out.getvalue() - + class Pic(cyclone.web.RequestHandler): def get(self): try: @@ -56,13 +68,99 @@ class Pic(cyclone.web.RequestHandler): self.write(getFrame(self.settings.camera, self.get_argument)) except Exception: traceback.print_exc() + +def captureContinuousAsync(c, resize, onFrame): + """ + Calls c.capture_continuous is called in another thread. onFrame is + called in this reactor thread with each (frameTime, frame) + result. Runs until onFrame raises StopIteration. + """ + def runner(c, resize): + stream = io.BytesIO() + t = time.time() + for nextFrame in c.capture_continuous(stream, 'jpeg', use_video_port=True, + resize=resize): + t2 = time.time() + log.debug(" - framecap got %s bytes in %.1f ms", + len(stream.getvalue()), 1000 * (t2 - t)) + try: + # This is slow, like 13ms. Hopefully + # capture_continuous is working on gathering the next + # pic during this time instead of pausing. + # Instead, we could be stashing frames onto a queue or + # something that the main thread can pull when + # possible (and toss if it gets behind). + threads.blockingCallFromThread(reactor, + onFrame, t, stream.getvalue()) + except StopIteration: + break + t3 = time.time() + log.debug(" - sending to onFrame took %.1fms", 1000 * (t3 - t2)) + stream.truncate() + stream.seek(0) + t = time.time() + return threads.deferToThread(runner, c, resize) + +class FpsReport(object): + def __init__(self): + self.frameTimes = [] + self.lastFpsLog = 0 + def frame(self): + now = time.time() + + self.frameTimes.append(now) + + if len(self.frameTimes) > 15: + del self.frameTimes[:5] + + if now > self.lastFpsLog + 2 and len(self.frameTimes) > 5: + deltas = [(b - a) for a, b in zip(self.frameTimes[:-1], + self.frameTimes[1:])] + avg = sum(deltas) / len(deltas) + log.info("fps: %.1f", 1 / avg) + self.lastFpsLog = now + + +class Pics(cyclone.web.RequestHandler): + @inlineCallbacks + def get(self): + try: + self.set_header('Content-Type', 'x-application/length-time-jpeg') + c = self.settings.camera + setCameraParams(c, self.get_argument) + resize = setupCrop(c, self.get_argument) + + self.running = True + log.info("connection open from %s", self.request.remote_ip) + fpsReport = FpsReport() + + def onFrame(frameTime, frame): + if not self.running: + raise StopIteration + + now = time.time() + self.write("%s %s\n" % (len(frame), frameTime)) + self.write(frame) + self.flush() + + fpsReport.frame() + + yield captureContinuousAsync(c, resize, onFrame) + except Exception: + traceback.print_exc() + + def on_connection_close(self, *a, **kw): + log.info("connection closed") + self.running = False + log.setLevel(logging.INFO) with cameraCls() as camera: port = 8001 reactor.listenTCP(port, cyclone.web.Application(handlers=[ (r'/pic', Pic), + (r'/pics', Pics), (r'/static/(.*)', cyclone.web.StaticFileHandler, {'path': 'static/'}), (r'/(|gui.js)', cyclone.web.StaticFileHandler, {'path': 'light9/vidref/', 'default_filename': 'index.html'}),