Play back working

This commit is contained in:
Ruben van de Ven 2021-12-23 15:54:03 +01:00
parent 4cad6ed741
commit 16d8259866
2 changed files with 142 additions and 30 deletions

View file

@ -82,6 +82,18 @@
width: 100%; 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 { .controls {
position: absolute !important; position: absolute !important;
z-index: 100; z-index: 100;
@ -227,6 +239,7 @@
vertical-align: middle; vertical-align: middle;
width: 100px; /* hides seek head */ width: 100px; /* hides seek head */
} }
</style> </style>
<link rel="stylesheet" href="assets/nouislider-15.5.0.css"> <link rel="stylesheet" href="assets/nouislider-15.5.0.css">
<link rel="stylesheet" href="core.css"> <link rel="stylesheet" href="core.css">

View file

@ -1,8 +1,8 @@
class Annotation { class Annotation {
constructor(tag, t_in, t_out) { constructor(tag, t_in, t_out) {
this.tag = tag; this.tag = tag;
this.t_in = t_in; this.t_in = Number.parseFloat(t_in);
this.t_out = t_out; this.t_out = Number.parseFloat(t_out);
} }
} }
@ -132,6 +132,14 @@ class Annotator extends EventTarget {
this.scrubTo(ev.target.value); 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 = document.createElement('div');
this.scrubberEl.classList.add('scrubber') this.scrubberEl.classList.add('scrubber')
this.controlsEl.appendChild(this.scrubberEl); this.controlsEl.appendChild(this.scrubberEl);
@ -157,7 +165,7 @@ class Annotator extends EventTarget {
tagEl.classList.add('annotation-rm'); tagEl.classList.add('annotation-rm');
tagEl.dataset.tag = 'rm'; tagEl.dataset.tag = 'rm';
tagEl.title = "Remove annotation"; tagEl.title = "Remove annotation";
tagEl.innerHTML = "&times;"; tagEl.innerHTML = "🚮"; // &times;
tagEl.addEventListener('click', (e) => { tagEl.addEventListener('click', (e) => {
if (this.selectedAnnotation) { if (this.selectedAnnotation) {
this.removeAnnotation(this.selectedAnnotationI); this.removeAnnotation(this.selectedAnnotationI);
@ -177,7 +185,7 @@ class Annotator extends EventTarget {
this.outPointPosition = null; this.outPointPosition = null;
this.outPointTimeMs = null; this.outPointTimeMs = null;
this._currentTimeMs = 0; this._currentTimeMs = 0;
this.isPlaying = false; this.videoIsPlaying = false;
const groups = ['before', 'annotation', 'after'] const groups = ['before', 'annotation', 'after']
this.strokeGroups = {}; this.strokeGroups = {};
@ -199,8 +207,10 @@ class Annotator extends EventTarget {
for (let annotation_i in this.annotations) { for (let annotation_i in this.annotations) {
const annotation = this.annotations[annotation_i]; const annotation = this.annotations[annotation_i];
this.annotationEl = document.createElement('div'); this.annotationEl = document.createElement('div');
const left = (annotation.t_in / this.lastFrameTime) * 100; const prerollDiff = Number.parseFloat(this.audioOffset < 0 ? this.audioOffset * -1000 : 0);
const right = 100 - (annotation.t_out / this.lastFrameTime) * 100; // 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.left = left + '%';
this.annotationEl.style.right = right + '%'; this.annotationEl.style.right = right + '%';
@ -248,6 +258,10 @@ class Annotator extends EventTarget {
this.inPointPosition = this.findPositionForTime(this.selectedAnnotation.t_in); this.inPointPosition = this.findPositionForTime(this.selectedAnnotation.t_in);
this.outPointPosition = this.findPositionForTime(this.selectedAnnotation.t_out); 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.drawStrokePosition(this.inPointPosition, this.outPointPosition);
this.updateAnnotations(false); //selects the right tag & highlights the annotation this.updateAnnotations(false); //selects the right tag & highlights the annotation
@ -256,8 +270,9 @@ class Annotator extends EventTarget {
} }
deselectAnnotation(keep_position) { deselectAnnotation(keep_position) {
if (this.selectedAnnotation) if (this.selectedAnnotation) {
this._currentTimeMs = this.selectedAnnotation.t_out; this._seekByTimeMs(this.selectedAnnotation.t_out);
}
this.wrapperEl.classList.remove('selected-annotation'); this.wrapperEl.classList.remove('selected-annotation');
@ -270,6 +285,18 @@ class Annotator extends EventTarget {
this.updateAnnotations(false); // selects the right tag & highlights the annotation 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) { load(file) {
const request = new Request(file, { const request = new Request(file, {
method: 'GET', method: 'GET',
@ -284,6 +311,7 @@ class Annotator extends EventTarget {
fetch(metadata_req) fetch(metadata_req)
.then(response => response.ok ? response.json() : null) .then(response => response.ok ? response.json() : null)
.then(metadata => { .then(metadata => {
metadata.annotations = metadata.annotations.map((a) => new Annotation(a.tag, a.t_in, a.t_out))
this.loadStrokes(data, metadata) this.loadStrokes(data, metadata)
}) })
.catch(e => console.log(e)); .catch(e => console.log(e));
@ -367,6 +395,7 @@ class Annotator extends EventTarget {
this.updateAnnotations(true); this.updateAnnotations(true);
this._currentTimeMs = t_out; this._currentTimeMs = t_out;
this.playheadEl.value = this._currentTimeMs;
this.setUpAnnotator(); this.setUpAnnotator();
} }
} }
@ -380,7 +409,7 @@ class Annotator extends EventTarget {
this.inPointPosition = this.findPositionForTime(this.currentTime); this.inPointPosition = this.findPositionForTime(this.currentTime);
this.inPointTimeMs = this._currentTimeMs; this.inPointTimeMs = this._currentTimeMs;
this.outPointPosition = this.findPositionForTime(this.lastFrameTime); // TODO: simplify to get the last frame indexes directly 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) { if (this.scrubberEl.noUiSlider) {
this.slider.destroy(); this.slider.destroy();
@ -388,7 +417,7 @@ class Annotator extends EventTarget {
// console.log(this._currentTimeMs, ) // console.log(this._currentTimeMs, )
this.slider = noUiSlider.create(this.scrubberEl, { this.slider = noUiSlider.create(this.scrubberEl, {
start: [this._currentTimeMs, this.lastFrameTime], start: [this._currentTimeMs, this.getEndTimeMs()],
connect: true, connect: true,
range: { range: {
'min': this.audioOffset < 0 ? this.audioOffset * 1000 : 0, 'min': this.audioOffset < 0 ? this.audioOffset * 1000 : 0,
@ -406,7 +435,7 @@ class Annotator extends EventTarget {
}); });
this.slider.on("slide", (values, handle) => { this.slider.on("slide", (values, handle) => {
this.isPlaying = false; this.videoIsPlaying = false;
this.inPointPosition = this.findPositionForTime(values[0]); this.inPointPosition = this.findPositionForTime(values[0]);
this.inPointTimeMs = Number.parseFloat(values[0]); this.inPointTimeMs = Number.parseFloat(values[0]);
this.outPointPosition = this.findPositionForTime(values[1]); this.outPointPosition = this.findPositionForTime(values[1]);
@ -415,8 +444,8 @@ class Annotator extends EventTarget {
// console.log(this.selectedAnnotation); // console.log(this.selectedAnnotation);
if (this.selectedAnnotation) { if (this.selectedAnnotation) {
this.selectedAnnotation.t_in = values[0]; this.selectedAnnotation.t_in = Number.parseFloat(values[0]);
this.selectedAnnotation.t_out = values[1]; this.selectedAnnotation.t_out = Number.parseFloat(values[1]);
this.updateAnnotations(false); this.updateAnnotations(false);
} }
}); });
@ -424,8 +453,10 @@ class Annotator extends EventTarget {
if (this.selectedAnnotation) { if (this.selectedAnnotation) {
this.updateAnnotations(true); 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); this.drawStrokePosition(this.inPointPosition, this.outPointPosition);
} }
@ -437,6 +468,7 @@ class Annotator extends EventTarget {
this.audioFile = metadata.hasOwnProperty('audio') ? metadata.audio.file : null; this.audioFile = metadata.hasOwnProperty('audio') ? metadata.audio.file : null;
this.audioOffset = metadata.hasOwnProperty('audio') ? Number.parseFloat(metadata.audio.offset) : 0; this.audioOffset = metadata.hasOwnProperty('audio') ? Number.parseFloat(metadata.audio.offset) : 0;
this._currentTimeMs = this.audioOffset < 0 ? this.audioOffset * 1000 : 0; this._currentTimeMs = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
this.playheadEl.value = this._currentTimeMs;
// //
// load any saved metadata // load any saved metadata
} }
@ -459,6 +491,7 @@ class Annotator extends EventTarget {
this.lastFrameTime = this.getFinalFrameTime(); this.lastFrameTime = this.getFinalFrameTime();
this.playheadEl.max = this.lastFrameTime; this.playheadEl.max = this.lastFrameTime;
this.nextFrameTimeout = null; this.nextFrameTimeout = null;
this._setPausedFlag(true);
this.formatter = wNumb({ this.formatter = wNumb({
decimals: 2, decimals: 2,
@ -480,6 +513,19 @@ class Annotator extends EventTarget {
this.setupAudioConfig().then(() => { this.setupAudioConfig().then(() => {
// this.setUpAnnotator() // this.setUpAnnotator()
this.updateAnnotations(false); 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); // this.playStrokePosition(0, 1);
@ -610,7 +656,12 @@ class Annotator extends EventTarget {
console.log(this.audioEl.currentTime, t_start, t_in, t_out); 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() { getFinalFrameTime() {
@ -712,18 +763,19 @@ class Annotator extends EventTarget {
playStrokePosition(path_i, point_i, allow_interrupt) { playStrokePosition(path_i, point_i, allow_interrupt) {
if (allow_interrupt) { if (allow_interrupt) {
if (!this.isPlaying) { if (!this.videoIsPlaying) {
console.log('not playing because of interrupt'); console.log('not playing because of interrupt');
return; return;
} }
} else { } else {
this.isPlaying = true; this.videoIsPlaying = true;
} }
this.drawStrokePosition(this.inPointPosition, [path_i, point_i]); this.drawStrokePosition(this.inPointPosition, [path_i, point_i]);
const [next_path, next_point] = this.getNextPosition(path_i, point_i); const [next_path, next_point] = this.getNextPosition(path_i, point_i);
if (next_path === null) { if (next_path === null) {
console.log('done playing'); console.debug('done playing video');
this.videoIsPlaying = false;
return; return;
} }
@ -742,6 +794,14 @@ class Annotator extends EventTarget {
// this.playHead = ms; // this.playHead = ms;
} }
playPause() {
if (this.paused) {
this.play();
} else {
this.pause()
}
}
/** /**
* Compatibility with HTMLMediaElement API * Compatibility with HTMLMediaElement API
* @returns None * @returns None
@ -756,7 +816,9 @@ class Annotator extends EventTarget {
clearTimeout(this.audioStartTimeout); clearTimeout(this.audioStartTimeout);
clearTimeout(this.startVideoTimeout); clearTimeout(this.startVideoTimeout);
this.audioEl.pause(); this.audioEl.pause();
this.isPlaying = false; this.videoIsPlaying = false;
this.audioIsPlaying = false;
this._setPausedFlag(true);
} }
/** /**
@ -766,6 +828,7 @@ class Annotator extends EventTarget {
play() { play() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._interruptPlayback(); this._interruptPlayback();
this._seekByTimeMs(this._currentTimeMs); // prevent playback issue for initial load
this.startTimeMs = window.performance.now() - this._currentTimeMs; 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); this.startVideoTimeout = setTimeout((e) => this.playStrokePosition(this.currentPathI, this.currentPointI), this._currentTimeMs * -1);
} else { } else {
this.playStrokePosition(this.currentPathI, this.currentPointI); this.playStrokePosition(this.currentPathI, this.currentPointI);
} } this.playAudioSegment(this._currentTimeMs, this.outPointTimeMs);
console.log(this._currentTimeMs, this.outPointTimeMs);
this.playAudioSegment(this._currentTimeMs, this.outPointTimeMs);
// this.playStrokePosition(this.currentPathI, this.currentPointI); // this.playStrokePosition(this.currentPathI, this.currentPointI);
this._setPausedFlag(false);
this.dispatchEvent(new CustomEvent('play', {})); this.dispatchEvent(new CustomEvent('play', {}));
this._animationFrame(); this._animationFrame();
resolve(); 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) { _animationFrame(timestamp) {
// TODO, move time at end of playStrokePosition to here // TODO, move time at end of playStrokePosition to here
const nextTime = window.performance.now() - this.startTimeMs; const nextTime = window.performance.now() - this.startTimeMs;
const endTime = this.outPointTimeMs ?? this.duration * 1000; 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; this.playheadEl.value = this._currentTimeMs;
if (this.isPlaying) { if (!interrupt && (this.videoIsPlaying || this.audioIsPlaying)) {
window.requestAnimationFrame((timestamp) => this._animationFrame(timestamp)); 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.dispatchEvent(new CustomEvent('seeking', {}));
this._currentTimeMs = this.strokes[point[0]].points[point[1]][2]; this._currentTimeMs = this.strokes[point[0]].points[point[1]][2];
[this.currentPathI, this.currentPointI] = point; [this.currentPathI, this.currentPointI] = point;
this.playheadEl.value = this._currentTimeMs;
this._updateFrame(); this._updateFrame();
// TODO set audio, wait for promise to finish // TODO set audio, wait for promise to finish
this.dispatchEvent(new CustomEvent('seeked', {})); this.dispatchEvent(new CustomEvent('seeked', {}));
} }
_seekByTimeMs(time) {
this._seekByTime(Number.parseFloat(time) / 1000);
}
_seekByTime(time) { _seekByTime(time) {
this.dispatchEvent(new CustomEvent('seeking', { time: time })); this.dispatchEvent(new CustomEvent('seeking', { detail: time }));
this._currentTimeMs = Number.parseFloat(time) * 1000; this._currentTimeMs = Number.parseFloat(time) * 1000;
[this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs); [this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs);
this.playheadEl.value = this._currentTimeMs;
this._updateFrame(); this._updateFrame();
this.dispatchEvent(new CustomEvent('seeked', { time: this.currentTime })); this.dispatchEvent(new CustomEvent('seeked', { detail: this.currentTime }));
} }
_updateFrame() { _updateFrame() {
@ -854,7 +953,7 @@ class Annotator extends EventTarget {
const prerollDuration = this.audioOffset < 0 ? this.audioOffset * -1 : 0; const prerollDuration = this.audioOffset < 0 ? this.audioOffset * -1 : 0;
return prerollDuration + this.getEndTimeMs(); return prerollDuration + this.getEndTimeMs() / 1000;
} }
findPositionForTime(ms) { findPositionForTime(ms) {