Changeset - 1b690005aabd
[Not reviewed]
default
0 4 0
Drew Perttula - 6 years ago 2019-06-09 19:16:37
drewp@bigasterisk.com
little vidref player inside timeline
Ignore-this: 87333fed2271d9eae182fff1e3d2be1
4 files changed with 46 insertions and 18 deletions:
0 comments (0 inline, 0 general)
bin/vidref
Show inline comments
 
@@ -132,49 +132,49 @@ class Clips(PrettyErrorHandler, cyclone.
 

	
 
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 = clips[-int(self.get_argument('maxClips', '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'
light9/web/light9-vidref-replay-stack.js
Show inline comments
 
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 }
 
            players: { type: Array, attribute: false },
 
            size: { type: String, attribute: true }
 
        };
 
    }
 

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

	
 
    setVideoTimesFromSongTime() {
 
        this.shadowRoot.querySelectorAll('light9-vidref-replay').forEach(
 
            (r) => {
 
                r.setVideoTimeFromSongTime(this.songTime, this.musicState.playing);
 
            });
 
    }
 
    nudgeTime(dt) {
 
        this.songTime += dt;
 
        log('song now', this.songTime);
 
    }
 
    fineTime() {       
 
    fineTime() {
 
        if (this.musicState.playing) {
 
            const sinceLastUpdate = (Date.now() - this.musicState.reportTime) / 1000;
 
            this.songTime = sinceLastUpdate + this.musicState.tStart;
 
        } else  {
 
            // this.songTime = this.musicState.t;
 
        } else if (this.lastFineTimePlayingState)  {
 
            this.songTime = this.musicState.t;
 
        }
 
        this.lastFineTimePlayingState = this.musicState.playing;
 
        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));
 
        reconnectingWebSocket('time/stream', this.receivedRemoteScrubbedTime.bind(this));
 
        reconnectingWebSocket('../vidref/time/stream', this.receivedRemoteScrubbedTime.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);
 
        }
 
    }
 

	
 
    receivedRemoteScrubbedTime(msg) {
 
        this.songTime = msg.st;
 

	
 
        // This doesn't work completely since it will keep getting
 
        // updates from ascoltami slow updates.
 
        if (msg.song != this.song) {
 
            this.song = msg.song;
 
            this.getReplayMapForSong(this.song);
 
        }
 
    }
 
        
 
    getReplayMapForSong(song) {
 
        const u = new URL(window.location.href);
 
        u.pathname = '/vidref/replayMap'
 
        u.searchParams.set('song', song);
 
        u.searchParams.set('maxClips', this.size == "small" ? '1' : '3');
 
        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>`;
 
        return html`<light9-vidref-replay @clips-changed="${this.onClipsChanged}" size="${this.size}"></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 {
 
           display: inline-block;
 
        }
 
        #songTime {
 
            width: 100%;
 
        }
 
        #clips {
 
            display: flex;
 
            flex-direction: column;
 
        }
 
        a {
 
            color: rgb(97, 97, 255);
 
        }
 
        #songTime {
 
            font-size: 27px;
 
        }
 
        `;
 
    }
 
    
 
    render() {
 
        return html`
 
  <div>
 
    <input id="songTime" type="range" 
 
        const songTimeRange = this.size != "small" ? html`<input id="songTime" type="range" 
 
           .value="${this.songTime}" 
 
           @input="${this.userMovedSongTime}" 
 
           min="0" max="0" step=".001"></div>
 
  <div><a href="${this.musicState.song}">${this.musicState.song}</a></div>
 
  <div id="songTime">showing song time ${rounding(this.songTime, 3)}</div>
 
  <div id="songTime">showing song time ${rounding(this.songTime, 3)}</div>` : '';
 

	
 
        const globalCommands = this.size != 'small' ? html`
 
  <div>
 
    <button @click="${this.onClipsChanged}">Refresh clips for song</button>
 
  </div>
 
` : '';
 
        return html`
 
  <div>
 
    ${songTimeRange}
 
  <div>clips:</div>
 
  <div id="clips">
 
    ${this.players}
 
  </div>
 
  <div>
 
    <button @click="${this.onClipsChanged}">Refresh clips for song</button>
 
  </div>
 
  ${globalCommands}
 
`;
 

	
 
    }
 
}
 
customElements.define('light9-vidref-replay-stack', Light9VidrefReplayStack);
light9/web/light9-vidref-replay.js
Show inline comments
 
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('replay');
 

	
 
class Light9VidrefReplay extends LitElement {
 
    
 
    static get properties() {
 
        return {
 
            uri: { type: String },
 
            videoUrl: { type: String },
 
            songToVideo: { type: Object },
 
            videoTime: { type: Number },
 
            outVideoCurrentTime: { type: Number },
 
            timeErr: { type: Number },
 
            playRate: { type: Number }
 
            playRate: { type: Number },
 
            size: { type: String, attribute: true }
 
        };
 
    }
 
    estimateRate() {
 
        const n = this.songToVideo.length;
 
        const x0 = Math.round(n * .3);
 
        const x1 = Math.round(n * .6);
 
        const pt0 = this.songToVideo[x0];
 
        const pt1 = this.songToVideo[x1];
 
        return (pt1[1] - pt0[1]) / (pt1[0] - pt0[0]);
 
    }
 
    setVideoTimeFromSongTime(songTime, isPlaying) {
 
        if (!this.songToVideo || !this.outVideo || this.outVideo.readyState < 1) {
 
            return;
 
        }
 
        const i = _.sortedIndex(this.songToVideo, [songTime],
 
                                (row) => { return row[0]; });
 
        this.videoTime = this.songToVideo[Math.max(0, i - 1)][1];
 

	
 
        this.outVideoCurrentTime = this.outVideo.currentTime;
 
        
 
        if (isPlaying) {
 
            if (this.outVideo.paused) {
 
                this.outVideo.play();
 
                this.setRate(this.estimateRate());
 
@@ -77,50 +78,57 @@ class Light9VidrefReplay extends LitElem
 
            let event = new CustomEvent('clips-changed', {detail: {}});
 
            this.dispatchEvent(event);
 
        });
 
    }
 

	
 
    static get styles() {
 
        return css`
 
        :host {
 
            margin: 5px;
 
            border: 2px solid #46a79f;
 
            display: flex;
 
            flex-direction: column;
 
        }
 
        div {
 
            padding: 5px;
 
        }
 
        .num {
 
            display: inline-block;
 
            width: 4em;
 
            color: #29ffa0;
 
        }
 
        a {
 
            color: rgb(97, 97, 255);
 
        }
 
        video.size-small {
 
          width: 460px;
 
        }
 
        `;
 
    }
 
    
 
    render() {
 
        return html`
 
  <video id="replay" src="${this.videoUrl}"></video>
 
  <div>
 
        let details = '';
 
        if (this.size != 'small') {
 
            details = html`  <div>
 
    take is <a href="${this.uri}">${this.uri}</a> 
 
    (${Object.keys(this.songToVideo).length} frames)
 
    <button @click="${this.onDelete}">Delete</button>
 
  </div>
 
  <!-- here, put a little canvas showing what coverage we have with the 
 
       actual/goal time cursors -->
 
  <div>
 
    video time should be <span class="num">${this.videoTime} </span>
 
    actual = <span class="num">${rounding(this.outVideoCurrentTime, 3)}</span>, 
 
    err = <span class="num">${rounding(this.timeErr, 3)} </span>
 
    rate = <span class="num">${rounding(this.playRate, 3)}</span>
 
  </div>
 
`;
 
        }
 
        return html`
 
  <video id="replay" class="size-${this.size}" src="${this.videoUrl}"></video>
 
  `;
 

	
 
    }
 
}
 
customElements.define('light9-vidref-replay', Light9VidrefReplay);
 
window.thresh=.3
 
window.p=.3
light9/web/timeline/timeline-elements.html
Show inline comments
 
<script src="/lib/debug/debug-build.js"></script>
 
<script>
 
 debug.enable('*');
 
</script>
 
<link rel="import" href="/lib/polymer/polymer.html">
 
<link rel="import" href="/lib/polymer/lib/utils/render-status.html">
 
<link rel="import" href="/lib/iron-resizable-behavior/iron-resizable-behavior.html">
 
<link rel="import" href="/lib/iron-ajax/iron-ajax.html">
 
<link rel="import" href="light9-timeline-audio.html">
 
<link rel="import" href="../rdfdb-synced-graph.html">
 
<link rel="import" href="../light9-music.html">
 
<link rel="import" href="../edit-choice.html">
 
<link rel="import" href="inline-attrs.html">
 
    <script src="/websocket.js"></script>
 
<script type="module" src="/light9-vidref-replay.js"></script>
 

	
 
<script type="module" src="/light9-vidref-replay-stack.js"></script>
 

	
 

	
 
<!-- Whole editor- include this on your page.
 
     Most coordinates are relative to this element.
 
   -->
 
<dom-module id="light9-timeline-editor">
 
  <template>
 
    <style>
 
     :host {
 
         background: #444;
 
         display: flex;
 
         flex-direction: column;
 
         position: relative;
 
         border: 1px solid black;
 
         overflow: hidden;
 
     }
 
     light9-timeline-audio {
 
         width: 100%;
 
         height: 30px;
 
     }
 
     light9-timeline-time-zoomed {
 
         flex-grow: 1;
 
     }
 
     #coveredByDiagram {
 
         position: relative;
 
         display: flex;
 
         flex-direction: column;
 
         height: 100%;
 
     }
 
     #dia, #adjusters, #cursorCanvas, #adjustersCanvas {
 
         position: absolute;
 
         left: 0; top: 0; right: 0; bottom: 0;
 
     }
 
     #debug {
 
         background: white;
 
         font-family: monospace;
 
         font-size: 125%;
 
         height: 15px;
 
     }
 
     light9-vidref-replay-stack {
 
             position: absolute;
 
             bottom: 10px;
 
             background: gray;
 
             box-shadow: 6px 10px 12px #0000006b;
 
             display: inline-block;
 
     }
 
    </style>
 
    <div>
 
      <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
 
      <light9-music id="music"
 
                    song="{{playerSong}}"
 
                    t="{{songTime}}"
 
                    playing="{{songPlaying}}"
 
                    duration="{{songDuration}}"></light9-music>
 
      timeline editor: song <edit-choice graph="{{graph}}" uri="{{song}}"></edit-choice>
 
      <label><input type="checkbox" checked="{{followPlayerSong::change}}" > follow player song choice</label>
 
    </div>
 
    <div id="debug">[[debug]]</div>
 
    <iron-ajax id="vidrefTime" url="/vidref/time" method="PUT" content-type="application/json"></iron-ajax>
 
    <div id="coveredByDiagram">
 
      <light9-timeline-audio id="audio"
 
                             graph="{{graph}}"
 
                             show="{{show}}"
 
                             song="{{song}}"></light9-timeline-audio>
 
      <light9-timeline-time-zoomed id="zoomed"
 
                                   graph="{{graph}}"
 
                                   project="{{project}}"
 
                                   selection="{{selection}}"
 
                                   set-adjuster="{{setAdjuster}}"
 
                                   song="{{song}}"
 
                                   show="{{show}}"
 
                                   view-state="{{viewState}}">
 
      </light9-timeline-time-zoomed>
 
      <light9-adjusters-canvas id="adjustersCanvas" set-adjuster="{{setAdjuster}}">
 
      </light9-adjusters-canvas>
 
      <light9-cursor-canvas id="cursorCanvas" view-state="{{viewState}}"></light9-cursor-canvas>
 
      <light9-vidref-replay-stack size="small"></light9-vidref-replay-stack>
 
    </div>
 
  </template>
 
  
 
</dom-module>
 

	
 
<!-- the whole section that pans/zooms in time (most of the editor) -->
 
<dom-module id="light9-timeline-time-zoomed">
 
  <template>
 
    <style>
 
     :host {
 
         display: flex;
 
         height: 100%;
 
         flex-direction: column;
 
     }
 
     #top {
 
     }
 
     #rows {
 
         height: 100%;
 
         overflow: hidden;
 
     }
 
     #rows.dragging {
 
         background: rgba(126, 52, 245, 0.0784);
 
     }
 
     light9-timeline-time-axis {
0 comments (0 inline, 0 general)