diff --git a/bin/vidref b/bin/vidref --- a/bin/vidref +++ b/bin/vidref @@ -2,14 +2,16 @@ from run_local import log from twisted.internet import gtk2reactor gtk2reactor.install() -from twisted.internet import reactor +from twisted.internet import reactor, defer import gobject gobject.threads_init() import gtk -import sys, logging, optparse +import sys, logging, optparse, json sys.path.append(".") +from light9 import networking from light9.vidref.main import Gui - +from light9.vidref.replay import snapshotDir +import cyclone.web, cyclone.httpclient, cyclone.websocket # find replay dirs correctly. show multiple # replays. trash. reorder/pin. dump takes that are too short; they're @@ -27,8 +29,38 @@ log.handlers[0].setLevel(logging.DEBUG i logging.getLogger("restkit.client").setLevel(logging.WARN) +class Snapshot(cyclone.web.RequestHandler): + @defer.inlineCallbacks + def post(self): + # save next pic + # return /snapshot/path + try: + outputFilename = yield self.settings.gui.snapshot() + + assert outputFilename.startswith(snapshotDir()) + out = networking.vidref.path( + "snapshot/%s" % outputFilename[len(snapshotDir()):].lstrip('/')) + + self.write(json.dumps({'snapshot': out})) + self.set_header("Location", out) + self.set_status(303) + except Exception as e: + import traceback + traceback.print_exc() + raise + +class SnapshotPic(cyclone.web.StaticFileHandler): + pass + -start=Gui() +gui = Gui() + +port = networking.vidref.port +reactor.listenTCP(port, cyclone.web.Application(handlers=[ + (r'/snapshot', Snapshot), + (r'/snapshot/(.*)', SnapshotPic, {"path": snapshotDir()}), + ], debug=True, gui=gui)) +log.info("serving on %s" % port) reactor.run() diff --git a/light9/networking.py b/light9/networking.py --- a/light9/networking.py +++ b/light9/networking.py @@ -38,3 +38,4 @@ dmxServer = ServiceAddress(L9['dmxServer musicPlayer = ServiceAddress(L9['musicPlayer']) keyboardComposer = ServiceAddress(L9['keyboardComposer']) curveCalc = ServiceAddress(L9['curveCalc']) +vidref = ServiceAddress(L9['vidref']) diff --git a/light9/vidref/main.py b/light9/vidref/main.py --- a/light9/vidref/main.py +++ b/light9/vidref/main.py @@ -51,6 +51,10 @@ class Gui(object): gobject.timeout_add(1000 // framerate, self.updateLoop) + + def snapshot(self): + return self.pipeline.snapshot() + def attachLog(self, textBuffer): """write log lines to this gtk buffer""" class ToBuffer(logging.Handler): diff --git a/light9/vidref/replay.py b/light9/vidref/replay.py --- a/light9/vidref/replay.py +++ b/light9/vidref/replay.py @@ -16,6 +16,9 @@ def takeDir(songDir, startTime): """ return os.path.join(songDir, str(int(startTime))) +def snapshotDir(): + return os.path.expanduser("~/light9-vidref/snapshot") + class ReplayViews(object): """ the whole list of replay windows. parent is the scrolling area for @@ -160,6 +163,7 @@ class ReplayView(object): replayPanel.pack_start(stack, False, False, 0) parent.pack_start(replayPanel, False, False) + log.debug("packed ReplayView %s" % replayPanel) self.replayPanel = replayPanel def destroy(self): diff --git a/light9/vidref/videorecorder.py b/light9/vidref/videorecorder.py --- a/light9/vidref/videorecorder.py +++ b/light9/vidref/videorecorder.py @@ -4,8 +4,9 @@ import gst, gobject, time, logging, os, import gtk import Image from threading import Thread -from Queue import Queue -from light9.vidref.replay import framerate, songDir, takeDir +from twisted.internet import defer +from Queue import Queue, Empty +from light9.vidref.replay import framerate, songDir, takeDir, snapshotDir log = logging.getLogger() class Pipeline(object): @@ -13,7 +14,29 @@ class Pipeline(object): self.musicTime = musicTime self.liveVideoXid = liveVideoXid self.recordingTo = recordingTo - + self.snapshotRequests = Queue() + + try: + os.makedirs(snapshotDir()) + except OSError: + pass + + def snapshot(self): + """ + returns deferred to the path (which is under snapshotDir()) where + we saved the image. This callback comes from another thread, + but I haven't noticed that being a problem yet. + """ + d = defer.Deferred() + def req(frame): + filename = "%s/%s.jpg" % (snapshotDir(), time.time()) + log.debug("received snapshot; saving in %s", filename) + frame.save(filename) + d.callback(filename) + log.debug("requesting snapshot") + self.snapshotRequests.put(req) + return d + def setInput(self, name): sourcePipe = { "auto": "autovideosrc name=src1", @@ -45,7 +68,7 @@ class Pipeline(object): with gtk.gdk.lock: self.recordingTo.set_text(t) self._lastRecText = t - recSink = VideoRecordSink(self.musicTime, setRec) + recSink = VideoRecordSink(self.musicTime, setRec, self.snapshotRequests) self.pipeline.add(recSink) tee = makeElem("tee") @@ -76,9 +99,10 @@ class VideoRecordSink(gst.Element): gst.PAD_ALWAYS, gst.caps_new_any()) - def __init__(self, musicTime, updateRecordingTo): + def __init__(self, musicTime, updateRecordingTo, snapshotRequests): gst.Element.__init__(self) self.updateRecordingTo = updateRecordingTo + self.snapshotRequests = snapshotRequests self.sinkpad = gst.Pad(self._sinkpadtemplate, "sink") self.add_pad(self.sinkpad) self.sinkpad.set_chain_function(self.chainfunc) @@ -96,6 +120,13 @@ class VideoRecordSink(gst.Element): args = imagesToSave.get() self.saveImg(*args) imagesToSave.task_done() + try: + req = self.snapshotRequests.get(block=False) + except Empty: + pass + else: + req(args[1]) + self.snapshotRequests.task_done() t = Thread(target=imageSaver) t.setDaemon(True) diff --git a/show/dance2013/networking.n3 b/show/dance2013/networking.n3 --- a/show/dance2013/networking.n3 +++ b/show/dance2013/networking.n3 @@ -7,4 +7,5 @@ sh:netHome :musicPlayer ; :dmxServer ; :curveCalc ; - :keyboardComposer . \ No newline at end of file + :keyboardComposer ; + :vidref . \ No newline at end of file