# 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 @@ - 990 - 709 - 500 + vidref + 690 500