diff --git a/bin/vidref b/bin/vidref --- a/bin/vidref +++ b/bin/vidref @@ -24,9 +24,9 @@ from light9 import networking, showconfi 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 cycloneerr import PrettyErrorHandler +from typing import cast parser = optparse.OptionParser() parser.add_option("-v", "--verbose", action="store_true", help="logging.DEBUG") @@ -96,13 +96,12 @@ class Time(cyclone.web.RequestHandler): self.set_status(202) -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 Clips(PrettyErrorHandler, cyclone.web.RequestHandler): + def delete(self): + clip = URIRef(self.get_argument('uri')) + videorecorder.deleteClip(clip) class ReplayMap(PrettyErrorHandler, cyclone.web.RequestHandler): @@ -120,12 +119,12 @@ class ReplayMap(PrettyErrorHandler, cycl ):].decode('ascii') clips.append({ - 'uri': takeUri(vid), + 'uri': videorecorder.takeUri(vid), 'videoUrl': url, 'songToVideo': pts }) - clips.sort(key=lambda c: len(c['songToVideo'])) + clips.sort(key=lambda c: len(cast(list, c['songToVideo']))) clips = clips[-3:] clips.sort(key=lambda c: c['uri'], reverse=True) @@ -153,6 +152,7 @@ reactor.listenTCP( 'default_filename': 'setup.html' }), (r'/live', Live), + (r'/clips', Clips), (r'/replayMap', ReplayMap), (r'/snapshot', Snapshot), (r'/snapshot/(.*)', SnapshotPic, { diff --git a/light9/ascoltami/webapp.py b/light9/ascoltami/webapp.py --- a/light9/ascoltami/webapp.py +++ b/light9/ascoltami/webapp.py @@ -154,6 +154,14 @@ class seekPlayOrPause(PrettyErrorHandler player.pause() player.seek(data['scrub']) return + if 'action' in data: + if data['action'] == 'play': + player.resume() + elif data['action'] == 'pause': + player.pause() + else: + raise NotImplementedError + return if player.isPlaying(): player.pause() else: diff --git a/light9/vidref/index.html b/light9/vidref/index.html --- a/light9/vidref/index.html +++ b/light9/vidref/index.html @@ -3,25 +3,50 @@ vidref - + + - - Live: +

vidref

- - -stats of all disk usage + +
Keys: + s stop, + p play, + ,/. step +
+ diff --git a/light9/vidref/videorecorder.py b/light9/vidref/videorecorder.py --- a/light9/vidref/videorecorder.py +++ b/light9/vidref/videorecorder.py @@ -11,6 +11,7 @@ import PIL.Image from gi.repository import Gst from rx.subject import BehaviorSubject from twisted.internet import threads +from rdflib import URIRef import moviepy.editor import numpy @@ -42,6 +43,22 @@ def songDir(song: Song) -> bytes: showconfig.root(), b'video', song.replace('http://', '').replace('/', '_').encode('ascii')) +def takeUri(songPath: bytes) -> URIRef: + 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])) + +def deleteClip(uri: URIRef): + # uri http://light9.bigasterisk.com/show/dance2019/song6/take_155 + # path show/dance2019/video/light9.bigasterisk.com_show_dance2019_song6/take_155.* + w = uri.split('/')[-4:] + path = '/'.join([w[0], w[1], 'video', + f'light9.bigasterisk.com_{w[0]}_{w[1]}_{w[2]}', w[3]]) + log.info(f'deleting {uri} {path}') + for fn in [path + '.mp4', path + '.timing']: + os.remove(fn) class FramesToVideoFiles: """ @@ -66,7 +83,7 @@ class FramesToVideoFiles: self.root = root self.nextImg: Optional[CaptureFrame] = None - self.currentOutputClip = None + self.currentOutputClip: Optional[moviepy.editor.VideoClip] = None self.currentOutputSong: Optional[Song] = None self.nextWriteAction = 'ignore' self.frames.subscribe(on_next=self.onFrame) @@ -101,7 +118,7 @@ class FramesToVideoFiles: """ return threads.deferToThread(self._bg_save, outBase) - def _bg_save(self, outBase): + def _bg_save(self, outBase: bytes): os.makedirs(os.path.dirname(outBase), exist_ok=True) self.frameMap = open(outBase + b'.timing', 'wt') @@ -109,6 +126,8 @@ class FramesToVideoFiles: # we get to call write_frame on a FFMPEG_VideoWriter instead # of it calling us back. + self.currentClipFrameCount = 0 + # (immediately calls make_frame) self.currentOutputClip = moviepy.editor.VideoClip(self._bg_make_frame, duration=999.) @@ -117,12 +136,13 @@ class FramesToVideoFiles: self.currentOutputClip.fps = 10 log.info(f'write_videofile {outBase} start') try: - self.currentOutputClip.write_videofile(outBase.decode('ascii') + - '.mp4', + self.outMp4 = outBase.decode('ascii') + '.mp4' + self.currentOutputClip.write_videofile(self.outMp4, codec='libx264', audio=False, preset='ultrafast', logger=None, + ffmpeg_params=['-g', '10'], bitrate='150000') except (StopIteration, RuntimeError): self.frameMap.close() @@ -130,6 +150,11 @@ class FramesToVideoFiles: log.info('write_videofile done') self.currentOutputClip = None + if self.currentClipFrameCount < 400: + log.info('too small- deleting') + deleteClip(takeUri(self.outMp4.encode('ascii'))) + + def _bg_make_frame(self, video_time_secs): if self.nextWriteAction == 'close': raise StopIteration # the one in write_videofile @@ -146,6 +171,7 @@ class FramesToVideoFiles: cf, self.nextImg = self.nextImg, None self.frameMap.write(f'video {video_time_secs:g} = song {cf.t:g}\n') + self.currentClipFrameCount += 1 return numpy.asarray(cf.img) diff --git a/light9/web/light9-vidref-live.js b/light9/web/light9-vidref-live.js --- a/light9/web/light9-vidref-live.js +++ b/light9/web/light9-vidref-live.js @@ -13,15 +13,6 @@ class Light9VidrefLive extends LitElemen enabled: { type: Boolean } }; } - - static get styles() { - return css` - :host { - border: 2px solid #46a79f; - display: inline-block; - } - `; - } constructor() { super(); @@ -51,13 +42,22 @@ class Light9VidrefLive extends LitElemen //close socket } + + static get styles() { + return css` + :host { + display: inline-block; + } +#live { +border: 4px solid orange; +} + `; + } render() { return html` -
-
-
+ `; } diff --git a/light9/web/light9-vidref-replay-stack.js b/light9/web/light9-vidref-replay-stack.js --- a/light9/web/light9-vidref-replay-stack.js +++ b/light9/web/light9-vidref-replay-stack.js @@ -23,17 +23,19 @@ class Light9VidrefReplayStack extends Li setVideoTimesFromSongTime() { this.shadowRoot.querySelectorAll('light9-vidref-replay').forEach( (r) => { - r.setVideoTimeFromSongTime(this.songTime); + r.setVideoTimeFromSongTime(this.songTime, this.musicState.playing); }); } - + nudgeTime(dt) { + this.songTime += dt; + log('song now', 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; + this.songTime = this.musicState.t; } requestAnimationFrame(this.fineTime.bind(this)); } @@ -63,7 +65,6 @@ class Light9VidrefReplayStack extends Li if (this.musicState.song != this.song) { this.song = this.musicState.song; this.getReplayMapForSong(this.song); - } } @@ -87,8 +88,6 @@ class Light9VidrefReplayStack extends Li node.uri = msg[i].uri; node.videoUrl = msg[i].videoUrl; node.songToVideo = msg[i].songToVideo; - - }); this.setVideoTimesFromSongTime(); } @@ -114,35 +113,45 @@ class Light9VidrefReplayStack extends Li 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; -} + #clips { + display: flex; + flex-direction: column; + } + a { + color: rgb(97, 97, 255); + } + #songTime { + font-size: 27px; + } `; } render() { return html` -
-
-
${this.musicState.song}
-
showing song time ${rounding(this.songTime, 3)} (${rounding(this.musicState.t, 3)})
-
clips:
-
- ${this.players} -
-
+
+
+
${this.musicState.song}
+
showing song time ${rounding(this.songTime, 3)}
+
clips:
+
+ ${this.players} +
+
+ +
`; } diff --git a/light9/web/light9-vidref-replay.js b/light9/web/light9-vidref-replay.js --- a/light9/web/light9-vidref-replay.js +++ b/light9/web/light9-vidref-replay.js @@ -1,6 +1,7 @@ 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'); @@ -12,21 +13,60 @@ class Light9VidrefReplay extends LitElem videoUrl: { type: String }, songToVideo: { type: Object }, videoTime: { type: Number }, + outVideoCurrentTime: { type: Number }, + timeErr: { type: Number }, + playRate: { type: Number } }; } - - setVideoTimeFromSongTime(songTime) { - if (!this.songToVideo || !this.outVideo) { + 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.outVideo.currentTime = this.videoTime; + + this.outVideoCurrentTime = this.outVideo.currentTime; + + if (isPlaying) { + if (this.outVideo.paused) { + this.outVideo.play(); + this.setRate(this.estimateRate()); + } + const err = this.outVideo.currentTime - this.videoTime; + this.timeErr = err; + + if (Math.abs(err) > window.thresh) { + this.outVideo.currentTime = this.videoTime; + const p = window.p; + if (err > 0) { + this.setRate(this.playRate - err * p); + } else { + this.setRate(this.playRate - err * p); + } + } + } else { + this.outVideo.pause(); + this.outVideoCurrentTime = this.outVideo.currentTime = this.videoTime; + this.timeErr = 0; + } + } + setRate(r) { + this.playRate = Math.max(.1, Math.min(4, r)); + this.outVideo.playbackRate = this.playRate; } firstUpdated() { this.outVideo = this.shadowRoot.querySelector('#replay'); + this.playRate = this.outVideo.playbackRate = 1.0; } onDelete() { @@ -42,23 +82,45 @@ class Light9VidrefReplay extends LitElem 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); } `; } render() { return html` -
-
-
take is ${this.uri} (${Object.keys(this.songToVideo).length} frames)
- -
video time is ${this.videoTime}
- -
-`; + +
+ take is ${this.uri} + (${Object.keys(this.songToVideo).length} frames) + +
+ +
+ video time should be ${this.videoTime} + actual = ${rounding(this.outVideoCurrentTime, 3)}, + err = ${rounding(this.timeErr, 3)} + rate = ${rounding(this.playRate, 3)} +
+ `; } } customElements.define('light9-vidref-replay', Light9VidrefReplay); +window.thresh=.3 +window.p=.3 diff --git a/light9/web/style.css b/light9/web/style.css --- a/light9/web/style.css +++ b/light9/web/style.css @@ -73,6 +73,24 @@ button a { .key { color: #888; } + +div.keys { + margin-top: 10px; + padding: 5px; +} + +.keyCap { + color: #ccc; + background: #525252; + display: inline-block; + border: 1px outset #b3b3b3; + padding: 2px 3px; + margin: 3px 0; + font-size: 16px; + box-shadow: 0.9px 0.9px 0px 2px #565656; + border-radius: 2px; +} + .currentSong button { background: #a90707; }