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
 
@@ -155,9 +155,10 @@
 
^\.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
 
@@ -15,19 +15,21 @@ light9/web/light9-vidref-playback.js Lit
 

	
 
"""
 
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)
 
@@ -37,17 +39,18 @@ class Snapshot(cyclone.web.RequestHandle
 

	
 
    @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
 
@@ -68,21 +71,17 @@ class Live(cyclone.websocket.WebSocketHa
 

	
 
    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
 

	
 
@@ -94,15 +93,54 @@ class Time(cyclone.web.RequestHandler):
 
        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=[
 
@@ -112,15 +150,16 @@ reactor.listenTCP(
 
            }),
 
            (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)
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
 
@@ -43,36 +43,38 @@ def playerSongUri(graph, player):
 
    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.
 
@@ -86,12 +88,34 @@ class timeResource(PrettyErrorHandler, c
 
        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)
 
@@ -117,17 +141,22 @@ class songResource(PrettyErrorHandler, c
 
            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()
 

	
 
@@ -159,12 +188,13 @@ class goButton(PrettyErrorHandler, cyclo
 

	
 

	
 
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, {
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'
 
@@ -48,14 +58,15 @@ class FramesToVideoFiles:
 
    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)
 
@@ -66,13 +77,14 @@ class FramesToVideoFiles:
 
        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
 
@@ -88,23 +100,29 @@ class FramesToVideoFiles:
 
        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()
 
@@ -121,13 +139,13 @@ class FramesToVideoFiles:
 
            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)
 

	
 

	
 
@@ -136,14 +154,14 @@ 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
 
@@ -170,25 +188,27 @@ class GstSource:
 
        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()
 
@@ -213,42 +233,37 @@ class GstSource:
 
            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"""
 

	
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
 
@@ -38,25 +38,25 @@ node_modules/n3/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
 

	
0 comments (0 inline, 0 general)