changeset 542:cfd5d5be1b50

vidref complete panels on each replay. replays load and delete pretty well Ignore-this: 3c0eb5c77caf08b16ab557fae7b46418
author drewp@bigasterisk.com
date Mon, 14 Jun 2010 06:21:09 +0000
parents edb75a48fcb8
children 4d500e6dc2f7
files bin/vidref light9/vidref/main.py light9/vidref/replay.py light9/vidref/vidref.glade
diffstat 4 files changed, 196 insertions(+), 52 deletions(-) [+]
line wrap: on
line diff
--- a/bin/vidref	Sun Jun 13 09:41:45 2010 +0000
+++ b/bin/vidref	Mon Jun 14 06:21:09 2010 +0000
@@ -2,16 +2,23 @@
 import gobject
 gobject.threads_init()
 import gtk
-import sys, logging
+import sys, logging, optparse
 sys.path.append(".")
 from light9.vidref.main import Main
 
- # find replay dirs correctly. show multiple replays. trash. reorder/pin.
-                
+ # find replay dirs correctly. show multiple
+ # replays. trash. reorder/pin. dump takes that are too short; they're
+ # just from seeking
+
+parser = optparse.OptionParser()
+parser.add_option("-v", "--verbose", action="store_true",
+                  help="logging.DEBUG")
+(options, args) = parser.parse_args()
+
 
 logging.basicConfig()
 log = logging.getLogger()
-log.setLevel(logging.DEBUG)
+log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 logging.getLogger("restkit.client").setLevel(logging.WARN)
 
 
--- a/light9/vidref/main.py	Sun Jun 13 09:41:45 2010 +0000
+++ b/light9/vidref/main.py	Mon Jun 14 06:21:09 2010 +0000
@@ -9,14 +9,13 @@
 import pygst
 pygst.require("0.10")
 import gst, gobject, time, jsonlib, restkit, logging, os, traceback
-from decimal import Decimal
 import gtk
 from twisted.python.util import sibpath
 import Image
 from threading import Thread
 from Queue import Queue
 from light9 import networking
-from light9.vidref.replay import ReplayViews, songDir, takeDir
+from light9.vidref.replay import ReplayViews, songDir, takeDir, framerate
 
 log = logging.getLogger()
 
@@ -26,7 +25,13 @@
     upon request, adjusted to be more precise with the system clock
     """
     def __init__(self, period=.2):
-        """period is the seconds between http time requests."""
+        """period is the seconds between http time requests.
+
+        The choice of period doesn't need to be tied to framerate,
+        it's more the size of the error you can tolerate (since we
+        make up times between the samples, and we'll just run off the
+        end of a song)
+        """
         self.period = period
         self.musicResource = restkit.Resource(networking.musicUrl())
         t = Thread(target=self._timeUpdate)
@@ -48,7 +53,13 @@
         while True:
             position = jsonlib.loads(self.musicResource.get("time").body,
                                      use_float=True)
+
+            # this is meant to be the time when the server gave me its
+            # report, and I don't know if that's closer to the
+            # beginning of my request or the end of it (or some
+            # fraction of the way through)
             self.positionFetchTime = time.time()
+            
             self.position = position
             time.sleep(self.period)
         
@@ -58,15 +69,14 @@
                                         gst.PAD_ALWAYS,
                                         gst.caps_new_any())
 
-    def __init__(self, replay):
+    def __init__(self, musicTime):
         gst.Element.__init__(self)
-        self.replay = replay
         self.sinkpad = gst.Pad(self._sinkpadtemplate, "sink")
         self.add_pad(self.sinkpad)
         self.sinkpad.set_chain_function(self.chainfunc)
         self.lastTime = 0
         
-        self.musicTime = MusicTime()
+        self.musicTime = musicTime
 
         self.imagesToSave = Queue()
         self.startBackgroundImageSaver(self.imagesToSave)
@@ -84,9 +94,6 @@
         t.start()
 
     def chainfunc(self, pad, buffer):
-        global nextImageCb
-        self.info("%s timestamp(buffer):%d" % (pad, buffer.timestamp))
-
         position = self.musicTime.getLatest()
 
         if not position['song']:
@@ -101,14 +108,10 @@
         except:
             traceback.print_exc()
 
-        try:
-            self.replay.update(position)
-        except:
-            traceback.print_exc()
-
         return gst.FLOW_OK
 
     def saveImg(self, position, img, bufferTimestamp):
+        t1 = time.time()
         outDir = takeDir(songDir(position['song']), position['started'])
         outFilename = "%s/%08.03f.jpg" % (outDir, position['t'])
         if os.path.exists(outFilename): # we're paused on one time
@@ -122,23 +125,24 @@
         img.save(outFilename)
 
         now = time.time()
-        log.info("wrote %s delay of %.2fms %s",
-                 outFilename,
-                 (now - self.lastTime) * 1000,
-                 bufferTimestamp)
+        log.debug("wrote %s delay of %.2fms, took %.2fms",
+                  outFilename,
+                  (now - self.lastTime) * 1000,
+                  (now - t1) * 1000)
         self.lastTime = now
 
 gobject.type_register(VideoRecordSink)
 
 class Main(object):
     def __init__(self):
+        self.musicTime = MusicTime()
         wtree = gtk.Builder()
         wtree.add_from_file(sibpath(__file__, "vidref.glade"))
         mainwin = wtree.get_object("MainWindow")
         mainwin.connect("destroy", gtk.main_quit)
         wtree.connect_signals(self)
 
-        wtree.get_object("replayPanel").show()
+        # wtree.get_object("replayPanel").show() # demo only
         rp = wtree.get_object("replayVbox")
         self.replayViews = ReplayViews(rp)
 
@@ -147,6 +151,17 @@
 
         self.setInput('dv')
 
+        gobject.timeout_add(1000 // framerate, self.updateLoop)
+
+    def updateLoop(self):
+        position = self.musicTime.getLatest()
+        try:
+            with gtk.gdk.lock:
+                self.replayViews.update(position)
+        except:
+            traceback.print_exc()
+        return True
+
     def getInputs(self):
         return ['auto', 'dv', 'video0']
 
@@ -159,9 +174,9 @@
             }[name]
 
         cam = (sourcePipe + " ! "
-              "videorate ! video/x-raw-yuv,framerate=15/1 ! "
+              "videorate ! video/x-raw-yuv,framerate=%s/1 ! "
               "videoscale ! video/x-raw-yuv,width=320,height=240;video/x-raw-rgb,width=320,height=240 ! "
-              "queue name=vid")
+              "queue name=vid" % framerate)
 
         self.pipeline = gst.parse_launch(cam)
 
@@ -171,7 +186,7 @@
             return e
         
         sink = makeElem("xvimagesink")
-        recSink = VideoRecordSink(self.replayViews)
+        recSink = VideoRecordSink(self.musicTime)
         self.pipeline.add(recSink)
 
         tee = makeElem("tee")
--- a/light9/vidref/replay.py	Sun Jun 13 09:41:45 2010 +0000
+++ b/light9/vidref/replay.py	Mon Jun 14 06:21:09 2010 +0000
@@ -1,7 +1,10 @@
-import os, gtk
+from __future__ import division
+import os, gtk, shutil, logging, time
 from bisect import bisect_left
 from decimal import Decimal
+log = logging.getLogger()
 
+framerate = 15
 
 def songDir(song):
     return "/tmp/vidref/play-%s" % song.split('://')[-1].replace('/','_')
@@ -20,7 +23,7 @@
     def __init__(self, parent):
         # today, parent is the vbox the replay windows should appear in
         self.parent = parent
-        self.lastSong = None
+        self.lastStart = None
 
         self.views = []
      
@@ -29,22 +32,36 @@
         freshen all replay windows. We get called this about every
         time there's a new live video frame.
 
-        may be responsible for making new children if we change song
+        Calls loadViewsForSong if we change songs, or even if we just
+        restart the playback of the current song (since there could be
+        a new replay view)
         """
-        if position['song'] != self.lastSong:
+        t1 = time.time()
+        if position['started'] != self.lastStart:
             self.loadViewsForSong(position['song'])
-            self.lastSong = position['song']
+            self.lastStart = position['started']
         for v in self.views:
             v.updatePic(position)
-
+        log.debug("update %s views in %.2fms",
+                  len(self.views), (time.time() - t1) * 1000)
 
     def loadViewsForSong(self, song):
-        # remove previous ones
-        
-        takes = os.listdir(songDir(song))
+        """
+        replace previous views, and cleanup short ones
+        """
+        for v in self.views:
+            v.destroy()
+        self.views[:] = []
+
+        takes = sorted(os.listdir(songDir(song)))
         for take in takes:
             td = takeDir(songDir(song), take)
-            rv = ReplayView(self.parent, Replay(td))
+            r = Replay(td)
+            if r.tooShort():
+                log.warn("cleaning up %s; too short" % r.takeDir)
+                r.deleteDir()
+                continue
+            rv = ReplayView(self.parent, r)
             self.views.append(rv)
 
 class ReplayView(object):
@@ -53,17 +70,100 @@
     """
     def __init__(self, parent, replay):
         self.replay = replay
+        self.enabled = True
 
         # this *should* be a composite widget from glade
-        img = gtk.Image()
-        img.set_size_request(320, 240)
-        parent.pack_end(img, False, False)
-        img.show()
-        self.picWidget = img
-        
-#        self.picWidget = parent.get_children()[0].get_child()
+
+        delImage = gtk.Image()
+        delImage.set_visible(True)
+        delImage.set_from_stock("gtk-delete", gtk.ICON_SIZE_BUTTON)
+
+        def withLabel(cls, label):
+            x = cls()
+            x.set_visible(True)
+            x.set_label(label)
+            return x
+
+        def labeledProperty(key, value, width=12):
+            lab = withLabel(gtk.Label, key)
+
+            ent = gtk.Entry()
+            ent.set_visible(True)
+            ent.props.editable = False
+            ent.props.width_chars = width
+            ent.props.text = value
+
+            cols = gtk.HBox()
+            cols.set_visible(True)
+            cols.add(lab)
+            cols.add(ent)
+            return cols
+
+        replayPanel = gtk.HBox()
+        replayPanel.set_visible(True)
+        if True:
+            af = gtk.AspectFrame()
+            af.set_visible(True)
+            af.set_size_request(320, 240)
+            af.set_shadow_type(gtk.SHADOW_OUT)
+            af.props.obey_child = True
+
+            img = gtk.Image()
+            img.set_visible(True)
+            img.set_size_request(320, 240)
+            self.picWidget = img
+
+            af.add(img)
+            replayPanel.pack_start(af, False, False, 0)
+
+        if True:
+            rows = []
+            rows.append(labeledProperty("Started:", self.replay.getTitle()))
+            rows.append(labeledProperty("Seconds:", self.replay.getDuration()))
+            if True:
+                en = withLabel(gtk.ToggleButton, "Enabled")
+                en.set_active(True)
+                def tog(w):
+                    self.enabled = w.get_active()
+                en.connect("toggled", tog)
+                rows.append(en)
+            if True:
+                d = withLabel(gtk.Button, "Delete")
+                d.props.image = delImage
+                def onClicked(w):
+                    self.replay.deleteDir()
+                    self.destroy()
+                d.connect("clicked", onClicked)
+                rows.append(d)
+            if True:
+                pin = withLabel(gtk.CheckButton, "Pin to top")
+                pin.props.draw_indicator = True
+                rows.append(pin)
+
+            stack = gtk.VBox()
+            stack.set_visible(True)
+            for r in rows:
+                stack.add(r)
+                stack.set_child_packing(r, False, False, 0, gtk.PACK_START)
+            
+            replayPanel.pack_start(stack, False, False, 0)
+
+        parent.pack_start(replayPanel, False, False)
+        self.replayPanel = replayPanel
+
+    def destroy(self):
+        self.replayPanel.destroy()
+        self.enabled = False
         
     def updatePic(self, position):
+
+        # this should skip updating off-screen widgets! maybe that is
+        # done by declaring the widget dirty and then reacting to a
+        # paint message if one comes
+
+        if not self.enabled:
+            return
+        
         inPic = self.replay.findClosestFrame(position['t']+.25)
         with gtk.gdk.lock:
             self.picWidget.set_from_file(inPic)
@@ -78,11 +178,34 @@
     """
     def __init__(self, takeDir):
         self.takeDir = takeDir
+        self.existingFrames = sorted([Decimal(f.split('.jpg')[0])
+                                 for f in os.listdir(self.takeDir)])
+
+    def tooShort(self, minSeconds=5):
+        return len(self.existingFrames) < (minSeconds * framerate)
+
+    def deleteDir(self):
+        shutil.rmtree(self.takeDir)
+
+    def getTitle(self):
+        tm = time.localtime(int(os.path.basename(self.takeDir)))
+        return time.strftime("%a %H:%M:%S", tm)
+
+    def getDuration(self):
+        """total number of seconds represented, which is most probably
+        a continuous section, but we aren't saying where in the song
+        that is"""
+        return "%.1f" % (len(self.existingFrames) / framerate)
 
     def findClosestFrame(self, t):
-        existingFrames = sorted([Decimal(f.split('.jpg')[0])
-                                 for f in os.listdir(self.takeDir)])
-        i = bisect_left(existingFrames, Decimal(str(t)))
-        if i >= len(existingFrames):
-            i = len(existingFrames) - 1
-        return os.path.join(self.takeDir, "%08.03f.jpg" % existingFrames[i])
+        # this is weird to be snapping our playback time to the frames
+        # on disk. More efficient and accurate would be to schedule
+        # the disk frames to playback exactly as fast as they want
+        # to. This might spread cpu load since the recorded streams
+        # might be a little more out of phase. It would also
+        # accomodate changes in framerate between playback streams.
+        i = bisect_left(self.existingFrames, Decimal(str(t)))
+        if i >= len(self.existingFrames):
+            i = len(self.existingFrames) - 1
+        return os.path.join(self.takeDir, "%08.03f.jpg" %
+                            self.existingFrames[i])
--- a/light9/vidref/vidref.glade	Sun Jun 13 09:41:45 2010 +0000
+++ b/light9/vidref/vidref.glade	Mon Jun 14 06:21:09 2010 +0000
@@ -3,9 +3,8 @@
   <requires lib="gtk+" version="2.16"/>
   <!-- interface-naming-policy project-wide -->
   <object class="GtkWindow" id="MainWindow">
-    <property name="width_request">990</property>
-    <property name="height_request">709</property>
-    <property name="default_width">500</property>
+    <property name="title" translatable="yes">vidref</property>
+    <property name="default_width">690</property>
     <property name="default_height">500</property>
     <child>
       <object class="GtkHBox" id="hbox3">