diff --git a/www/annotate.html b/www/annotate.html index 46aa4c3..d33fea0 100644 --- a/www/annotate.html +++ b/www/annotate.html @@ -82,6 +82,18 @@ width: 100%; } + .controls button.paused, .controls button.playing{ + position: absolute; + left: 100%; + width: 30px; + } + .controls button.paused::before{ + content: '⏵'; + } + .controls button.playing::before{ + content: '⏸'; + } + .controls { position: absolute !important; z-index: 100; @@ -227,6 +239,7 @@ vertical-align: middle; width: 100px; /* hides seek head */ } + diff --git a/www/annotate.js b/www/annotate.js index bd10965..42fc7f7 100644 --- a/www/annotate.js +++ b/www/annotate.js @@ -1,8 +1,8 @@ class Annotation { constructor(tag, t_in, t_out) { this.tag = tag; - this.t_in = t_in; - this.t_out = t_out; + this.t_in = Number.parseFloat(t_in); + this.t_out = Number.parseFloat(t_out); } } @@ -132,6 +132,14 @@ class Annotator extends EventTarget { this.scrubTo(ev.target.value); }) + this.playPauseEl = document.createElement('button'); + this.playPauseEl.classList.add('paused'); + this.controlsEl.appendChild(this.playPauseEl); + + this.playPauseEl.addEventListener("click", (ev) => { + this.playPause() + }) + this.scrubberEl = document.createElement('div'); this.scrubberEl.classList.add('scrubber') this.controlsEl.appendChild(this.scrubberEl); @@ -157,7 +165,7 @@ class Annotator extends EventTarget { tagEl.classList.add('annotation-rm'); tagEl.dataset.tag = 'rm'; tagEl.title = "Remove annotation"; - tagEl.innerHTML = "×"; + tagEl.innerHTML = "🚮"; // × tagEl.addEventListener('click', (e) => { if (this.selectedAnnotation) { this.removeAnnotation(this.selectedAnnotationI); @@ -177,7 +185,7 @@ class Annotator extends EventTarget { this.outPointPosition = null; this.outPointTimeMs = null; this._currentTimeMs = 0; - this.isPlaying = false; + this.videoIsPlaying = false; const groups = ['before', 'annotation', 'after'] this.strokeGroups = {}; @@ -199,8 +207,10 @@ class Annotator extends EventTarget { for (let annotation_i in this.annotations) { const annotation = this.annotations[annotation_i]; this.annotationEl = document.createElement('div'); - const left = (annotation.t_in / this.lastFrameTime) * 100; - const right = 100 - (annotation.t_out / this.lastFrameTime) * 100; + const prerollDiff = Number.parseFloat(this.audioOffset < 0 ? this.audioOffset * -1000 : 0); + // console.log('diff', prerollDiff, annotation.t_in, typeof annotation.t_in, this.duration,annotation.t_in + prerollDiff, (annotation.t_in + prerollDiff) / this.duration); + const left = ((annotation.t_in + prerollDiff) / (this.duration * 1000)) * 100; + const right = 100 - ((annotation.t_out + prerollDiff) / (this.duration * 1000)) * 100; this.annotationEl.style.left = left + '%'; this.annotationEl.style.right = right + '%'; @@ -248,6 +258,10 @@ class Annotator extends EventTarget { this.inPointPosition = this.findPositionForTime(this.selectedAnnotation.t_in); this.outPointPosition = this.findPositionForTime(this.selectedAnnotation.t_out); + this.inPointTimeMs = this.selectedAnnotation.t_in; + this.outPointTimeMs = this.selectedAnnotation.t_out; + this._seekByTimeMs(this.selectedAnnotation.t_in); + // draw full stroke of annotation: this.drawStrokePosition(this.inPointPosition, this.outPointPosition); this.updateAnnotations(false); //selects the right tag & highlights the annotation @@ -256,8 +270,9 @@ class Annotator extends EventTarget { } deselectAnnotation(keep_position) { - if (this.selectedAnnotation) - this._currentTimeMs = this.selectedAnnotation.t_out; + if (this.selectedAnnotation) { + this._seekByTimeMs(this.selectedAnnotation.t_out); + } this.wrapperEl.classList.remove('selected-annotation'); @@ -270,6 +285,18 @@ class Annotator extends EventTarget { this.updateAnnotations(false); // selects the right tag & highlights the annotation } + resetInOutPoint() { + this.inPointPosition = [0, 0]; + this.inPointTimeMs = null; + this.outPointPosition = null; + this.outPointTimeMs = null; + this._seekByTimeMs(this.audioOffset < 0 ? this.audioOffset * 1000 : 0); + // draw full stroke of annotation + console.log('reset!'); + this.drawStrokePosition(this.inPointPosition, [Infinity, Infinity]); + this.setUpAnnotator(); + } + load(file) { const request = new Request(file, { method: 'GET', @@ -284,6 +311,7 @@ class Annotator extends EventTarget { fetch(metadata_req) .then(response => response.ok ? response.json() : null) .then(metadata => { + metadata.annotations = metadata.annotations.map((a) => new Annotation(a.tag, a.t_in, a.t_out)) this.loadStrokes(data, metadata) }) .catch(e => console.log(e)); @@ -367,6 +395,7 @@ class Annotator extends EventTarget { this.updateAnnotations(true); this._currentTimeMs = t_out; + this.playheadEl.value = this._currentTimeMs; this.setUpAnnotator(); } } @@ -380,7 +409,7 @@ class Annotator extends EventTarget { this.inPointPosition = this.findPositionForTime(this.currentTime); this.inPointTimeMs = this._currentTimeMs; this.outPointPosition = this.findPositionForTime(this.lastFrameTime); // TODO: simplify to get the last frame indexes directly - this.outPointTimeMs = null; + this.outPointTimeMs = this.getEndTimeMs(); if (this.scrubberEl.noUiSlider) { this.slider.destroy(); @@ -388,7 +417,7 @@ class Annotator extends EventTarget { // console.log(this._currentTimeMs, ) this.slider = noUiSlider.create(this.scrubberEl, { - start: [this._currentTimeMs, this.lastFrameTime], + start: [this._currentTimeMs, this.getEndTimeMs()], connect: true, range: { 'min': this.audioOffset < 0 ? this.audioOffset * 1000 : 0, @@ -406,7 +435,7 @@ class Annotator extends EventTarget { }); this.slider.on("slide", (values, handle) => { - this.isPlaying = false; + this.videoIsPlaying = false; this.inPointPosition = this.findPositionForTime(values[0]); this.inPointTimeMs = Number.parseFloat(values[0]); this.outPointPosition = this.findPositionForTime(values[1]); @@ -415,8 +444,8 @@ class Annotator extends EventTarget { // console.log(this.selectedAnnotation); if (this.selectedAnnotation) { - this.selectedAnnotation.t_in = values[0]; - this.selectedAnnotation.t_out = values[1]; + this.selectedAnnotation.t_in = Number.parseFloat(values[0]); + this.selectedAnnotation.t_out = Number.parseFloat(values[1]); this.updateAnnotations(false); } }); @@ -424,8 +453,10 @@ class Annotator extends EventTarget { if (this.selectedAnnotation) { this.updateAnnotations(true); } - this.playAudioSegment(values[0], values[1]); - }) + this._seekByTimeMs(values[0]); + this.play(); + // this.playAudioSegment(values[0], values[1]); + }); this.drawStrokePosition(this.inPointPosition, this.outPointPosition); } @@ -437,6 +468,7 @@ class Annotator extends EventTarget { this.audioFile = metadata.hasOwnProperty('audio') ? metadata.audio.file : null; this.audioOffset = metadata.hasOwnProperty('audio') ? Number.parseFloat(metadata.audio.offset) : 0; this._currentTimeMs = this.audioOffset < 0 ? this.audioOffset * 1000 : 0; + this.playheadEl.value = this._currentTimeMs; // // load any saved metadata } @@ -459,6 +491,7 @@ class Annotator extends EventTarget { this.lastFrameTime = this.getFinalFrameTime(); this.playheadEl.max = this.lastFrameTime; this.nextFrameTimeout = null; + this._setPausedFlag(true); this.formatter = wNumb({ decimals: 2, @@ -480,6 +513,19 @@ class Annotator extends EventTarget { this.setupAudioConfig().then(() => { // this.setUpAnnotator() this.updateAnnotations(false); + + document.body.addEventListener('keyup', (ev) => { + if (ev.key == ' ') { + this.playPause(); + } + if (ev.key == 'Escape') { + if (this.selectedAnnotation) { + this.deselectAnnotation(); + } else { + this.resetInOutPoint(); + } + } + }); }); // this.playStrokePosition(0, 1); @@ -550,7 +596,7 @@ class Annotator extends EventTarget { this.setUpAnnotator(); resolve(); } - + }); } @@ -610,7 +656,12 @@ class Annotator extends EventTarget { console.log(this.audioEl.currentTime, t_start, t_in, t_out); } - this.audioEndTimeout = setTimeout((e) => this.audioEl.pause(), t_diff); + this.audioIsPlaying = true; // also state as playing in preroll + this.audioEndTimeout = setTimeout((e) => { + this.audioEl.pause(); + this.audioIsPlaying = false; + console.debug('done playing audio'); + }, t_diff); } getFinalFrameTime() { @@ -712,18 +763,19 @@ class Annotator extends EventTarget { playStrokePosition(path_i, point_i, allow_interrupt) { if (allow_interrupt) { - if (!this.isPlaying) { + if (!this.videoIsPlaying) { console.log('not playing because of interrupt'); return; } } else { - this.isPlaying = true; + this.videoIsPlaying = true; } this.drawStrokePosition(this.inPointPosition, [path_i, point_i]); const [next_path, next_point] = this.getNextPosition(path_i, point_i); if (next_path === null) { - console.log('done playing'); + console.debug('done playing video'); + this.videoIsPlaying = false; return; } @@ -742,6 +794,14 @@ class Annotator extends EventTarget { // this.playHead = ms; } + playPause() { + if (this.paused) { + this.play(); + } else { + this.pause() + } + } + /** * Compatibility with HTMLMediaElement API * @returns None @@ -756,7 +816,9 @@ class Annotator extends EventTarget { clearTimeout(this.audioStartTimeout); clearTimeout(this.startVideoTimeout); this.audioEl.pause(); - this.isPlaying = false; + this.videoIsPlaying = false; + this.audioIsPlaying = false; + this._setPausedFlag(true); } /** @@ -766,6 +828,7 @@ class Annotator extends EventTarget { play() { return new Promise((resolve, reject) => { this._interruptPlayback(); + this._seekByTimeMs(this._currentTimeMs); // prevent playback issue for initial load this.startTimeMs = window.performance.now() - this._currentTimeMs; @@ -773,26 +836,57 @@ class Annotator extends EventTarget { this.startVideoTimeout = setTimeout((e) => this.playStrokePosition(this.currentPathI, this.currentPointI), this._currentTimeMs * -1); } else { this.playStrokePosition(this.currentPathI, this.currentPointI); - } - console.log(this._currentTimeMs, this.outPointTimeMs); - this.playAudioSegment(this._currentTimeMs, this.outPointTimeMs); + } this.playAudioSegment(this._currentTimeMs, this.outPointTimeMs); // this.playStrokePosition(this.currentPathI, this.currentPointI); + this._setPausedFlag(false); + this.dispatchEvent(new CustomEvent('play', {})); this._animationFrame(); resolve(); }); } + _setPausedFlag(paused) { + this._paused = !!paused; //convert to boolean + if (paused) { + this.playPauseEl.classList.remove('playing'); + this.playPauseEl.classList.add('paused'); + } else { + this.playPauseEl.classList.remove('paused'); + this.playPauseEl.classList.add('playing'); + } + } + + get paused() { + return this._paused; + } + _animationFrame(timestamp) { // TODO, move time at end of playStrokePosition to here const nextTime = window.performance.now() - this.startTimeMs; const endTime = this.outPointTimeMs ?? this.duration * 1000; - if (nextTime > this.duration * 1000) { - + let interrupt = false; + if (nextTime > endTime) { + this._currentTimeMs = endTime; + interrupt = true; + } else { + this._currentTimeMs = nextTime; } this.playheadEl.value = this._currentTimeMs; - if (this.isPlaying) { + if (!interrupt && (this.videoIsPlaying || this.audioIsPlaying)) { window.requestAnimationFrame((timestamp) => this._animationFrame(timestamp)); + } else { + console.debug('finished playback'); + this._interruptPlayback(true); + this.resetPlayhead(); + } + } + + resetPlayhead() { + this._seekByTimeMs(this.inPointTimeMs); + if (this.selectedAnnotation) { + // show the hole selected annotation + this.drawStrokePosition(this.inPointPosition, this.outPointPosition); } } @@ -816,17 +910,22 @@ class Annotator extends EventTarget { this.dispatchEvent(new CustomEvent('seeking', {})); this._currentTimeMs = this.strokes[point[0]].points[point[1]][2]; [this.currentPathI, this.currentPointI] = point; + this.playheadEl.value = this._currentTimeMs; this._updateFrame(); // TODO set audio, wait for promise to finish this.dispatchEvent(new CustomEvent('seeked', {})); } + _seekByTimeMs(time) { + this._seekByTime(Number.parseFloat(time) / 1000); + } _seekByTime(time) { - this.dispatchEvent(new CustomEvent('seeking', { time: time })); + this.dispatchEvent(new CustomEvent('seeking', { detail: time })); this._currentTimeMs = Number.parseFloat(time) * 1000; [this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs); + this.playheadEl.value = this._currentTimeMs; this._updateFrame(); - this.dispatchEvent(new CustomEvent('seeked', { time: this.currentTime })); + this.dispatchEvent(new CustomEvent('seeked', { detail: this.currentTime })); } _updateFrame() { @@ -854,7 +953,7 @@ class Annotator extends EventTarget { const prerollDuration = this.audioOffset < 0 ? this.audioOffset * -1 : 0; - return prerollDuration + this.getEndTimeMs(); + return prerollDuration + this.getEndTimeMs() / 1000; } findPositionForTime(ms) {