Changeset - 9f0f2b39ad95
[Not reviewed]
default
1 7 2
Drew Perttula - 6 years ago 2019-06-06 00:09:39
drewp@bigasterisk.com
vidref web is working
Ignore-this: 686b512c0368f8cc419000e784f13935
10 files changed with 410 insertions and 345 deletions:
0 comments (0 inline, 0 general)
.boring
Show inline comments
 
@@ -149,15 +149,16 @@
 
^show/.*/doc
 
^stagesim/three.js-master
 
^tkdnd
 
^node_modules
 

	
 
\.js\.map$
 
^\.mypy_cache
 
^lib/FlameGraph/
 
^lib/pyflame/
 
^show/dance..../capture
 
^show/dance2017/networking.n3
 
^show/dance2019/model/
 
^show/dance2019/video/
 
^timelapse/
 
__pycache__
 
rgbled/build-nano328/
bin/vidref
Show inline comments
 
@@ -9,51 +9,54 @@ 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 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")
 
(options, args) = parser.parse_args()
 

	
 
log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 

	
 
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())
 
            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)
 
            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'
 
@@ -62,67 +65,103 @@ pipeline = videorecorder.GstSource(
 

	
 

	
 
class Live(cyclone.websocket.WebSocketHandler):
 

	
 
    def connectionMade(self, *args, **kwargs):
 
        pipeline.liveImages.subscribe(on_next=self.onFrame)
 

	
 
    def connectionLost(self, reason):
 
        0  #self.subj.dispose()
 

	
 
    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}',
 
            }))
 

	
 

	
 
class SnapshotPic(cyclone.web.StaticFileHandler):
 
    pass
 

	
 

	
 
class Time(cyclone.web.RequestHandler):
 

	
 
    def put(self):
 
        body = json.loads(self.request.body)
 
        t = body['t']
 
        source = body['source']
 
        self.settings.gui.incomingTime(t, source)
 
        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(
 
    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'/replayMap', ReplayMap),
 
            (r'/snapshot', Snapshot),
 
            (r'/snapshot/(.*)', SnapshotPic, {
 
                "path": snapshotDir()
 
                "path": 'todo',
 
            }),
 
            (r'/time', Time),
 
        ],
 
        debug=True,
 
    ))
 
log.info("serving on %s" % port)
 

	
 
reactor.run()
light9/ascoltami/webapp.py
Show inline comments
 
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__))
 

	
 

	
 
def songLocation(graph, songUri):
 
    loc = URIRef("file://%s" % songOnDisk(songUri))
 
    _songUris[loc] = songUri
 
    return loc
 

	
 

	
 
def songUri(graph, locationUri):
 
    return _songUris[locationUri]
 

	
 
@@ -37,67 +37,91 @@ class root(PrettyErrorHandler, cyclone.w
 

	
 

	
 
def playerSongUri(graph, player):
 
    """or None"""
 

	
 
    playingLocation = player.getSong()
 
    if playingLocation:
 
        return songUri(graph, URIRef(playingLocation))
 
    else:
 
        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):
 
        """
 
        post a json object with {pause: true} or {resume: true} if you
 
        want those actions. Use {t: <seconds>} to seek, optionally
 
        with a pause/resume command too.
 
        """
 
        params = json.loads(self.request.body)
 
        player = self.settings.app.player
 
        if params.get('pause', False):
 
            player.pause()
 
        if params.get('resume', False):
 
            player.resume()
 
        if 't' in params:
 
            player.seek(params['t'])
 
        self.set_header("Content-Type", "text/plain")
 
        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):
 
        graph = self.settings.app.graph
 

	
 
        songs = getSongsFromShow(graph, self.settings.app.show)
 

	
 
        self.set_header("Content-Type", "application/json")
 
        self.write(
 
            json.dumps({
 
                "songs": [{
 
                    "uri": s,
 
@@ -111,29 +135,34 @@ class songResource(PrettyErrorHandler, c
 

	
 
    def post(self):
 
        """post a uri of song to switch to (and start playing)"""
 
        graph = self.settings.app.graph
 

	
 
        self.settings.app.player.setSong(
 
            songLocation(graph, URIRef(self.request.body.decode('utf8'))))
 
        self.set_header("Content-Type", "text/plain")
 
        self.write("ok")
 

	
 

	
 
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:
 
            player.seek(data['t'])
 
            player.resume()
 

	
 

	
 
class output(PrettyErrorHandler, cyclone.web.RequestHandler):
 

	
 
    def post(self):
 
        d = json.loads(self.request.body)
 
        subprocess.check_call(["bin/movesinks", str(d['sink'])])
 
@@ -153,22 +182,23 @@ class goButton(PrettyErrorHandler, cyclo
 
            pass
 
        else:
 
            player.resume()
 

	
 
        self.set_header("Content-Type", "text/plain")
 
        self.write("ok")
 

	
 

	
 
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),
 
        (r"/output", output),
 
        (r"/go", goButton),
 
        (r'/stats/(.*)', StatsHandler, {
 
            'serverName': 'ascoltami'
 
        }),
 
    ],
 
                                   app=app)
light9/vidref/index.html
Show inline comments
 
<!doctype html>
 
<html>
 
  <head>
 
    <title>vidref</title>
 
    <meta charset="utf-8" />
 
    <style>
 

	
 
     
 
    </style>
 
    <script src="/lib/debug/debug-build.js"></script>
 
    <script>
 
     debug.enable('*');
 
    </script>
 
    <script src="/lib/jquery/dist/jquery.slim.min.js"></script>
 

	
 
    <script src="/websocket.js"></script>
 
    <script type="module"  src="/light9-vidref-live.js"></script>
 

	
 

	
 
    <script type="module" src="/light9-vidref-live.js"></script>
 
    <script type="module" src="/light9-vidref-replay-stack.js"></script>
 
  </head>
 
  <body>
 
    Live:
 
    <light9-vidref-live></light9-vidref-live>
 
    <div>
 
      <light9-vidref-live></light9-vidref-live>
 
    </div>
 
    <light9-vidref-replay-stack></light9-vidref-replay-stack>
 

	
 

	
 
stats of all disk usage
 
  </body>
 
</html>
light9/vidref/replay.py
Show inline comments
 
deleted file
light9/vidref/videorecorder.py
Show inline comments
 
from dataclasses import dataclass
 
from queue import Queue
 
from typing import Optional
 
import time, logging, os, traceback, sys
 
import time, logging, os, traceback
 
from io import BytesIO
 

	
 
import gi
 
gi.require_version('Gst', '1.0')
 
gi.require_version('GstBase', '1.0')
 

	
 
from PIL import Image
 
import PIL.Image
 
from gi.repository import Gst
 
from rx.subject import BehaviorSubject
 
from twisted.internet import threads
 
import moviepy.editor
 
import numpy
 

	
 
from light9.ascoltami.musictime_client import MusicTime
 
from light9.newtypes import Song
 

	
 
from IPython.core import ultratb
 
sys.excepthook = ultratb.FormattedTB(mode='Verbose',
 
                                     color_scheme='Linux',
 
                                     call_pdb=1)
 
from light9 import showconfig
 

	
 
log = logging.getLogger()
 

	
 

	
 
@dataclass(frozen=True)
 
@dataclass
 
class CaptureFrame:
 
    img: Image
 
    img: PIL.Image
 
    song: Song
 
    t: float
 
    isPlaying: bool
 
    imgJpeg: Optional[bytes] = None
 

	
 
    def asJpeg(self):
 
        if not self.imgJpeg:
 
            output = BytesIO()
 
            self.img.save(output, 'jpeg', quality=80)
 
            self.imgJpeg = output.getvalue()
 
        return self.imgJpeg
 

	
 

	
 
def songDir(song: Song) -> bytes:
 
    return os.path.join(
 
        showconfig.root(), b'video',
 
        song.replace('http://', '').replace('/', '_').encode('ascii'))
 

	
 

	
 
class FramesToVideoFiles:
 
    """
 

	
 
    nextWriteAction: 'ignore'
 
    currentOutputClip: None
 

	
 
    (frames come in for new video)
 
    nextWriteAction: 'saveFrame'
 
    currentOutputClip: new VideoClip
 
    (many frames)
 

	
 
    (music stops or song changes)
 
    nextWriteAction: 'close'
 
    currentOutputClip: None
 
    nextWriteAction: 'ignore'
 
    
 
    """
 

	
 
    def __init__(self, frames: BehaviorSubject):
 
    def __init__(self, frames: BehaviorSubject, root: bytes):
 
        self.frames = frames
 
        self.root = root
 
        self.nextImg: Optional[CaptureFrame] = None
 

	
 
        self.currentOutputClip = None
 
        self.currentOutputSong: Optional[Song] = None
 
        self.nextWriteAction = 'ignore'
 
        self.frames.subscribe(on_next=self.onFrame)
 

	
 
    def onFrame(self, cf: Optional[CaptureFrame]):
 
        if cf is None:
 
            return
 
        self.nextImg = cf
 

	
 
        if self.currentOutputClip is None and cf.isPlaying:
 
            # start up
 
            self.nextWriteAction = 'saveFrames'
 
            self.currentOutputSong = cf.song
 
            self.save('/tmp/out%s' % time.time())
 
            self.save(
 
                os.path.join(songDir(cf.song), b'take_%d' % int(time.time())))
 
        elif self.currentOutputClip and cf.isPlaying:
 
            self.nextWriteAction = 'saveFrames'
 
            # continue recording this
 
        elif self.currentOutputClip is None and not cf.isPlaying:
 
            self.nextWriteAction = 'notWritingClip'
 
            pass  # continue waiting
 
        elif self.currentOutputClip and not cf.isPlaying or self.currentOutputSong != cf.song:
 
            # stop
 
            self.nextWriteAction = 'close'
 
        else:
 
            raise NotImplementedError(str(vars()))
 

	
 
    def save(self, outBase):
 
        """
 
        receive frames (infinite) and wall-to-song times (stream ends with
 
        the song), and write a video file and a frame map
 
        """
 
        return threads.deferToThread(self._bg_save, outBase)
 

	
 
    def _bg_save(self, outBase):
 
        os.makedirs(os.path.dirname(outBase), exist_ok=True)
 
        self.frameMap = open(outBase + '.timing', 'wt')
 
        self.frameMap = open(outBase + b'.timing', 'wt')
 

	
 
        # todo: see moviestore.py for a better-looking version where
 
        # we get to call write_frame on a FFMPEG_VideoWriter instead
 
        # of it calling us back.
 

	
 
        # (immediately calls make_frame)
 
        self.currentOutputClip = moviepy.editor.VideoClip(self._bg_make_frame,
 
                                                          duration=999.)
 
        # The fps recorded in the file doesn't matter much; we'll play
 
        # it back in sync with the music regardless.
 
        self.currentOutputClip.fps = 10
 
        log.info(f'write_videofile {outBase} start')
 
        try:
 
            self.currentOutputClip.write_videofile(outBase + '.mp4',
 
            self.currentOutputClip.write_videofile(outBase.decode('ascii') +
 
                                                   '.mp4',
 
                                                   codec='libx264',
 
                                                   audio=False,
 
                                                   preset='ultrafast',
 
                                                   logger=None,
 
                                                   bitrate='150000')
 
        except (StopIteration, RuntimeError):
 
            self.frameMap.close()
 

	
 
        log.info('write_videofile done')
 
        self.currentOutputClip = None
 

	
 
    def _bg_make_frame(self, video_time_secs):
 
        if self.nextWriteAction == 'close':
 
            raise StopIteration  # the one in write_videofile
 
        elif self.nextWriteAction == 'notWritingClip':
 
            raise NotImplementedError
 
        elif self.nextWriteAction == 'saveFrames':
 
            pass
 
        else:
 
            raise NotImplementedError(self.nextWriteAction)
 

	
 
        # should be a little queue to miss fewer frames
 
        while self.nextImg is None:
 
            time.sleep(.03)
 
            time.sleep(.015)
 
        cf, self.nextImg = self.nextImg, None
 

	
 
        self.frameMap.write(f'video {video_time_secs:g} = song {cf.t:g}\n')
 
        return numpy.asarray(cf.img)
 

	
 

	
 
class GstSource:
 

	
 
    def __init__(self, dev):
 
        """
 
        make new gst pipeline
 
        """
 
        Gst.init(None)
 
        self.musicTime = MusicTime(pollCurvecalc=False)
 
        self.liveImages: BehaviorSubject[
 
            Optional[CaptureFrame]] = BehaviorSubject(None)
 
        self.liveImages: BehaviorSubject = BehaviorSubject(
 
            None)  # stream of Optional[CaptureFrame]
 

	
 
        size = [640, 480]
 

	
 
        log.info("new pipeline using device=%s" % dev)
 

	
 
        # using videocrop breaks the pipeline, may be this issue
 
        # https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/issues/732
 
        pipeStr = (
 
            #f"v4l2src device=\"{dev}\""
 
            f'autovideosrc'
 
            f" ! videoconvert"
 
            f" ! appsink emit-signals=true max-buffers=1 drop=true name=end0 caps=video/x-raw,format=RGB,width={size[0]},height={size[1]}"
 
@@ -164,37 +182,39 @@ class GstSource:
 
        self.appsink.connect('new-sample', self.new_sample)
 

	
 
        self.pipe.set_state(Gst.State.PLAYING)
 
        log.info('gst pipeline is recording video')
 

	
 
    def new_sample(self, appsink):
 
        try:
 
            sample = appsink.emit('pull-sample')
 
            caps = sample.get_caps()
 
            buf = sample.get_buffer()
 
            (result, mapinfo) = buf.map(Gst.MapFlags.READ)
 
            try:
 
                img = Image.frombytes(
 
                img = PIL.Image.frombytes(
 
                    'RGB', (caps.get_structure(0).get_value('width'),
 
                            caps.get_structure(0).get_value('height')),
 
                    mapinfo.data)
 
                img = img.crop((0, 100, 640, 380))
 
            finally:
 
                buf.unmap(mapinfo)
 
            # could get gst's frame time and pass it to getLatest
 
            latest = self.musicTime.getLatest()
 
            if 'song' in latest:
 
                self.liveImages.on_next(
 
                    CaptureFrame(img, Song(latest['song']), latest['t'],
 
                                 latest['playing']))
 
                    CaptureFrame(img=img,
 
                                 song=Song(latest['song']),
 
                                 t=latest['t'],
 
                                 isPlaying=latest['playing']))
 
        except Exception:
 
            traceback.print_exc()
 
        return Gst.FlowReturn.OK
 

	
 
    def setupPipelineError(self, pipe, cb):
 
        bus = pipe.get_bus()
 

	
 
        def onBusMessage(bus, msg):
 

	
 
            print('nusmsg', msg)
 
            if msg.type == Gst.MessageType.ERROR:
 
                _, txt = msg.parse_error()
 
@@ -207,54 +227,49 @@ class GstSource:
 

	
 
    def onError(self, messageText):
 
        if ('v4l2src' in messageText and
 
            ('No such file or directory' in messageText or
 
             'Resource temporarily unavailable' in messageText or
 
             'No such device' in messageText)):
 
            log.error(messageText)
 
            os.abort()
 
        else:
 
            log.error("ignoring error: %r" % messageText)
 

	
 

	
 
'''
 
class oldPipeline(object):
 

	
 
    def __init__(self):
 
        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())
 
            filename = "%s/%s.jpg" % ('todo', 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
 

	
 

	
 
'''
 
        self.imagesToSave = Queue()
 
        self.startBackgroundImageSaver(self.imagesToSave)
 

	
 
    def startBackgroundImageSaver(self, imagesToSave):
 
        """do image saves in another thread to not block gst"""
 

	
 
        def imageSaver():
 
            while True:
 
                args = imagesToSave.get()
 
                self.saveImg(*args)
 
                imagesToSave.task_done()
 

	
light9/web/light9-vidref-live.js
Show inline comments
 
import { LitElement, TemplateResult, html, css } from '/node_modules/lit-element/lit-element.js';
 
import { rounding }  from '/node_modules/significant-rounding/index.js';
 
import './light9-vidref-replay.js';
 

	
 
import debug from '/lib/debug/debug-build-es6.js';
 
debug.enable('*');
 
const log = debug('live');
 
log('hi it is live')
 

	
 
class Light9VidrefLive extends LitElement {
 
    
 
    static get properties() {
 
        return {
 
            description: { type: String }
 
            description: { type: String },
 
            enabled: { type: Boolean }
 
        };
 
    }
 

	
 
    static get styles() {
 
        return css`
 
        :host {
 
            border: 2px solid #46a79f;
 
            display: inline-block;
 
        }
 
        `;
 
    }
 

	
 
    firstUpdated() {
 
        const ws = reconnectingWebSocket('live', (msg) => {
 
            this.shadowRoot.querySelector('#live').src = 'data:image/jpeg;base64,' + msg.jpeg;
 
            this.description = msg.description;
 
        });
 
        
 
    
 
    constructor() {
 
        super();
 
        this.live = null;
 
    }
 
    
 
    onEnabled() {
 
        if (this.shadowRoot.querySelector('#enabled').checked) {
 
            
 
            this.live = reconnectingWebSocket(
 
                'live', (msg) => {
 
                    this.shadowRoot.querySelector('#live').src = 'data:image/jpeg;base64,' + msg.jpeg;
 
                    this.description = msg.description;
 
                });
 
            this.shadowRoot.querySelector('#liveWidget').style.display = 'block';
 
        } else {
 
            if (this.live) {
 
                this.live.disconnect();
 
                this.live = null;
 
                this.shadowRoot.querySelector('#liveWidget').style.display = 'none';
 
            }
 
        }
 
    }
 

	
 
    disconnectedCallback() {
 
        log('bye');
 
        //close socket
 
        
 
    }
 
    
 
    render() {
 
        return html`
 
<div>
 
<div><img id="live" ></div>
 
<div>${this.description}</div>
 
  <label><input type="checkbox" id="enabled" ?checked="${this.enabled}" @change="${this.onEnabled}">Show live</label>
 
  <div id="liveWidget"><img id="live" ></div>
 
</div>
 
`;
 

	
 
    }
 
}
 
customElements.define('light9-vidref-live', Light9VidrefLive);
light9/web/light9-vidref-replay-stack.js
Show inline comments
 
new file 100644
 
import { LitElement, TemplateResult, html, css } from '/node_modules/lit-element/lit-element.js';
 
import debug from '/lib/debug/debug-build-es6.js';
 
import _ from '/lib/underscore/underscore-min-es6.js';
 
import { rounding }  from '/node_modules/significant-rounding/index.js';
 

	
 
const log = debug('stack');
 

	
 
class Light9VidrefReplayStack extends LitElement {
 
    
 
    static get properties() {
 
        return {
 
            songTime: { type: Number, attribute: false }, // from musicState.t but higher res
 
            musicState: { type: Object, attribute: false },
 
            players: { type: Array, attribute: false }
 
        };
 
    }
 

	
 
    constructor() {
 
        super();
 
        this.musicState = {};
 
    }
 

	
 
    setVideoTimesFromSongTime() {
 
        this.shadowRoot.querySelectorAll('light9-vidref-replay').forEach(
 
            (r) => {
 
                r.setVideoTimeFromSongTime(this.songTime);
 
            });
 
    }
 

	
 
    fineTime() {       
 
        if (this.musicState.playing) {
 
            const sinceLastUpdate = (Date.now() - this.musicState.reportTime) / 1000;
 
            this.songTime = sinceLastUpdate + this.musicState.tStart;
 
            this.songTimeRangeInput.value = this.songTime;
 
        } else  {
 
            //this.songTime = this.musicState.t;
 
        }
 
        requestAnimationFrame(this.fineTime.bind(this));
 
    }
 

	
 
    updated(changedProperties) {
 
        if (changedProperties.has('songTime')) {
 
            this.setVideoTimesFromSongTime();
 
        }
 
    }
 

	
 
    firstUpdated() {
 
        this.songTimeRangeInput = this.shadowRoot.querySelector('#songTime');
 

	
 
        const ws = reconnectingWebSocket('../ascoltami/time/stream',
 
                                         this.receivedSongAndTime.bind(this));
 
        // bug: upon connecting, clear this.song
 
        this.fineTime();
 
    }
 

	
 
    receivedSongAndTime(msg) {
 
        this.musicState = msg;
 
        this.musicState.reportTime = Date.now();
 
        this.musicState.tStart = this.musicState.t;            
 

	
 
        this.songTimeRangeInput.max = this.musicState.duration;
 

	
 
        if (this.musicState.song != this.song) {
 
            this.song = this.musicState.song;
 
            this.getReplayMapForSong(this.song);
 

	
 
        }
 
    }
 
        
 
    getReplayMapForSong(song) {
 
        const u = new URL(window.location.href);
 
        u.pathname = '/vidref/replayMap'
 
        u.searchParams.set('song', song);
 
        fetch(u.toString()).then((resp) => {
 
            if (resp.ok) {
 
                resp.json().then((msg) => {
 
                    this.players = msg.map(this.makeClipRow.bind(this));
 
                    this.updateComplete.then(this.setupClipRows.bind(this, msg));
 
                });
 
            }
 
        });          
 
    }
 
    
 
    setupClipRows(msg) {
 
        const nodes = this.shadowRoot.querySelectorAll('light9-vidref-replay');
 
        nodes.forEach((node, i) => {
 
            node.uri = msg[i].uri;
 
            node.videoUrl = msg[i].videoUrl;
 
            node.songToVideo = msg[i].songToVideo;
 

	
 

	
 
        });
 
        this.setVideoTimesFromSongTime();
 
    }
 
    
 
    makeClipRow(clip) {
 
        return html`<light9-vidref-replay @clips-changed="${this.onClipsChanged}"></light9-vidref-replay>`;
 
    }
 
    
 
    onClipsChanged(ev) {
 
        this.getReplayMapForSong(this.song);
 
    }
 
    
 
    disconnectedCallback() {
 
        log('bye');
 
        //close socket
 
    }
 

	
 
    userMovedSongTime(ev) {
 
        const st = this.songTimeRangeInput.valueAsNumber;
 
        this.songTime = st;
 

	
 
        fetch('/ascoltami/seekPlayOrPause', {
 
            method: 'POST',
 
            body: JSON.stringify({scrub: st}),
 
        });
 

	
 
    }
 

	
 
    static get styles() {
 
        return css`
 
        :host {
 
         
 
        }
 
        #songTime {
 
            width: 100%;
 
        }
 
#clips {
 
display: flex;
 
flex-direction: column;
 
}
 
        `;
 
    }
 
    
 
    render() {
 
        return html`
 
<div>
 
  <div><input id="songTime" type="range" @input="${this.userMovedSongTime}" min="0" max="0" step=".001"></div>
 
  <div>${this.musicState.song}</div>
 
  <div>showing song time ${rounding(this.songTime, 3)} (${rounding(this.musicState.t, 3)})</div>
 
<div>clips:</div>
 
<div id="clips">
 
   ${this.players}
 
</div>
 
</div>
 
`;
 

	
 
    }
 
}
 
customElements.define('light9-vidref-replay-stack', Light9VidrefReplayStack);
light9/web/light9-vidref-replay.js
Show inline comments
 
new file 100644
 
import { LitElement, TemplateResult, html, css } from '/node_modules/lit-element/lit-element.js';
 
import debug from '/lib/debug/debug-build-es6.js';
 
import _ from '/lib/underscore/underscore-min-es6.js';
 

	
 
const log = debug('replay');
 

	
 
class Light9VidrefReplay extends LitElement {
 
    
 
    static get properties() {
 
        return {
 
            uri: { type: String },
 
            videoUrl: { type: String },
 
            songToVideo: { type: Object },
 
            videoTime: { type: Number },
 
        };
 
    }
 

	
 
    setVideoTimeFromSongTime(songTime) {
 
        if (!this.songToVideo || !this.outVideo) {
 
            return;
 
        }
 
        const i = _.sortedIndex(this.songToVideo, [songTime],
 
                                (row) => { return row[0]; });
 
        this.videoTime = this.songToVideo[Math.max(0, i - 1)][1];
 
        this.outVideo.currentTime = this.videoTime;
 
    }
 

	
 
    firstUpdated() {
 
        this.outVideo = this.shadowRoot.querySelector('#replay');
 
    }
 

	
 
    onDelete() {
 
        const u = new URL(window.location.href);
 
        u.pathname = '/vidref/clips'
 
        u.searchParams.set('uri', this.uri);
 
        fetch(u.toString(), {method: 'DELETE'}).then((resp) => {
 
            let event = new CustomEvent('clips-changed', {detail: {}});
 
            this.dispatchEvent(event);
 
        });
 
    }
 

	
 
    static get styles() {
 
        return css`
 
        :host {
 
            border: 2px solid #46a79f;
 
            display: inline-block;
 
        }
 
        `;
 
    }
 
    
 
    render() {
 
        return html`
 
<div>
 
  <div><video id="replay" src="${this.videoUrl}"></video></div>
 
  <div>take is ${this.uri} (${Object.keys(this.songToVideo).length} frames)</div>
 
  <!-- a little canvas showing what coverage we have -->
 
  <div>video time is ${this.videoTime}</div>
 
  <button @click="${this.onDelete}">Delete</button>
 
</div>
 
`;
 

	
 
    }
 
}
 
customElements.define('light9-vidref-replay', Light9VidrefReplay);
makefile
Show inline comments
 
@@ -32,37 +32,37 @@ bower: node_modules/bower/bin/bower bin/
 

	
 
npm_install:
 
	npm install
 

	
 
node_modules/n3/n3-browser.js:
 
	(cd node_modules/n3; nodejs ../browserify/bin/cmd.js --standalone N3 --require n3 -o n3-browser.js)
 

	
 
light9/web/lib/debug/debug-build.js:
 
	node_modules/browserify/bin/cmd.js light9/web/lib/debug/src/browser.js -o light9/web/lib/debug/debug-build.js --standalone debug
 

	
 
light9/web/lib/debug/debug-build-es6.js:
 
	node_modules/browserify/bin/cmd.js light9/web/lib/debug/src/browser.js -o light9/web/lib/debug/debug-build-es6.js --standalone debug
 
	echo "export default window.debug;" >> light9/web/lib/debug/debug-build-es6.js
 
	echo "\nexport default window.debug;" >> light9/web/lib/debug/debug-build-es6.js
 

	
 
lit_fix:
 
	perl -pi -e "s,'lit-html,'/node_modules/lit-html,; s,lit-html',lit-html/lit-html.js'," node_modules/lit-element/lit-element.js
 

	
 
round_fix:
 
	perl -pi -e 's/module.exports = rounding/export { rounding }/' node_modules/significant-rounding/index.js
 

	
 
debug_es6: 
 
	node_modules/browserify/bin/cmd.js light9/web/lib/debug/src/browser.js -o light9/web/lib/debug/debug-build-es6.js
 
	node_modules/cjs-to-es6/index.js light9/web/lib/debug/debug-build-es6.js
 
light9/web/lib/underscore/underscore-min-es6.js:
 
	cp light9/web/lib/underscore/underscore-min.js light9/web/lib/underscore/underscore-min-es6.js
 
	perl -pi -e 's/call\(this\);/call(window); export default window._;/' light9/web/lib/underscore/underscore-min-es6.js
 

	
 
npm: npm_install node_modules/n3/n3-browser.js light9/web/lib/debug/debug-build.js light9/web/lib/debug/debug-build-es6.js lit_fix round_fix debug_es6
 
npm: npm_install node_modules/n3/n3-browser.js light9/web/lib/debug/debug-build.js light9/web/lib/debug/debug-build-es6.js lit_fix round_fix light9/web/lib/underscore/underscore-min-es6.js
 

	
 

	
 
bin/ascoltami2: gst_packages link_to_sys_packages
 

	
 
effect_node_setup: create_virtualenv packages binexec install_python_deps
 

	
 
tkdnd_build:
 
	# get tkdnd r95 with subversion
 
	# then apply tkdnd-patch-on-r95 to that
 
	cd tkdnd/trunk
 
	./configure
 
	make
0 comments (0 inline, 0 general)