diff --git a/.boring b/.boring --- a/.boring +++ b/.boring @@ -158,6 +158,7 @@ ^show/dance..../capture ^show/dance2017/networking.n3 ^show/dance2019/model/ +^show/dance2019/video/ ^timelapse/ __pycache__ rgbled/build-nano328/ diff --git a/bin/vidref b/bin/vidref --- a/bin/vidref +++ b/bin/vidref @@ -18,13 +18,15 @@ from run_local import log from twisted.internet import reactor, defer -import logging, optparse, json, base64 +import logging, optparse, json, base64, os, glob import cyclone.web, cyclone.httpclient, cyclone.websocket -from light9 import networking -from light9.vidref.replay import snapshotDir +from light9 import networking, showconfig from light9.vidref import videorecorder +from rdflib import URIRef +from light9.newtypes import Song +from light9.namespaces import L9 from rdfdb.syncedgraph import SyncedGraph -from io import BytesIO +from cycloneerr import PrettyErrorHandler parser = optparse.OptionParser() parser.add_option("-v", "--verbose", action="store_true", help="logging.DEBUG") @@ -40,11 +42,12 @@ class Snapshot(cyclone.web.RequestHandle # save next pic # return /snapshot/path try: + snapshotDir = 'todo' outputFilename = yield self.settings.gui.snapshot() - assert outputFilename.startswith(snapshotDir()) + assert outputFilename.startswith(snapshotDir) out = networking.vidref.path( - "snapshot/%s" % outputFilename[len(snapshotDir()):].lstrip('/')) + "snapshot/%s" % outputFilename[len(snapshotDir):].lstrip('/')) self.write(json.dumps({'snapshot': out})) self.set_header("Location", out) @@ -71,15 +74,11 @@ class Live(cyclone.websocket.WebSocketHa def onFrame(self, cf: videorecorder.CaptureFrame): if cf is None: return - output = BytesIO() - cf.img.save(output, 'jpeg', quality=80) self.sendMessage( json.dumps({ - 'jpeg': - base64.b64encode(output.getvalue()).decode('ascii'), - 'description': - f't={cf.t}', + 'jpeg': base64.b64encode(cf.asJpeg()).decode('ascii'), + 'description': f't={cf.t}', })) @@ -97,9 +96,48 @@ class Time(cyclone.web.RequestHandler): self.set_status(202) -#graph = SyncedGraph(networking.rdfdb.url, "vidref") -outVideos = videorecorder.FramesToVideoFiles(pipeline.liveImages) -#outVideos.save('/tmp/mov1') +def takeUri(songPath: bytes): + p = songPath.decode('ascii').split('/') + take = p[-1].replace('.mp4', '') + song = p[-2].split('_') + return URIRef('/'.join( + ['http://light9.bigasterisk.com/show', song[-2], song[-1], take])) + + +class ReplayMap(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + song = Song(self.get_argument('song')) + clips = [] + for vid in glob.glob(os.path.join(videorecorder.songDir(song), + b'*.mp4')): + pts = [] + for line in open(vid.replace(b'.mp4', b'.timing'), 'rb'): + _v, vt, _eq, _song, st = line.split() + pts.append([float(st), float(vt)]) + + url = vid[len(os.path.dirname(os.path.dirname(showconfig.root())) + ):].decode('ascii') + + clips.append({ + 'uri': takeUri(vid), + 'videoUrl': url, + 'songToVideo': pts + }) + + clips.sort(key=lambda c: len(c['songToVideo'])) + clips = clips[-3:] + clips.sort(key=lambda c: c['uri'], reverse=True) + + ret = json.dumps(clips) + log.info('replayMap had %s videos; json is %s bytes', len(clips), + len(ret)) + self.write(ret) + + +graph = SyncedGraph(networking.rdfdb.url, "vidref") +outVideos = videorecorder.FramesToVideoFiles( + pipeline.liveImages, os.path.join(showconfig.root(), b'video')) port = networking.vidref.port reactor.listenTCP( @@ -115,9 +153,10 @@ reactor.listenTCP( 'default_filename': 'setup.html' }), (r'/live', Live), + (r'/replayMap', ReplayMap), (r'/snapshot', Snapshot), (r'/snapshot/(.*)', SnapshotPic, { - "path": snapshotDir() + "path": 'todo', }), (r'/time', Time), ], diff --git a/light9/ascoltami/webapp.py b/light9/ascoltami/webapp.py --- a/light9/ascoltami/webapp.py +++ b/light9/ascoltami/webapp.py @@ -1,16 +1,16 @@ -import json, socket, subprocess, os +import json, socket, subprocess, os, logging, time from cyclone import template from rdflib import URIRef -import cyclone.web +import cyclone.web, cyclone.websocket from greplin.scales.cyclonehandler import StatsHandler from cycloneerr import PrettyErrorHandler from light9.namespaces import L9 from light9.showconfig import getSongsFromShow, songOnDisk - +from twisted.internet import reactor _songUris = {} # locationUri : song - +log = logging.getLogger() loader = template.Loader(os.path.dirname(__file__)) @@ -46,30 +46,32 @@ def playerSongUri(graph, player): return None +def currentState(graph, player): + if player.isAutostopped(): + nextAction = 'finish' + elif player.isPlaying(): + nextAction = 'disabled' + else: + nextAction = 'play' + + return { + "song": playerSongUri(graph, player), + "started": player.playStartTime, + "duration": player.duration(), + "playing": player.isPlaying(), + "t": player.currentTime(), + "state": player.states(), + "next": nextAction, + } + + class timeResource(PrettyErrorHandler, cyclone.web.RequestHandler): def get(self): player = self.settings.app.player graph = self.settings.app.graph self.set_header("Content-Type", "application/json") - - if player.isAutostopped(): - nextAction = 'finish' - elif player.isPlaying(): - nextAction = 'disabled' - else: - nextAction = 'play' - - self.write( - json.dumps({ - "song": playerSongUri(graph, player), - "started": player.playStartTime, - "duration": player.duration(), - "playing": player.isPlaying(), - "t": player.currentTime(), - "state": player.states(), - "next": nextAction, - })) + self.write(json.dumps(currentState(graph, player))) def post(self): """ @@ -89,6 +91,28 @@ class timeResource(PrettyErrorHandler, c self.write("ok") +class timeStreamResource(cyclone.websocket.WebSocketHandler): + + def connectionMade(self, *args, **kwargs) -> None: + self.lastSent = None + self.lastSentTime = 0. + self.loop() + + def loop(self): + now = time.time() + msg = currentState(self.settings.app.graph, self.settings.app.player) + if msg != self.lastSent or now > self.lastSentTime + 2: + self.sendMessage(json.dumps(msg)) + self.lastSent = msg + self.lastSentTime = now + + if self.transport.connected: + reactor.callLater(.2, self.loop) + + def connectionLost(self, reason): + log.info("bye ws client %r: %s", self, reason) + + class songs(PrettyErrorHandler, cyclone.web.RequestHandler): def get(self): @@ -120,11 +144,16 @@ class songResource(PrettyErrorHandler, c class seekPlayOrPause(PrettyErrorHandler, cyclone.web.RequestHandler): + """curveCalc's ctrl-p or a vidref scrub""" def post(self): player = self.settings.app.player data = json.loads(self.request.body) + if 'scrub' in data: + player.pause() + player.seek(data['scrub']) + return if player.isPlaying(): player.pause() else: @@ -162,6 +191,7 @@ def makeWebApp(app): return cyclone.web.Application(handlers=[ (r"/", root), (r"/time", timeResource), + (r"/time/stream", timeStreamResource), (r"/song", songResource), (r"/songs", songs), (r"/seekPlayOrPause", seekPlayOrPause), diff --git a/light9/vidref/index.html b/light9/vidref/index.html --- a/light9/vidref/index.html +++ b/light9/vidref/index.html @@ -4,20 +4,24 @@