#!bin/python """ Camera images of the stage. View live on a web page and also save them to disk. Retrieve images based on the song and time that was playing when they were taken. Also, save snapshot images to a place they can be used again as thumbnails of effects. bin/vidref main light9/vidref/videorecorder.py capture frames and save them light9/vidref/replay.py backend for vidref.js playback element- figures out which frames go with the current song and time light9/vidref/index.html web ui for watching current stage and song playback light9/vidref/setup.html web ui for setup of camera params and frame crop light9/web/light9-vidref-live.js LitElement for live video frames light9/web/light9-vidref-playback.js LitElement for video playback """ from run_local import log from typing import cast import logging, optparse, json, base64, os, glob from greplin import scales from greplin.scales.cyclonehandler import StatsHandler from rdflib import URIRef from twisted.internet import reactor, defer import cyclone.web, cyclone.httpclient, cyclone.websocket from cycloneerr import PrettyErrorHandler from light9 import networking, showconfig from light9.newtypes import Song from light9.vidref import videorecorder from rdfdb.syncedgraph import SyncedGraph from standardservice.scalessetup import gatherProcessStats parser = optparse.OptionParser() parser.add_option("-v", "--verbose", action="store_true", help="logging.DEBUG") (options, args) = parser.parse_args() log.setLevel(logging.DEBUG if options.verbose else logging.INFO) gatherProcessStats() stats = scales.collection( '/webServer', scales.RecentFpsStat('liveWebsocketFrameFps'), scales.IntStat('liveClients'), ) class Snapshot(cyclone.web.RequestHandler): @defer.inlineCallbacks def post(self): # save next pic # return /snapshot/path try: snapshotDir = 'todo' 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: import traceback traceback.print_exc() raise pipeline = videorecorder.GstSource( #'/dev/v4l/by-id/usb-Bison_HD_Webcam_200901010001-video-index0' '/dev/v4l/by-id/usb-Generic_FULL_HD_1080P_Webcam_200901010001-video-index0') class Live(cyclone.websocket.WebSocketHandler): def connectionMade(self, *args, **kwargs): pipeline.liveImages.subscribe(on_next=self.onFrame) stats.liveClients += 1 def connectionLost(self, reason): #self.subj.dispose() stats.liveClients -= 1 def onFrame(self, cf: videorecorder.CaptureFrame): if cf is None: return stats.liveWebsocketFrameFps.mark() self.sendMessage( json.dumps({ 'jpeg': base64.b64encode(cf.asJpeg()).decode('ascii'), 'description': f't={cf.t}', })) class SnapshotPic(cyclone.web.StaticFileHandler): pass class Time(PrettyErrorHandler, cyclone.web.RequestHandler): def put(self): body = json.loads(self.request.body) t = body['t'] for listener in TimeStream.time_stream_listeners: listener.sendMessage(json.dumps({ 'st': t, 'song': body['song'], })) self.set_status(202) class TimeStream(cyclone.websocket.WebSocketHandler): time_stream_listeners = [] def connectionMade(self, *args, **kwargs): TimeStream.time_stream_listeners.append(self) def connectionLost(self, reason): TimeStream.time_stream_listeners.remove(self) class Clips(PrettyErrorHandler, cyclone.web.RequestHandler): def delete(self): clip = URIRef(self.get_argument('uri')) videorecorder.deleteClip(clip) class ReplayMap(PrettyErrorHandler, cyclone.web.RequestHandler): def get(self): song = Song(self.get_argument('song')) clips = [] videoPaths = glob.glob( os.path.join(videorecorder.songDir(song), b'*.mp4')) for vid in videoPaths: 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': videorecorder.takeUri(vid), 'videoUrl': url, 'songToVideo': pts }) clips.sort(key=lambda c: len(cast(list, 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( port, cyclone.web.Application( handlers=[ (r'/()', cyclone.web.StaticFileHandler, { 'path': 'light9/vidref', 'default_filename': 'index.html' }), (r'/setup/()', cyclone.web.StaticFileHandler, { 'path': 'light9/vidref', 'default_filename': 'setup.html' }), (r'/live', Live), (r'/clips', Clips), (r'/replayMap', ReplayMap), (r'/snapshot', Snapshot), (r'/snapshot/(.*)', SnapshotPic, { "path": 'todo', }), (r'/time', Time), (r'/time/stream', TimeStream), (r'/stats/(.*)', StatsHandler, { 'serverName': 'vidref' }), ], debug=True, )) log.info("serving on %s" % port) reactor.run()