Compare commits

..

No commits in common. "4cad6ed741489ea41fdf8ae466b27c44938a5ca0" and "9a8509e56dee44a8f4418c89aab0c988d25499da" have entirely different histories.

1 changed files with 97 additions and 240 deletions

View File

@ -109,10 +109,8 @@ class StrokeSlice {
} }
} }
class Annotator extends EventTarget { class Annotator {
constructor(wrapperEl, tags, fileurl) { constructor(wrapperEl, tags, fileurl) {
super();
this.wrapperEl = wrapperEl; this.wrapperEl = wrapperEl;
this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.wrapperEl.appendChild(this.svgEl); this.wrapperEl.appendChild(this.svgEl);
@ -122,13 +120,13 @@ class Annotator extends EventTarget {
this.controlsEl.classList.add('controls') this.controlsEl.classList.add('controls')
this.wrapperEl.appendChild(this.controlsEl); this.wrapperEl.appendChild(this.controlsEl);
this.playheadEl = document.createElement('input'); this.scrubberElOld = document.createElement('input');
this.playheadEl.type = "range"; this.scrubberElOld.type = "range";
this.playheadEl.min = 0; this.scrubberElOld.min = 0;
this.playheadEl.step = 0.01; this.scrubberElOld.step = 0.01;
this.controlsEl.appendChild(this.playheadEl); this.controlsEl.appendChild(this.scrubberElOld);
this.playheadEl.addEventListener("input", (ev) => { this.scrubberElOld.addEventListener("input", (ev) => {
this.scrubTo(ev.target.value); this.scrubTo(ev.target.value);
}) })
@ -172,11 +170,9 @@ class Annotator extends EventTarget {
this.controlsEl.appendChild(this.annotationsEl); this.controlsEl.appendChild(this.annotationsEl);
this.inPointPosition = [0, 0]; this.inPointPosition = null;
this.inPointTimeMs = null;
this.outPointPosition = null; this.outPointPosition = null;
this.outPointTimeMs = null; this.currentTime = 0;
this._currentTimeMs = 0;
this.isPlaying = false; this.isPlaying = false;
const groups = ['before', 'annotation', 'after'] const groups = ['before', 'annotation', 'after']
@ -190,7 +186,7 @@ class Annotator extends EventTarget {
this.annotations = []; this.annotations = [];
this.load(fileurl); this.play(fileurl);
} }
updateAnnotations(save) { updateAnnotations(save) {
@ -199,8 +195,8 @@ 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 left = (annotation.t_in / this.duration) * 100;
const right = 100 - (annotation.t_out / this.lastFrameTime) * 100; const right = 100 - (annotation.t_out / this.duration) * 100;
this.annotationEl.style.left = left + '%'; this.annotationEl.style.left = left + '%';
this.annotationEl.style.right = right + '%'; this.annotationEl.style.right = right + '%';
@ -257,7 +253,7 @@ class Annotator extends EventTarget {
deselectAnnotation(keep_position) { deselectAnnotation(keep_position) {
if (this.selectedAnnotation) if (this.selectedAnnotation)
this._currentTimeMs = this.selectedAnnotation.t_out; this.currentTime = this.selectedAnnotation.t_out;
this.wrapperEl.classList.remove('selected-annotation'); this.wrapperEl.classList.remove('selected-annotation');
@ -270,7 +266,7 @@ class Annotator extends EventTarget {
this.updateAnnotations(false); // selects the right tag & highlights the annotation this.updateAnnotations(false); // selects the right tag & highlights the annotation
} }
load(file) { play(file) {
const request = new Request(file, { const request = new Request(file, {
method: 'GET', method: 'GET',
}); });
@ -284,7 +280,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 => {
this.loadStrokes(data, metadata) this.playStrokes(data, metadata)
}) })
.catch(e => console.log(e)); .catch(e => console.log(e));
// do something with the data sent in the request // do something with the data sent in the request
@ -366,33 +362,26 @@ class Annotator extends EventTarget {
this.annotations.push(new Annotation(tag, t_in, t_out)); this.annotations.push(new Annotation(tag, t_in, t_out));
this.updateAnnotations(true); this.updateAnnotations(true);
this._currentTimeMs = t_out; this.currentTime = t_out;
this.setUpAnnotator(); this.setUpAnnotator();
} }
} }
setUpAnnotator() { setUpAnnotator() {
this.playheadEl.min = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
this.playheadEl.max = this.getEndTimeMs();
this.playheadEl.value = this._currentTimeMs;
this.inPointPosition = this.findPositionForTime(this.currentTime); this.inPointPosition = this.findPositionForTime(this.currentTime);
this.inPointTimeMs = this._currentTimeMs; this.outPointPosition = this.findPositionForTime(this.duration);
this.outPointPosition = this.findPositionForTime(this.lastFrameTime); // TODO: simplify to get the last frame indexes directly
this.outPointTimeMs = null;
if (this.scrubberEl.noUiSlider) { if (this.scrubberEl.noUiSlider) {
this.slider.destroy(); this.slider.destroy();
} }
// console.log(this._currentTimeMs, )
this.slider = noUiSlider.create(this.scrubberEl, { this.slider = noUiSlider.create(this.scrubberEl, {
start: [this._currentTimeMs, this.lastFrameTime], start: [this.currentTime, this.duration],
connect: true, connect: true,
range: { range: {
'min': this.audioOffset < 0 ? this.audioOffset * 1000 : 0, 'min': 0,
'max': this.getEndTimeMs(), 'max': this.duration
}, },
tooltips: [ tooltips: [
this.formatter, this.formatter,
@ -408,9 +397,7 @@ class Annotator extends EventTarget {
this.slider.on("slide", (values, handle) => { this.slider.on("slide", (values, handle) => {
this.isPlaying = false; this.isPlaying = false;
this.inPointPosition = this.findPositionForTime(values[0]); this.inPointPosition = this.findPositionForTime(values[0]);
this.inPointTimeMs = Number.parseFloat(values[0]);
this.outPointPosition = this.findPositionForTime(values[1]); this.outPointPosition = this.findPositionForTime(values[1]);
this.outPointTimeMs = Number.parseFloat(values[1]);
this.drawStrokePosition(this.inPointPosition, this.outPointPosition); this.drawStrokePosition(this.inPointPosition, this.outPointPosition);
// console.log(this.selectedAnnotation); // console.log(this.selectedAnnotation);
@ -430,13 +417,12 @@ class Annotator extends EventTarget {
this.drawStrokePosition(this.inPointPosition, this.outPointPosition); this.drawStrokePosition(this.inPointPosition, this.outPointPosition);
} }
loadStrokes(drawing, metadata) { playStrokes(drawing, metadata) {
this.audioOffset = 0; this.audioOffset = 0;
if (metadata) { if (metadata) {
this.annotations = metadata.annotations; this.annotations = metadata.annotations;
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') ? metadata.audio.offset : 0;
this._currentTimeMs = this.audioOffset < 0 ? this.audioOffset * 1000 : 0;
// //
// load any saved metadata // load any saved metadata
} }
@ -455,103 +441,84 @@ class Annotator extends EventTarget {
bgEl.classList.add('background'); bgEl.classList.add('background');
this.svgEl.prepend(bgEl); this.svgEl.prepend(bgEl);
this.firstFrameTime = this.strokes[0].points[0][3]; this.startTime = window.performance.now() - this.strokes[0].points[0][3];
this.lastFrameTime = this.getFinalFrameTime(); this.duration = this.getDuration();
this.playheadEl.max = this.lastFrameTime; this.scrubberElOld.max = this.duration;
this.nextFrameTimeout = null; this.playTimout = null;
this.formatter = wNumb({ this.formatter = wNumb({
decimals: 2, decimals: 2,
edit: (time) => { edit: (time) => {
let neg = "";
if (time < 0) {
neg = "-";
time *= -1;
}
const s = Math.floor(time / 1000); const s = Math.floor(time / 1000);
const minutes = Math.floor(s / 60); const minutes = Math.floor(s / 60);
const seconds = s - minutes * 60; const seconds = s - minutes * 60;
const ms = Math.floor((time / 1000 - s) * 1000); const ms = Math.floor((time / 1000 - s) * 1000);
return `${neg}${minutes}:${seconds}:${ms}`; return `${minutes}:${seconds}:${ms}`;
} }
}); });
this.setUpAnnotator()
this.updateAnnotations(false);
this.setupAudioConfig().then(() => { this.setupAudioConfig();
// this.setUpAnnotator()
this.updateAnnotations(false);
});
// this.playStrokePosition(0, 1); // this.playStrokePosition(0, 1);
} }
setupAudioConfig() { setupAudioConfig() {
// audio config // audio config
return new Promise((resolve, reject) => { let audioConfigEl = document.createElement('div');
audioConfigEl.classList.add('audioconfig')
this.wrapperEl.appendChild(audioConfigEl);
let audioConfigEl = document.createElement('div'); let audioSelectEl = document.createElement('select');
audioConfigEl.classList.add('audioconfig') audioSelectEl.classList.add('audioselect');
this.wrapperEl.appendChild(audioConfigEl); audioConfigEl.appendChild(audioSelectEl);
let audioSelectEl = document.createElement('select'); fetch('/audio')
audioSelectEl.classList.add('audioselect'); .then(response => response.json())
audioConfigEl.appendChild(audioSelectEl); .then(data => {
data.unshift(''); // add empty, to deselect any file
fetch('/audio') data.forEach(audioFile => {
.then(response => response.json()) let optionEl = document.createElement('option');
.then(data => { optionEl.selected = this.audioFile == audioFile;
data.unshift(''); // add empty, to deselect any file optionEl.innerText = audioFile;
data.forEach(audioFile => { audioSelectEl.appendChild(optionEl);
let optionEl = document.createElement('option'); });
optionEl.selected = this.audioFile == audioFile;
optionEl.innerText = audioFile;
audioSelectEl.appendChild(optionEl);
});
})
audioSelectEl.addEventListener('change', (ev) => {
this.setAudioFile(ev.target.value);
});
let audioOffsetTextEl = document.createElement('label');
audioOffsetTextEl.innerText = "Offset (s)";
audioConfigEl.appendChild(audioOffsetTextEl);
let audioOffsetEl = document.createElement('input');
audioOffsetEl.setAttribute('type', 'number');
audioOffsetEl.setAttribute('step', '.01');
audioOffsetEl.value = this.audioOffset ?? 0;
audioOffsetEl.addEventListener('change', (ev) => {
this.setAudioOffset(ev.target.value);
});
audioOffsetTextEl.appendChild(audioOffsetEl);
this.audioEl = document.createElement('audio');
this.audioEl.setAttribute('controls', true);
this.audioEl.addEventListener('canplaythrough', (ev) => {
console.log('loaded audio', ev);
this.audioEl.play();
});
// this.audioEl.addEventListener('seeked', (ev)=>{
// console.log(ev);
// })
audioConfigEl.prepend(this.audioEl);
this.audioEl.addEventListener('loadedmetadata', (ev) => {
// resolve the 'set up audio' when metadata has loaded
this.setUpAnnotator(); // if offset is negative, annotator starts at negative time
resolve();
}) })
if (this.audioFile) {
this.audioEl.setAttribute('src', this.audioFile);
} else {
this.setUpAnnotator();
resolve();
}
audioSelectEl.addEventListener('change', (ev) => {
this.setAudioFile(ev.target.value);
}); });
let audioOffsetTextEl = document.createElement('label');
audioOffsetTextEl.innerText = "Offset (s)";
audioConfigEl.appendChild(audioOffsetTextEl);
let audioOffsetEl = document.createElement('input');
audioOffsetEl.setAttribute('type', 'number');
audioOffsetEl.setAttribute('step', '.01');
audioOffsetEl.value = this.audioOffset ?? 0;
audioOffsetEl.addEventListener('change', (ev) => {
this.setAudioOffset(ev.target.value);
});
audioOffsetTextEl.appendChild(audioOffsetEl);
this.audioEl = document.createElement('audio');
if (this.audioFile) {
this.audioEl.setAttribute('src', this.audioFile);
}
this.audioEl.setAttribute('controls', true);
this.audioEl.addEventListener('canplaythrough', (ev) => {
console.log('loaded audio', ev);
this.audioEl.play();
});
// this.audioEl.addEventListener('seeked', (ev)=>{
// console.log(ev);
// })
audioConfigEl.prepend(this.audioEl);
} }
setAudioFile(audioFile) { setAudioFile(audioFile) {
@ -564,10 +531,9 @@ class Annotator extends EventTarget {
} }
setAudioOffset(audioOffset) { setAudioOffset(audioOffset) {
this.audioOffset = Number.parseFloat(audioOffset); this.audioOffset = audioOffset;
// TODO update playhead // TODO update playhead
// TODO update this.duration // TODO update this.duration
this.setUpAnnotator(); // if offset is negative, annotator starts at negative time
this.updateState(); this.updateState();
} }
@ -576,7 +542,7 @@ class Annotator extends EventTarget {
* @returns float * @returns float
*/ */
getAudioTime(time) { getAudioTime(time) {
return Number.parseFloat(time) - (this.audioOffset * 1000 ?? 0); return Number.parseFloat(time) + (this.audioOffset * 1000 ?? 0);
} }
/** /**
@ -590,15 +556,16 @@ class Annotator extends EventTarget {
// TODO, handle playback delay // TODO, handle playback delay
const t_start = this.getAudioTime(t_in); // in ms const t_start = this.getAudioTime(t_in); // in ms
const t_diff = (t_out ?? this.audioEl.duration * 1000) - t_in; // in ms const t_diff = t_out - t_in; // in ms
console.log('set time', t_in, t_start, typeof t_start, typeof t_in, t_start < 0);
this.audioEl.pause(); this.audioEl.pause();
if (t_start < 0) { if (t_start < 0) {
if (t_diff <= t_start * -1) { if (t_diff <= t_start * -1) {
console.debug('no audio playback in segment', t_start, t_diff); console.log('no audio playback in segment', t_start, t_diff);
} else { } else {
console.debug('delay audio playback', t_start, t_diff); console.log('huh?', t_start, t_diff);
// a negative audiooffset delays playback from the start // a negative audiooffset delays playback from the start
// this.audioStartTimeout = setTimeout((e) => this.audioEl.play(), t*-1000); // this.audioStartTimeout = setTimeout((e) => this.audioEl.play(), t*-1000);
this.audioStartTimeout = setTimeout((e) => { this.audioEl.currentTime = 0 }, t_start * -1); // triggers play with "seeked" event this.audioStartTimeout = setTimeout((e) => { this.audioEl.currentTime = 0 }, t_start * -1); // triggers play with "seeked" event
@ -607,13 +574,13 @@ class Annotator extends EventTarget {
} else { } else {
this.audioEl.currentTime = t_start / 1000; this.audioEl.currentTime = t_start / 1000;
// this.audioEl.play(); // play is done in "seeked" evenlistener // this.audioEl.play(); // play is done in "seeked" evenlistener
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.audioEndTimeout = setTimeout((e) => this.audioEl.pause(), t_diff);
} }
getFinalFrameTime() { getDuration() {
const points = this.strokes[this.strokes.length - 1].points; const points = this.strokes[this.strokes.length - 1].points;
return points[points.length - 1][3]; return points[points.length - 1][3];
} }
@ -719,7 +686,7 @@ class Annotator extends EventTarget {
} else { } else {
this.isPlaying = true; this.isPlaying = true;
} }
this.drawStrokePosition(this.inPointPosition, [path_i, point_i]); this.drawStrokePosition(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) {
@ -729,136 +696,26 @@ class Annotator extends EventTarget {
const t = this.strokes[next_path].points[next_point][3];// - path.points[point_i][3]; const t = this.strokes[next_path].points[next_point][3];// - path.points[point_i][3];
// calculate interval based on playback start to avoid drifting of time const dt = t - (window.performance.now() - this.startTime);
const dt = t - (window.performance.now() - this.startTimeMs); this.playTimout = setTimeout(() => this.playStrokePosition(next_path, next_point, true), dt);
this.nextFrameTimeout = setTimeout(() => this.playStrokePosition(next_path, next_point, true), dt); }
playUntil(path_i) {
// for scrubber
} }
scrubTo(ms) { scrubTo(ms) {
// const [path_i, point_i] = this.findPositionForTime(ms); const [path_i, point_i] = this.findPositionForTime(ms);
// console.log(path_i, point_i); // console.log(path_i, point_i);
this.pause(); clearTimeout(this.playTimout);
this._seekByTime(ms / 1000); this.playStrokePosition(path_i, point_i);
// this.playHead = ms; // this.playHead = ms;
} }
/**
* Compatibility with HTMLMediaElement API
* @returns None
*/
pause() {
this._interruptPlayback();
}
_interruptPlayback() {
clearTimeout(this.nextFrameTimeout);
clearTimeout(this.audioEndTimeout);
clearTimeout(this.audioStartTimeout);
clearTimeout(this.startVideoTimeout);
this.audioEl.pause();
this.isPlaying = false;
}
/**
* Compatibility with HTMLMediaElement API
* @returns Promise
*/
play() {
return new Promise((resolve, reject) => {
this._interruptPlayback();
this.startTimeMs = window.performance.now() - this._currentTimeMs;
if (this._currentTimeMs < 0) {
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.playStrokePosition(this.currentPathI, this.currentPointI);
this.dispatchEvent(new CustomEvent('play', {}));
this._animationFrame();
resolve();
});
}
_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) {
}
this.playheadEl.value = this._currentTimeMs;
if (this.isPlaying) {
window.requestAnimationFrame((timestamp) => this._animationFrame(timestamp));
}
}
/**
* Note that both t_in and t_out can be negative
* @param float|Array t_in in point time, in ms or array with path/frame points
* @param float|Array t_out out point time, in ms or array with path/frame points
*/
playSegment(in_point, out_point) {
if (!Array.isArray(in_point)) in_point = this.findPositionForTime(in_point);
if (!Array.isArray(out_point)) out_point = this.findPositionForTime(out_point);
this.inPointPosition = in_point;
this.outPointPosition = out_point;
this._seekByPoint(in_point);
this.play();
}
_seekByPoint(point) {
this.dispatchEvent(new CustomEvent('seeking', {}));
this._currentTimeMs = this.strokes[point[0]].points[point[1]][2];
[this.currentPathI, this.currentPointI] = point;
this._updateFrame();
// TODO set audio, wait for promise to finish
this.dispatchEvent(new CustomEvent('seeked', {}));
}
_seekByTime(time) {
this.dispatchEvent(new CustomEvent('seeking', { time: time }));
this._currentTimeMs = Number.parseFloat(time) * 1000;
[this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs);
this._updateFrame();
this.dispatchEvent(new CustomEvent('seeked', { time: this.currentTime }));
}
_updateFrame() {
this.drawStrokePosition(this.inPointPosition, [this.currentPathI, this.currentPointI]);
}
/**
* For compatibility with HTMLMediaElement API convert seconds to ms of internal timer
*/
set currentTime(time) {
this._seekByTime(time);
}
get currentTime() {
return this._currentTimeMs / 1000;
}
getEndTimeMs() {
const videoDuration = this.getFinalFrameTime();
const audioDuration = (this.audioEl) ? this.audioEl.duration + this.audioOffset : 0;
return Math.max(videoDuration, audioDuration * 1000);
}
get duration() {
const prerollDuration = this.audioOffset < 0 ? this.audioOffset * -1 : 0;
return prerollDuration + this.getEndTimeMs();
}
findPositionForTime(ms) { findPositionForTime(ms) {
ms = Math.min(Math.max(ms, 0), this.lastFrameTime); ms = Math.min(Math.max(ms, 0), this.duration);
// console.log('scrub to', ms) // console.log('scrub to', ms)
let path_i = 0; let path_i = 0;
let point_i = 0; let point_i = 0;