# HG changeset patch
# User drewp@bigasterisk.com
# Date 2010-06-14 06:21:09
# Node ID cfd5d5be1b502c6d4efaf1f182a2410e52c5bc3c
# Parent edb75a48fcb8a1a4c8aa0ee9cb16252b48990c7f
vidref complete panels on each replay. replays load and delete pretty well
Ignore-this: 3c0eb5c77caf08b16ab557fae7b46418
diff --git a/bin/vidref b/bin/vidref
--- a/bin/vidref
+++ b/bin/vidref
@@ -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)
diff --git a/light9/vidref/main.py b/light9/vidref/main.py
--- a/light9/vidref/main.py
+++ b/light9/vidref/main.py
@@ -9,14 +9,13 @@ gst-launch dv1394src ! dvdemux name=d !
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 @@ class MusicTime(object):
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 @@ class MusicTime(object):
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 @@ class VideoRecordSink(gst.Element):
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 @@ class VideoRecordSink(gst.Element):
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 @@ class VideoRecordSink(gst.Element):
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 @@ class VideoRecordSink(gst.Element):
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 @@ class Main(object):
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 @@ class Main(object):
}[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 @@ class Main(object):
return e
sink = makeElem("xvimagesink")
- recSink = VideoRecordSink(self.replayViews)
+ recSink = VideoRecordSink(self.musicTime)
self.pipeline.add(recSink)
tee = makeElem("tee")
diff --git a/light9/vidref/replay.py b/light9/vidref/replay.py
--- a/light9/vidref/replay.py
+++ b/light9/vidref/replay.py
@@ -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 @@ class ReplayViews(object):
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 @@ class ReplayViews(object):
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 @@ class ReplayView(object):
"""
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 @@ class Replay(object):
"""
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])
diff --git a/light9/vidref/vidref.glade b/light9/vidref/vidref.glade
--- a/light9/vidref/vidref.glade
+++ b/light9/vidref/vidref.glade
@@ -3,9 +3,8 @@