changeset 1088:bb92c50438ed

picamserve now has /pics that streams jpegs at 10fps Ignore-this: 25d54686d020a02046c79c4c87a8868a
author Drew Perttula <drewp@bigasterisk.com>
date Thu, 05 Jun 2014 05:54:18 +0000
parents 1f877950ad28
children 2ee97997ee56
files bin/picamserve
diffstat 1 files changed, 106 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/bin/picamserve	Wed Jun 04 08:30:45 2014 +0000
+++ b/bin/picamserve	Thu Jun 05 05:54:18 2014 +0000
@@ -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 @@
 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 @@
     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 @@
     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 @@
             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'}),