changeset 1096:087f6cbe4b22

vidrefsetup tool now prepares a url that vidref will use for rpi camera requests Ignore-this: 3b10bb845aa51811f21f63d4a280d2bd
author Drew Perttula <drewp@bigasterisk.com>
date Sun, 08 Jun 2014 09:30:03 +0000
parents f6c5b9b94dae
children f7618f29bb89
files bin/picamserve bin/vidref bin/vidrefsetup light9/vidref/main.py light9/vidref/remotepivideo.py light9/vidref/vidref.html
diffstat 6 files changed, 280 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/bin/picamserve	Sun Jun 08 09:29:45 2014 +0000
+++ b/bin/picamserve	Sun Jun 08 09:30:03 2014 +0000
@@ -33,7 +33,7 @@
     c.exposure_mode = arg('exposure_mode', 'fixedfps')
     c.awb_mode = arg('awb_mode', 'off')
     c.brightness = int(arg('brightness', 50))
-    
+    c.exposure_compensation= int(arg('exposure_compensation', 0))
     c.awb_gains = (float(arg('redgain', 1)), float(arg('bluegain', 1)))
     c.ISO = int(arg('iso', 250))
 
--- a/bin/vidref	Sun Jun 08 09:29:45 2014 +0000
+++ b/bin/vidref	Sun Jun 08 09:30:03 2014 +0000
@@ -9,11 +9,11 @@
 gobject.threads_init()
 import gtk
 import sys, logging, optparse, json
-sys.path.append(".")
-from light9 import networking
+import cyclone.web, cyclone.httpclient, cyclone.websocket
+from light9 import networking, showconfig
 from light9.vidref.main import Gui
 from light9.vidref.replay import snapshotDir
-import cyclone.web, cyclone.httpclient, cyclone.websocket
+from light9.rdfdb.syncedgraph import SyncedGraph
 
  # find replay dirs correctly. show multiple
  # replays. trash. reorder/pin. dump takes that are too short; they're
@@ -50,11 +50,15 @@
 class SnapshotPic(cyclone.web.StaticFileHandler):
     pass
 
-        
-gui = Gui()
+graph = SyncedGraph("vidref")
+
+gui = Gui(graph)
 
 port = networking.vidref.port
 reactor.listenTCP(port, cyclone.web.Application(handlers=[
+    (r'/()', cyclone.web.StaticFileHandler,
+     {'path': 'light9/vidref', 'default_filename': 'vidref.html'}),
+    (r'/static/(.*)', cyclone.web.StaticFileHandler, {'path': 'static/'}),
     (r'/snapshot', Snapshot),
     (r'/snapshot/(.*)', SnapshotPic, {"path": snapshotDir()}),
     ], debug=True, gui=gui))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/vidrefsetup	Sun Jun 08 09:30:03 2014 +0000
@@ -0,0 +1,70 @@
+#!bin/python
+""" this should be part of vidref, but I haven't worked out sharing
+camera captures with a continuous camera capture yet """
+
+from run_local import log
+import sys, optparse, logging, json, subprocess, datetime
+from dateutil.tz import tzlocal
+from twisted.internet import reactor, defer
+import cyclone.web, cyclone.httpclient, cyclone.websocket
+from rdflib import RDF, URIRef, Literal
+import pyjade.utils
+from light9.rdfdb.syncedgraph import SyncedGraph
+from light9.rdfdb.patch import Patch
+from light9.namespaces import L9, DCTERMS
+from light9 import networking, showconfig
+
+from lib.cycloneerr import PrettyErrorHandler
+
+class RedirToCamera(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        return self.redirect(networking.picamserve.path(
+            'pic?' + self.request.query))
+        
+class UrlToCamera(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header('Content-Type', 'text/plain')
+        self.write(networking.picamserve.path('pic'))
+                   
+class VidrefCamRequest(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def get(self):
+        graph = self.settings.graph
+        show = showconfig.showUri()
+        with graph.currentState(tripleFilter=(show, None, None)) as g:
+            ret = g.value(show, L9['vidrefCamRequest'])
+            if ret is None:
+                self.send_error(404)
+            self.redirect(ret)
+            
+    def put(self):
+        graph = self.settings.graph
+        show = showconfig.showUri()
+        graph.patchObject(context=URIRef(show + '/vidrefConfig'),
+                          subject=show,
+                          predicate=L9['vidrefCamRequest'],
+                          newObject=URIRef(self.get_argument('uri')))
+        self.send_error(202)
+
+def main():
+    parser = optparse.OptionParser()
+    parser.add_option("-v", "--verbose", action="store_true",
+                      help="logging.DEBUG")
+    (options, args) = parser.parse_args()
+
+    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
+    graph = SyncedGraph("vidrefsetup")
+
+    # deliberately conflict with vidref since they can't talk at once to cam
+    port = networking.vidref.port 
+
+    reactor.listenTCP(port, cyclone.web.Application(handlers=[
+        (r'/pic', RedirToCamera),
+        (r'/picUrl', UrlToCamera),
+        (r'/vidrefCamRequest', VidrefCamRequest),
+        (r'/static/(.*)', cyclone.web.StaticFileHandler, {'path': 'static/'}),
+        (r'/()', cyclone.web.StaticFileHandler, {'path': 'light9/vidref/', 'default_filename': 'vidref.html'}),
+        ], debug=True, graph=graph))
+    log.info("serving on %s" % port)
+    reactor.run()
+
+main()
--- a/light9/vidref/main.py	Sun Jun 08 09:29:45 2014 +0000
+++ b/light9/vidref/main.py	Sun Jun 08 09:30:03 2014 +0000
@@ -17,7 +17,7 @@
 log = logging.getLogger()
 
 class Gui(object):
-    def __init__(self):
+    def __init__(self, graph):
         wtree = gtk.Builder()
         wtree.add_from_file(sibpath(__file__, "vidref.glade"))
         mainwin = wtree.get_object("MainWindow")
@@ -51,7 +51,7 @@
                 liveVideo=vid3,
                 musicTime=self.musicTime,
                 recordingTo=self.recordingTo,
-                picsUrl=networking.picamserve.path('pics?res=1080&resize=450&x=0&y=.3&w=1&h=.5&awb_mode=auto&exposure_mode=auto'))
+                graph=graph)
 
         vid3.props.width_request = 360
         vid3.props.height_request = 220
--- a/light9/vidref/remotepivideo.py	Sun Jun 08 09:29:45 2014 +0000
+++ b/light9/vidref/remotepivideo.py	Sun Jun 08 09:30:03 2014 +0000
@@ -7,23 +7,34 @@
 import treq
 from twisted.internet import defer
 from light9.vidref.replay import framerate, songDir, takeDir, snapshotDir
-from light9 import prof
+from light9 import prof, showconfig
+from light9.namespaces import L9
 from PIL import Image
 from StringIO import StringIO
 log = logging.getLogger('remotepi')
 
 class Pipeline(object):
-    def __init__(self, liveVideo, musicTime, recordingTo, picsUrl):
+    def __init__(self, liveVideo, musicTime, recordingTo, graph):
         self.musicTime = musicTime
         self.recordingTo = recordingTo
 
         self.liveVideo = self._replaceLiveVideoWidget(liveVideo)
         
-        self._startRequest(picsUrl)
-        self._buffer = ''
+        self._snapshotRequests = []
+        self.graph = graph
+        self.graph.addHandler(self.updateCamUrl)
 
-        self._snapshotRequests = []
-
+    def updateCamUrl(self):
+        show = showconfig.showUri()
+        self.picsUrl = self.graph.value(show, L9['vidrefCamRequest'])
+        log.info("picsUrl now %r", self.picsUrl)
+        if not self.picsUrl:
+            return
+        
+        # this cannot yet survive being called a second time
+        self._startRequest(str(self.picsUrl.replace('/pic', '/pics')) +
+                           '&res=1080&resize=450')
+        
     def _replaceLiveVideoWidget(self, liveVideo):
         aspectFrame = liveVideo.get_parent()
         liveVideo.destroy()
@@ -34,6 +45,7 @@
         return img
         
     def _startRequest(self, url):
+        self._buffer = ''
         d = treq.get(url)
         d.addCallback(treq.collect, self._dataReceived)
         # not sure how to stop this
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/vidref/vidref.html	Sun Jun 08 09:30:03 2014 +0000
@@ -0,0 +1,180 @@
+<!doctype html>
+<html>
+  <head>
+    <title>vidref</title>
+    <meta charset="utf-8" />
+    <link rel="stylesheet" href="static/tapmodo-Jcrop-1902fbc/css/jquery.Jcrop.min.css" type="text/css">
+    <style>
+     body {
+       background: black;
+       color: rgb(170, 170, 170);
+       font-family: sans-serif; 
+     }
+     a {
+       color: rgb(163, 163, 255);
+     }
+     input[type=range] { width: 400px; }
+     .smallUrl { font-size: 60%; }
+
+     .jcrop-holder {
+       position: absolute !important;
+       top: 0 !important;
+       background-color: initial !important;
+     }
+    </style>
+  </head>
+  <body>
+    <h1>video setup</h1>
+    
+    <div>Camera view</div>
+    <div>
+      <div style="position: relative; display: inline-block">
+        <img id="cam" src="pic?resize=500&awb_mode=auto&exposure_mode=auto&shutter=100000">
+        <div id="cover" style="position: absolute; left: 0; top: 0; right: 0; bottom: 0;"></div>
+      </div>
+    </div>
+   
+    <fieldset>
+      <legend>set these</legend>
+      <div><label>shutter <input type="range" min="1" max="100000" data-bind="value: params.shutter, valueUpdate: 'input'"></label></div>
+      <div><label>brightness <span data-bind="text: params.brightness"></span> <input type="range" min="0" max="100" step="1" data-bind="value: params.brightness, valueUpdate: 'input'"></label></div>
+      <div><label>exposure_mode
+        <select data-bind="value: params.exposure_mode">
+          <option>auto</option>
+          <option>fireworks</option>
+          <option>verylong</option>
+          <option>fixedfps</option>
+          <option>backlight</option>
+          <option>antishake</option>
+          <option>snow</option>
+          <option>sports</option>
+          <option>nightpreview</option>
+          <option>night</option>
+          <option>beach</option>
+          <option>spotlight</option>
+        </select>           
+      </label></div>
+      <div><label>exposure_compensation <span data-bind="text: params.exposure_compensation"></span> <input type="range" min="-25" max="25" step="1" data-bind="value: params.exposure_compensation, valueUpdate: 'input'"></label></div>
+      <div><label>awb_mode
+        <select data-bind="value: params.awb_mode">
+          <option>horizon</option>
+          <option>off</option>
+          <option>cloudy</option>
+          <option>shade</option>
+          <option>fluorescent</option>
+          <option>tungsten</option>
+          <option>auto</option>
+          <option>flash</option>
+          <option>sunlight</option>
+          <option>incandescent</option>
+        </select>
+      </label></div>
+      <div><label>redgain <input type="range" min="0" max="8" step=".1" data-bind="value: params.redgain, valueUpdate: 'input'"></label></div>
+      <div><label>bluegain <input type="range" min="0" max="8" step=".1" data-bind="value: params.bluegain, valueUpdate: 'input'"></label></div>
+      <div><label>iso <input type="range" min="100" max="800" step="20" list="isos" data-bind="value: params.iso, valueUpdate: 'input'"></label></div>
+      <datalist id="isos">
+        <option>100</option>
+        <option>200</option>
+        <option>320</option>
+        <option>400</option>
+        <option>500</option>
+        <option>640</option>
+        <option>800</option>
+      </datalist>
+      <div>See <a href="http://picamera.readthedocs.org/en/release-1.4/api.html#picamera.PiCamera.ISO">picamera attribute docs</a></div>
+    </fieldset>
+
+    <div>Resulting url: <a class="smallUrl" data-bind="attr: {href: currentUrl}, text: currentUrl"></a></div>
+
+    <div>Resulting crop image:</div>
+    <div><img id="cropped"></div>
+
+    
+    <script src="static/knockout-3.1.0.js"></script>
+    <script src="static/jquery-2.1.1.min.js"></script>
+    <script src="static/underscore-min.js"></script>
+    <script src="static/tapmodo-Jcrop-1902fbc/js/jquery.Jcrop.js"></script>
+    <script>
+     jQuery(function () {
+       var model = {
+         baseUrl: ko.observable(),
+         crop: ko.observable({x: 0, y: 0, w: 1, h: 1}),
+         params: {
+           shutter: ko.observable(50000),
+           exposure_mode: ko.observable('auto'),
+           awb_mode: ko.observable('auto'),
+           brightness: ko.observable(50),
+           redgain: ko.observable(1),
+           bluegain: ko.observable(1),
+           iso: ko.observable(250),
+           exposure_compensation: ko.observable(0),
+         }
+       };
+       model.currentUrl = ko.computed(assembleCamUrlWithCrop);
+
+       function getBaseUrl() {
+         $.ajax({
+           url: 'picUrl',
+           success: model.baseUrl
+         });
+       }
+       
+       function imageUpdatesForever(model, img, onFirstLoad) {
+         var everLoaded = false;
+         function onLoad(ev) {
+           if (ev.type == 'load' && !everLoaded) {
+             everLoaded = true;
+             onFirstLoad();
+           }
+           
+           var src = assembleCamUrl() + '&t=' + (+new Date());
+           img.src = src;
+
+           $("#cropped").attr({src: assembleCamUrlWithCrop()});
+         }
+         img.addEventListener('load', onLoad);
+         img.addEventListener('error', onLoad);
+         
+         onLoad({type: '<startup>'})
+       }
+       
+       function assembleCamUrl() {
+         if (!model.baseUrl()) {
+           return '#';
+         }
+         return model.baseUrl() + '?resize=500&' + $.param(ko.toJS(model.params));
+       }
+       
+       function assembleCamUrlWithCrop() {
+         return assembleCamUrl() + '&' + $.param(model.crop());
+       }
+       
+       getBaseUrl();
+       
+       imageUpdatesForever(model, document.getElementById('cam'), function onFirstLoad() {
+         var crop = $('#cover').Jcrop({onChange: function (c) {
+           var size = this.getBounds();
+           model.crop({x: c.x / size[0], y: c.y / size[1], w: c.w / size[0], h: c.h / size[1]});
+         }}, function() {
+           this.setSelect([50, 50, 100, 100]);
+         });
+       });
+
+       var putVidrefCamRequest = _.debounce(
+           function(uri) {
+             $.ajax({
+               type: 'PUT',
+               url: 'vidrefCamRequest',
+               data: {uri: uri}
+             });
+           }, 1000);
+       ko.computed(function saver() {
+         var uri = assembleCamUrlWithCrop();
+         putVidrefCamRequest(uri);
+       });
+
+       ko.applyBindings(model);
+     });
+    </script>
+  </body>
+</html>