Add all work on statistics. Still need to add work on display manipulation but the ground is laid.

This commit is contained in:
Matt McWilliams 2024-10-18 22:13:52 -04:00
parent a4d964ef20
commit 1be3c8308d
19 changed files with 573 additions and 131 deletions

27
browser/globals.d.ts vendored
View File

@ -18,12 +18,39 @@ interface SequenceState {
status? : any status? : any
} }
interface SequenceStatistics {
totalFrameLast? : number,
totalFrameAvg? : number,
totalFrameMargin? : number,
fps? : number,
loadAvg? : number,
loadLast? : number,
loadMargin? : number,
openLast? : number,
openAvg? : number,
openMargin? : number,
closeLast? : number,
closeAvg? : number,
closeMargin? : number,
exposureLast? : number,
exposureAvg? : number,
exposureMargin? : number,
elapsed? : number,
estimate? : number
}
interface State { interface State {
display? : StateDimensions, display? : StateDimensions,
offset? : StateOffset, offset? : StateOffset,
source? : StateDimensions, source? : StateDimensions,
screen? : StateDimensions, screen? : StateDimensions,
sequence? : SequenceState, sequence? : SequenceState,
statistics? : SequenceStatistics,
exposure? : number exposure? : number
} }

View File

@ -1,3 +1,5 @@
let client : Client;
enum SequenceStatus { enum SequenceStatus {
IDLE, IDLE,
RUNNING, RUNNING,
@ -152,6 +154,7 @@ class Client {
(document.getElementById('sequenceCtrlForm') as HTMLFormElement ).reset(); (document.getElementById('sequenceCtrlForm') as HTMLFormElement ).reset();
(document.getElementById('manualCtrlForm') as HTMLFormElement ).reset(); (document.getElementById('manualCtrlForm') as HTMLFormElement ).reset();
(document.getElementById('exposureCtrlForm') as HTMLFormElement ).reset(); (document.getElementById('exposureCtrlForm') as HTMLFormElement ).reset();
(document.getElementById('statisticsForm') as HTMLFormElement).reset();
this.disableClass('sequenceCtrl'); this.disableClass('sequenceCtrl');
this.disableClass('manualCtrl'); this.disableClass('manualCtrl');
this.disableClass('exposureCtrl'); this.disableClass('exposureCtrl');
@ -191,7 +194,8 @@ class Client {
this.setStatus(state.sequence); this.setStatus(state.sequence);
this.setExposure(state); this.setExposure(state);
this.setDisplay(state); this.setDisplay(state);
(document.getElementById('sequence') as HTMLSelectElement ).value = state.sequence.hash; this.set('sequence', state.sequence.hash);
this.removeClass('sequence', 'edited');
} }
private setUpdate(state : State) { private setUpdate(state : State) {
@ -199,6 +203,7 @@ class Client {
this.setFrame(state.sequence); this.setFrame(state.sequence);
this.setStatus(state.sequence); this.setStatus(state.sequence);
this.setExposure(state); this.setExposure(state);
this.setStatistics(state.statistics);
this.display.updateImage(); this.display.updateImage();
} }
@ -225,45 +230,72 @@ class Client {
private setProgress (sequence : SequenceState) { private setProgress (sequence : SequenceState) {
const percent : number = sequence.progress * 100.0; const percent : number = sequence.progress * 100.0;
if (this.progress !== null) {
this.progress.value = sequence.progress; this.progress.value = sequence.progress;
this.progressText.innerText = `Progress: ${Math.floor(percent)}%`; this.progressText.innerText = `Progress: ${Math.floor(percent)}%`;
} }
}
private setFrame (sequence : SequenceState) { private setFrame (sequence : SequenceState) {
if (typeof sequence.current !== 'undefined') { if (typeof sequence.current !== 'undefined') {
(document.getElementById('frame') as HTMLInputElement).value = `${sequence.current}`.padStart(5, '0'); this.set('frame', `${sequence.current}`.padStart(5, '0'));
this.removeClass('frame', 'edited');
} }
} }
private setExposure (state : State) { private setExposure (state : State) {
if (typeof state.exposure !== 'undefined') { if (typeof state.exposure !== 'undefined') {
const el : HTMLInputElement = document.getElementById('exposure') as HTMLInputElement;
this.enableClass('exposureCtrl'); this.enableClass('exposureCtrl');
(document.getElementById('exposure') as HTMLInputElement).value = `${state.exposure}`; this.set('exposure', `${state.exposure}`);
this.removeClass('exposure', 'edited');
} }
} }
private setDisplay (state : State) { private setDisplay (state : State) {
const widthEl : HTMLInputElement = document.getElementById('displayWidth') as HTMLInputElement;
const heightEl : HTMLInputElement = document.getElementById('displayHeight') as HTMLInputElement;
const srcWidthEl : HTMLInputElement = document.getElementById('sourceWidth') as HTMLInputElement;
const srcHeightEl : HTMLInputElement = document.getElementById('sourceHeight') as HTMLInputElement;
if (typeof state.display !== 'undefined') { if (typeof state.display !== 'undefined') {
widthEl.value = state.display.width as any; this.set('displayWidth', state.display.width.toString());
heightEl.value = state.display.height as any; this.set('displayHeight', state.display.height.toString());
this.set('sourceWidth', state.source.width.toString());
this.set('sourceHeight', state.source.height.toString());
srcWidthEl.value = state.source.width as any; //widthEl.readOnly = false;
srcHeightEl.value = state.source.height as any; //heightEl.readOnly = false;
widthEl.readOnly = false;
heightEl.readOnly = false;
//console.dir(state); //console.dir(state);
this.display.set(state); this.display.set(state);
} }
} }
public edited (el : HTMLElement) {
el.classList.add('edited');
}
private setStatistics (stats : SequenceStatistics) {
if (stats !== null) {
this.set('statsFrameTotalAvg', stats.totalFrameAvg.toString());
this.set('statsFrameTotalLast', stats.totalFrameLast.toString());
this.set('statsFrameTotalMargin', stats.totalFrameMargin.toString());
this.set('statsFPS', stats.fps.toString());
this.set('statsFrameLoadAvg', stats.loadAvg.toString());
this.set('statsFrameLoadMargin', stats.loadMargin.toString());
this.set('statsFrameOpenLast', stats.openLast.toString());
this.set('statsFrameOpenAvg', stats.openAvg.toString());
this.set('statsFrameOpenMargin', stats.openMargin.toString());
this.set('statsFrameCloseLast', stats.closeLast.toString());
this.set('statsFrameCloseAvg', stats.closeAvg.toString());
this.set('statsFrameCloseMargin', stats.closeMargin.toString());
this.set('statsExposureLast', stats.exposureLast.toString());
this.set('statsExposureAvg', stats.exposureAvg.toString());
this.set('statsExposureMargin', stats.exposureMargin.toString());
//this.set('statsFrameTotalAvg', stats.elapsed.toString());
//this.set('statsFrameTotalAvg', stats.estimate.toString());
}
}
private cmd (msg : Message) { private cmd (msg : Message) {
switch (msg.cmd) { switch (msg.cmd) {
case 'ping' : case 'ping' :
@ -374,14 +406,11 @@ class Client {
return; return;
} }
msg = { cmd : 'select', state : { sequence : { hash } } }; msg = { cmd : 'select', state : { sequence : { hash } } };
console.log('send select'); console.log(`send select ${hash}`);
console.log(hash)
this.client.send(JSON.stringify(msg)); this.client.send(JSON.stringify(msg));
} }
private receiveSelect (msg : Message) { private receiveSelect (msg : Message) {
console.log('got select');
//console.dir(msg);
this.enableClass('sequenceCtrl'); this.enableClass('sequenceCtrl');
this.setSequence(msg.state); this.setSequence(msg.state);
} }
@ -395,7 +424,7 @@ class Client {
} }
public sendExposure () { public sendExposure () {
const exposure : number = parseInt((document.getElementById('exposure') as HTMLSelectElement ).value); const exposure : number = parseInt(this.get('exposure'));
this.client.send(JSON.stringify({ cmd : 'exposure', state : { exposure }})); this.client.send(JSON.stringify({ cmd : 'exposure', state : { exposure }}));
} }
@ -418,11 +447,30 @@ class Client {
} }
private active () { private active () {
document.getElementById('overlay').classList.add('active'); this.addClass('overlay', 'active');
} }
private inactive () { private inactive () {
document.getElementById('overlay').classList.remove('active'); this.removeClass('overlay', 'active');
}
public addClass (id : string, className : string) {
document.getElementById(id).classList.add(className);
}
public removeClass (id : string, className : string) {
document.getElementById(id).classList.remove(className);
}
public set (id : string, value : string) {
try {
(document.getElementById(id) as HTMLInputElement).value = value;
} catch (err) {
console.warn(`Element ${id} does not exist or cannot be set`);
}
}
public get (id : string) {
return (document.getElementById(id) as HTMLInputElement).value;
} }
} }
const client : Client = new Client(); client = new Client();

16
dist/camera/index.js vendored
View File

@ -221,35 +221,35 @@ class Camera {
}.bind(this)); }.bind(this));
} }
async frame() { async frame() {
const start = +new Date(); const start = Date.now();
let ms; let ms;
await this.confirm(Commands.CAMERA, Commands.CAMERA); await this.confirm(Commands.CAMERA, Commands.CAMERA);
ms = (+new Date()) - start; ms = (Date.now()) - start;
this.log.info(this.prefix + `frame() - ${ms}ms`); this.log.info(this.prefix + `frame() - ${ms}ms`);
return ms; return ms;
} }
async open() { async open() {
const start = +new Date(); const start = Date.now();
let ms; let ms;
await this.confirm(Commands.CAMERA_OPEN, Commands.CAMERA_OPEN); await this.confirm(Commands.CAMERA_OPEN, Commands.CAMERA_OPEN);
ms = (+new Date()) - start; ms = (Date.now()) - start;
this.log.info(this.prefix + `open() - ${ms}ms`); this.log.info(this.prefix + `open() - ${ms}ms`);
return ms; return ms;
} }
async close() { async close() {
const start = +new Date(); const start = Date.now();
let ms; let ms;
await this.confirm(Commands.CAMERA_CLOSE, Commands.CAMERA_CLOSE); await this.confirm(Commands.CAMERA_CLOSE, Commands.CAMERA_CLOSE);
ms = (+new Date()) - start; ms = (Date.now()) - start;
this.log.info(this.prefix + `close() - ${ms}ms`); this.log.info(this.prefix + `close() - ${ms}ms`);
return ms; return ms;
} }
async direction(dir) { async direction(dir) {
const start = +new Date(); const start = Date.now();
let ms; let ms;
const cmd = dir ? Commands.CAMERA_FORWARD : Commands.CAMERA_BACKWARD; const cmd = dir ? Commands.CAMERA_FORWARD : Commands.CAMERA_BACKWARD;
await this.confirm(cmd, cmd); await this.confirm(cmd, cmd);
ms = (+new Date()) - start; ms = (Date.now()) - start;
this.log.info(this.prefix + `direction(${dir}) - ${ms}ms`); this.log.info(this.prefix + `direction(${dir}) - ${ms}ms`);
return ms; return ms;
} }

File diff suppressed because one or more lines are too long

4
dist/fd/index.d.ts vendored
View File

@ -27,13 +27,15 @@ interface fdOutgoingMessage {
} }
interface fdIncomingMessage { interface fdIncomingMessage {
action: Action; action: Action;
data?: number;
success: boolean; success: boolean;
error?: string; error?: string;
} }
interface fdResult { interface fdResult {
action: Action; action: Action;
image: string; image: string;
time: number; reported?: number;
elapsed?: number;
} }
export declare class FD { export declare class FD {
private bin; private bin;

25
dist/fd/index.js vendored
View File

@ -126,12 +126,13 @@ class FD {
h h
} }
}; };
const startTime = +new Date(); const startTime = Date.now();
if (this.mock) { if (this.mock) {
return { return {
action: Action.LOAD, action: Action.LOAD,
image, image,
time: (+new Date()) - startTime reported: (Date.now()) - startTime,
elapsed: (Date.now()) - startTime
}; };
} }
const promise = new Promise(function (resolve, reject) { const promise = new Promise(function (resolve, reject) {
@ -140,7 +141,8 @@ class FD {
return resolve({ return resolve({
action: Action.LOAD, action: Action.LOAD,
image, image,
time: (+new Date()) - startTime reported: msg.data,
elapsed: (Date.now()) - startTime
}); });
} }
else if (typeof msg.error !== 'undefined') { else if (typeof msg.error !== 'undefined') {
@ -158,7 +160,7 @@ class FD {
image, image,
exposure exposure
}; };
const startTime = +new Date(); const startTime = Date.now();
if (this.mock) { if (this.mock) {
for (let exp of exposure) { for (let exp of exposure) {
await (0, delay_1.delay)(exp); await (0, delay_1.delay)(exp);
@ -166,7 +168,8 @@ class FD {
return { return {
action: Action.DISPLAY, action: Action.DISPLAY,
image, image,
time: (+new Date()) - startTime reported: (Date.now()) - startTime,
elapsed: (Date.now()) - startTime
}; };
} }
const promise = new Promise(function (resolve, reject) { const promise = new Promise(function (resolve, reject) {
@ -175,7 +178,8 @@ class FD {
return resolve({ return resolve({
action: Action.DISPLAY, action: Action.DISPLAY,
image, image,
time: (+new Date()) - startTime reported: msg.data,
elapsed: (Date.now()) - startTime
}); });
} }
else if (typeof msg.error !== 'undefined') { else if (typeof msg.error !== 'undefined') {
@ -192,21 +196,24 @@ class FD {
action: Action.STOP, action: Action.STOP,
image image
}; };
const startTime = +new Date(); const startTime = Date.now();
if (this.mock) { if (this.mock) {
return { return {
action: Action.STOP, action: Action.STOP,
image, image,
time: (+new Date()) - startTime reported: (Date.now()) - startTime,
elapsed: (Date.now()) - startTime
}; };
} }
const promise = new Promise(function (resolve, reject) { const promise = new Promise(function (resolve, reject) {
this.waiting = function (msg) { this.waiting = function (msg) {
if (msg.action == Action.STOP && msg.success) { if (msg.action == Action.STOP && msg.success) {
this.log.info(msg.data);
return resolve({ return resolve({
action: Action.STOP, action: Action.STOP,
image, image,
time: (+new Date()) - startTime reported: msg.data,
elapsed: (Date.now()) - startTime
}); });
} }
else if (typeof msg.error !== 'undefined') { else if (typeof msg.error !== 'undefined') {

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,7 @@ export declare class Sequence {
private ffprobe; private ffprobe;
private fd; private fd;
private send; private send;
private stats;
private running; private running;
private paused; private paused;
private progress; private progress;

View File

@ -9,11 +9,78 @@ var SequenceStatus;
SequenceStatus[SequenceStatus["RUNNING"] = 1] = "RUNNING"; SequenceStatus[SequenceStatus["RUNNING"] = 1] = "RUNNING";
SequenceStatus[SequenceStatus["PAUSED"] = 2] = "PAUSED"; SequenceStatus[SequenceStatus["PAUSED"] = 2] = "PAUSED";
})(SequenceStatus || (SequenceStatus = {})); })(SequenceStatus || (SequenceStatus = {}));
class Statistics {
constructor(exposure, frames) {
this.exposure = exposure;
this.frames = frames;
this.frameLoad = [];
this.frameOpen = [];
this.frameExposureElapsed = [];
this.frameExposureReported = [];
this.frameClose = [];
this.frameTotal = [];
}
add(load, open, exposureElapsed, exposureReported, close, total) {
this.frameLoad.push(load);
this.frameOpen.push(open);
this.frameExposureElapsed.push(exposureElapsed);
this.frameExposureReported.push(exposureReported);
this.frameClose.push(close);
this.frameTotal.push(total);
}
average(arr) {
return arr.reduce((a, b) => a + b) / arr.length;
}
elapsed() {
return this.frameTotal.reduce((a, b) => a + b);
}
estimate(frame, avg) {
const frames = this.frames - frame;
return frames * avg;
}
margin(target, arr) {
const min = Math.min(...arr);
const max = Math.max(...arr);
const diff = Math.abs(max - min);
return (diff / target) / 2;
}
calculate(frame) {
if (this.frameTotal.length === 0) {
return null;
}
const totalFrameAvg = this.average(this.frameTotal);
const openAvg = this.average(this.frameOpen);
const closeAvg = this.average(this.frameClose);
const exposureAvg = this.average(this.frameExposureReported);
const loadAvg = this.average(this.frameLoad);
return {
totalFrameLast: this.frameTotal[this.frameTotal.length - 1],
totalFrameAvg,
totalFrameMargin: this.margin(totalFrameAvg, this.frameTotal) * 100.0,
fps: 1000.0 / totalFrameAvg,
loadLast: this.frameLoad[this.frameLoad.length - 1],
loadAvg,
loadMargin: this.margin(loadAvg, this.frameLoad),
openLast: this.frameOpen[this.frameOpen.length - 1],
openAvg,
openMargin: this.margin(openAvg, this.frameOpen) * 100.0,
closeLast: this.frameClose[this.frameClose.length - 1],
closeAvg,
closeMargin: this.margin(closeAvg, this.frameClose) * 100.0,
exposureLast: this.frameExposureReported[this.frameExposureReported.length - 1],
exposureAvg,
exposureMargin: this.margin(exposureAvg, this.frameExposureReported) * 100.0,
elapsed: this.elapsed(),
estimate: this.estimate(frame, totalFrameAvg)
};
}
}
class Sequence { class Sequence {
constructor(camera, fd, display, ffprobe, send) { constructor(camera, fd, display, ffprobe, send) {
this.current = null; this.current = null;
this.info = null; this.info = null;
this.images = []; this.images = [];
this.stats = null;
this.running = false; this.running = false;
this.paused = false; this.paused = false;
this.progress = 0; this.progress = 0;
@ -31,6 +98,7 @@ class Sequence {
if (this.current !== null) { if (this.current !== null) {
this.running = true; this.running = true;
this.log.info(`Started sequence: ${this.current.name}`); this.log.info(`Started sequence: ${this.current.name}`);
this.stats = new Statistics(this.exposure, this.frames);
this.run(); this.run();
} }
} }
@ -59,6 +127,7 @@ class Sequence {
} }
//complete running //complete running
this.updateClientsOnState(); this.updateClientsOnState();
this.stats = null;
} }
async load(seq) { async load(seq) {
this.current = seq; this.current = seq;
@ -131,7 +200,8 @@ class Sequence {
frames: this.frames, frames: this.frames,
status: this.getStatus() status: this.getStatus()
}, },
exposure: this.exposure exposure: this.exposure,
statistics: this.stats !== null ? this.stats.calculate(this.frame) : null
}; };
} }
getUpdateState() { getUpdateState() {
@ -209,13 +279,28 @@ class Sequence {
this.updateClientsOnState(); this.updateClientsOnState();
} }
async frameRecord() { async frameRecord() {
const start = Date.now();
let load;
let open;
let exposureElapsed;
let exposureReported;
let close;
let total;
let result;
const img = this.images[this.frame]; const img = this.images[this.frame];
const dimensions = this.display.getDimensions(); const dimensions = this.display.getDimensions();
this.log.info(`Frame: ${this.frame} / ${this.images.length}`); this.log.info(`Frame: ${this.frame} / ${this.images.length}`);
await this.fd.load(img.path, dimensions.x, dimensions.y, dimensions.w, dimensions.h); await this.fd.load(img.path, dimensions.x, dimensions.y, dimensions.w, dimensions.h);
load = Date.now() - start;
await this.camera.open(); await this.camera.open();
await this.fd.display(img.path, [this.exposure]); open = Date.now() - start - load;
result = await this.fd.display(img.path, [this.exposure]);
exposureReported = result.reported;
exposureElapsed = Date.now() - start - load - open;
await this.camera.close(); await this.camera.close();
close = Date.now() - start - load - open - exposureElapsed;
total = Date.now() - start;
this.stats.add(load, open, exposureElapsed, exposureReported, close, total);
} }
} }
exports.Sequence = Sequence; exports.Sequence = Sequence;

File diff suppressed because one or more lines are too long

View File

@ -239,38 +239,38 @@ export class Camera {
} }
public async frame () : Promise<number> { public async frame () : Promise<number> {
const start : number = +new Date(); const start : number = Date.now();
let ms : number; let ms : number;
await this.confirm(Commands.CAMERA, Commands.CAMERA); await this.confirm(Commands.CAMERA, Commands.CAMERA);
ms = (+new Date()) - start; ms = (Date.now()) - start;
this.log.info(this.prefix + `frame() - ${ms}ms`); this.log.info(this.prefix + `frame() - ${ms}ms`);
return ms; return ms;
} }
public async open () : Promise<number> { public async open () : Promise<number> {
const start : number = +new Date(); const start : number = Date.now();
let ms : number; let ms : number;
await this.confirm(Commands.CAMERA_OPEN, Commands.CAMERA_OPEN); await this.confirm(Commands.CAMERA_OPEN, Commands.CAMERA_OPEN);
ms = (+new Date()) - start; ms = (Date.now()) - start;
this.log.info(this.prefix + `open() - ${ms}ms`); this.log.info(this.prefix + `open() - ${ms}ms`);
return ms; return ms;
} }
public async close () : Promise<number> { public async close () : Promise<number> {
const start : number = +new Date(); const start : number = Date.now();
let ms : number; let ms : number;
await this.confirm(Commands.CAMERA_CLOSE, Commands.CAMERA_CLOSE); await this.confirm(Commands.CAMERA_CLOSE, Commands.CAMERA_CLOSE);
ms = (+new Date()) - start; ms = (Date.now()) - start;
this.log.info(this.prefix + `close() - ${ms}ms`); this.log.info(this.prefix + `close() - ${ms}ms`);
return ms; return ms;
} }
public async direction (dir : boolean) : Promise<number> { public async direction (dir : boolean) : Promise<number> {
const start : number = +new Date(); const start : number = Date.now();
let ms : number; let ms : number;
const cmd : string = dir ? Commands.CAMERA_FORWARD : Commands.CAMERA_BACKWARD; const cmd : string = dir ? Commands.CAMERA_FORWARD : Commands.CAMERA_BACKWARD;
await this.confirm(cmd, cmd); await this.confirm(cmd, cmd);
ms = (+new Date()) - start; ms = (Date.now()) - start;
this.log.info(this.prefix + `direction(${dir}) - ${ms}ms`); this.log.info(this.prefix + `direction(${dir}) - ${ms}ms`);
return ms; return ms;
} }

View File

@ -38,6 +38,7 @@ interface fdOutgoingMessage {
interface fdIncomingMessage { interface fdIncomingMessage {
action : Action, action : Action,
data? : number,
success : boolean, success : boolean,
error? : string error? : string
} }
@ -45,7 +46,8 @@ interface fdIncomingMessage {
interface fdResult { interface fdResult {
action : Action, action : Action,
image : string, image : string,
time : number reported? :number,
elapsed? : number
} }
export class FD { export class FD {
@ -166,12 +168,13 @@ export class FD {
h h
} }
}; };
const startTime : number = +new Date(); const startTime : number = Date.now();
if (this.mock) { if (this.mock) {
return { return {
action : Action.LOAD, action : Action.LOAD,
image, image,
time : (+new Date()) - startTime reported : (Date.now()) - startTime,
elapsed : (Date.now()) - startTime
}; };
} }
const promise : Promise<fdResult> = new Promise(function (resolve : Function, reject : Function) { const promise : Promise<fdResult> = new Promise(function (resolve : Function, reject : Function) {
@ -180,7 +183,8 @@ export class FD {
return resolve({ return resolve({
action : Action.LOAD, action : Action.LOAD,
image, image,
time : (+new Date()) - startTime reported : msg.data,
elapsed : (Date.now()) - startTime
}); });
} else if (typeof msg.error !== 'undefined') { } else if (typeof msg.error !== 'undefined') {
return reject(new Error(msg.error)); return reject(new Error(msg.error));
@ -198,7 +202,7 @@ export class FD {
image, image,
exposure exposure
}; };
const startTime : number = +new Date(); const startTime : number = Date.now();
if (this.mock) { if (this.mock) {
for (let exp of exposure) { for (let exp of exposure) {
await delay(exp); await delay(exp);
@ -206,7 +210,8 @@ export class FD {
return { return {
action : Action.DISPLAY, action : Action.DISPLAY,
image, image,
time : (+new Date()) - startTime reported : (Date.now()) - startTime,
elapsed : (Date.now()) - startTime
}; };
} }
const promise : Promise<fdResult> = new Promise(function (resolve : Function, reject : Function) { const promise : Promise<fdResult> = new Promise(function (resolve : Function, reject : Function) {
@ -215,7 +220,8 @@ export class FD {
return resolve({ return resolve({
action : Action.DISPLAY, action : Action.DISPLAY,
image, image,
time : (+new Date()) - startTime reported : msg.data,
elapsed : (Date.now()) - startTime
}); });
} else if (typeof msg.error !== 'undefined') { } else if (typeof msg.error !== 'undefined') {
return reject(new Error(msg.error)); return reject(new Error(msg.error));
@ -232,21 +238,24 @@ export class FD {
action : Action.STOP, action : Action.STOP,
image image
}; };
const startTime : number = +new Date(); const startTime : number = Date.now();
if (this.mock) { if (this.mock) {
return { return {
action : Action.STOP, action : Action.STOP,
image, image,
time : (+new Date()) - startTime reported : (Date.now()) - startTime,
elapsed : (Date.now()) - startTime
}; };
} }
const promise : Promise<fdResult> = new Promise(function (resolve : Function, reject : Function) { const promise : Promise<fdResult> = new Promise(function (resolve : Function, reject : Function) {
this.waiting = function (msg : fdIncomingMessage) { this.waiting = function (msg : fdIncomingMessage) {
if (msg.action == Action.STOP && msg.success) { if (msg.action == Action.STOP && msg.success) {
this.log.info(msg.data);
return resolve({ return resolve({
action : Action.STOP, action : Action.STOP,
image, image,
time : (+new Date()) - startTime reported : msg.data,
elapsed : (Date.now()) - startTime
}); });
} else if (typeof msg.error !== 'undefined') { } else if (typeof msg.error !== 'undefined') {
return reject(new Error(msg.error)); return reject(new Error(msg.error));

22
src/globals.d.ts vendored
View File

@ -19,7 +19,29 @@ interface SequenceState {
} }
interface SequenceStatistics { interface SequenceStatistics {
totalFrameLast? : number,
totalFrameAvg? : number,
totalFrameMargin? : number,
fps? : number,
loadAvg? : number,
loadLast? : number,
loadMargin? : number,
openLast? : number,
openAvg? : number,
openMargin? : number,
closeLast? : number,
closeAvg? : number,
closeMargin? : number,
exposureLast? : number,
exposureAvg? : number,
exposureMargin? : number,
elapsed? : number,
estimate? : number
} }
interface State { interface State {

View File

@ -3,7 +3,7 @@ import { createLog } from '../log';
import { delay } from '../delay'; import { delay } from '../delay';
import type { Logger } from 'winston'; import type { Logger } from 'winston';
import type { SequenceObject, ImageObject } from '../files'; import type { SequenceObject, ImageObject } from '../files';
import type { FD, fdOutgoingPosition } from '../fd'; import type { FD, fdOutgoingPosition, fdResult } from '../fd';
import type { Camera } from '../camera'; import type { Camera } from '../camera';
import type { Display, Dimensions } from '../display'; import type { Display, Dimensions } from '../display';
import type { FFPROBE, VideoInfo } from '../ffprobe'; import type { FFPROBE, VideoInfo } from '../ffprobe';
@ -14,6 +14,97 @@ enum SequenceStatus {
PAUSED PAUSED
} }
class Statistics {
private frameLoad : number[];
private frameOpen : number[];
private frameExposureElapsed : number[];
private frameExposureReported : number[];
private frameClose : number[];
private frameTotal : number[];
private exposure : number;
private frames : number;
constructor (exposure : number, frames : number) {
this.exposure = exposure;
this.frames = frames;
this.frameLoad = [];
this.frameOpen = [];
this.frameExposureElapsed = [];
this.frameExposureReported = [];
this.frameClose = [];
this.frameTotal = [];
}
public add (load : number, open : number, exposureElapsed : number, exposureReported : number, close : number, total : number) {
this.frameLoad.push(load);
this.frameOpen.push(open);
this.frameExposureElapsed.push(exposureElapsed);
this.frameExposureReported.push(exposureReported);
this.frameClose.push(close);
this.frameTotal.push(total);
}
private average (arr : number[]) : number {
return arr.reduce((a, b) => a + b) / arr.length;
}
private elapsed () : number {
return this.frameTotal.reduce((a, b) => a + b);
}
private estimate (frame : number, avg : number) : number {
const frames : number = this.frames - frame;
return frames * avg;
}
private margin (target : number, arr : number[]) : number {
const min : number = Math.min(...arr);
const max : number = Math.max(...arr);
const diff : number = Math.abs(max - min);
return (diff / target) / 2;
}
public calculate (frame : number) : SequenceStatistics {
if (this.frameTotal.length === 0) {
return null;
}
const totalFrameAvg : number = this.average(this.frameTotal);
const openAvg : number = this.average(this.frameOpen);
const closeAvg : number = this.average(this.frameClose);
const exposureAvg : number = this.average(this.frameExposureReported);
const loadAvg : number = this.average(this.frameLoad);
return {
totalFrameLast : this.frameTotal[this.frameTotal.length - 1],
totalFrameAvg,
totalFrameMargin : this.margin(totalFrameAvg, this.frameTotal) * 100.0,
fps : 1000.0 / totalFrameAvg,
loadLast : this.frameLoad[this.frameLoad.length - 1],
loadAvg,
loadMargin : this.margin(loadAvg, this.frameLoad),
openLast : this.frameOpen[this.frameOpen.length - 1],
openAvg,
openMargin : this.margin(openAvg, this.frameOpen) * 100.0,
closeLast : this.frameClose[this.frameClose.length - 1],
closeAvg,
closeMargin : this.margin(closeAvg, this.frameClose) * 100.0,
exposureLast : this.frameExposureReported[this.frameExposureReported.length - 1],
exposureAvg,
exposureMargin : this.margin(exposureAvg, this.frameExposureReported) * 100.0,
elapsed : this.elapsed(),
estimate : this.estimate(frame, totalFrameAvg)
} as SequenceStatistics;
}
}
export class Sequence { export class Sequence {
private log : Logger; private log : Logger;
private current : SequenceObject = null; private current : SequenceObject = null;
@ -24,9 +115,11 @@ export class Sequence {
private ffprobe : FFPROBE; private ffprobe : FFPROBE;
private fd : FD; private fd : FD;
private send : Function; private send : Function;
private stats : Statistics = null;
private running : boolean = false; private running : boolean = false;
private paused : boolean = false; private paused : boolean = false;
private progress : number = 0; private progress : number = 0;
private frame : number = 0; private frame : number = 0;
private frames : number = 0; private frames : number = 0;
@ -46,6 +139,7 @@ export class Sequence {
if (this.current !== null) { if (this.current !== null) {
this.running = true; this.running = true;
this.log.info(`Started sequence: ${this.current.name}`); this.log.info(`Started sequence: ${this.current.name}`);
this.stats = new Statistics(this.exposure, this.frames);
this.run(); this.run();
} }
} }
@ -76,6 +170,7 @@ export class Sequence {
} }
//complete running //complete running
this.updateClientsOnState(); this.updateClientsOnState();
this.stats = null;
} }
public async load (seq : SequenceObject) { public async load (seq : SequenceObject) {
@ -158,7 +253,8 @@ export class Sequence {
frames : this.frames, frames : this.frames,
status : this.getStatus() as SequenceStatus status : this.getStatus() as SequenceStatus
}, },
exposure : this.exposure exposure : this.exposure,
statistics : this.stats !== null ? this.stats.calculate(this.frame) : null
} }
} }
@ -243,12 +339,27 @@ export class Sequence {
} }
private async frameRecord () { private async frameRecord () {
const start : number = Date.now();
let load : number;
let open : number;
let exposureElapsed : number;
let exposureReported : number;
let close : number;
let total : number;
let result : fdResult;
const img : ImageObject = this.images[this.frame]; const img : ImageObject = this.images[this.frame];
const dimensions : fdOutgoingPosition = this.display.getDimensions(); const dimensions : fdOutgoingPosition = this.display.getDimensions();
this.log.info(`Frame: ${this.frame} / ${this.images.length}`); this.log.info(`Frame: ${this.frame} / ${this.images.length}`);
await this.fd.load(img.path, dimensions.x, dimensions.y, dimensions.w, dimensions.h); await this.fd.load(img.path, dimensions.x, dimensions.y, dimensions.w, dimensions.h);
load = Date.now() - start;
await this.camera.open(); await this.camera.open();
await this.fd.display(img.path, [ this.exposure ] ); open = Date.now() - start - load;
result = await this.fd.display(img.path, [ this.exposure ] );
exposureReported = result.reported;
exposureElapsed = Date.now() - start - load - open;
await this.camera.close(); await this.camera.close();
close = Date.now() - start - load - open - exposureElapsed;
total = Date.now() - start;
this.stats.add(load, open, exposureElapsed, exposureReported, close, total);
} }
} }

View File

@ -55,10 +55,20 @@ html, body{
flex: 0 0 33.33333%; flex: 0 0 33.33333%;
} }
.quarter{
box-sizing: border-box;
flex: 0 0 25%;
}
.flex { .flex {
display: flex; display: flex;
} }
.edited {
background-color: yellow !important;
color: rgb(0, 0, 0) !important;
}
fieldset.inline .field-row { fieldset.inline .field-row {
display: inline-block; display: inline-block;
} }
@ -71,6 +81,14 @@ fieldset.inline input.medium {
max-width: 70px; max-width: 70px;
} }
label {
width: 80px;
text-align: right;
display: inline-block;
padding-right: 3px;
}
}
progress { progress {
width: 97vw; width: 97vw;
position: absolute; position: absolute;
@ -78,16 +96,26 @@ progress {
left: 0.5vw; left: 0.5vw;
} }
button.small{ button.small,
input.small {
width: 35px; width: 35px;
max-width: 35px; max-width: 35px;
min-width: 35px; min-width: 35px;
padding: 0 6px; padding: 0 6px;
} }
button.medium{ button.medium,
input.medium {
width: 50px; width: 50px;
max-width: 50px; max-width: 50px;
min-width: 50px; min-width: 50px;
padding: 0 6px; padding: 0 6px;
} }
button.large,
input.large {
width: 75px;
min-width: 75px;
max-width: 75px;
padding: 0 6px;
}

View File

@ -1,3 +1,4 @@
declare let client: Client;
declare enum SequenceStatus { declare enum SequenceStatus {
IDLE = 0, IDLE = 0,
RUNNING = 1, RUNNING = 1,
@ -56,6 +57,8 @@ declare class Client {
private setFrame; private setFrame;
private setExposure; private setExposure;
private setDisplay; private setDisplay;
edited(el: HTMLElement): void;
private setStatistics;
private cmd; private cmd;
disableClass(className: string): void; disableClass(className: string): void;
enableClass(className: string): void; enableClass(className: string): void;
@ -80,5 +83,8 @@ declare class Client {
exitFullscreen(): void; exitFullscreen(): void;
private active; private active;
private inactive; private inactive;
addClass(id: string, className: string): void;
removeClass(id: string, className: string): void;
set(id: string, value: string): void;
get(id: string): string;
} }
declare const client: Client;

View File

@ -1,3 +1,4 @@
let client;
var SequenceStatus; var SequenceStatus;
(function (SequenceStatus) { (function (SequenceStatus) {
SequenceStatus[SequenceStatus["IDLE"] = 0] = "IDLE"; SequenceStatus[SequenceStatus["IDLE"] = 0] = "IDLE";
@ -127,6 +128,7 @@ class Client {
document.getElementById('sequenceCtrlForm').reset(); document.getElementById('sequenceCtrlForm').reset();
document.getElementById('manualCtrlForm').reset(); document.getElementById('manualCtrlForm').reset();
document.getElementById('exposureCtrlForm').reset(); document.getElementById('exposureCtrlForm').reset();
document.getElementById('statisticsForm').reset();
this.disableClass('sequenceCtrl'); this.disableClass('sequenceCtrl');
this.disableClass('manualCtrl'); this.disableClass('manualCtrl');
this.disableClass('exposureCtrl'); this.disableClass('exposureCtrl');
@ -159,13 +161,15 @@ class Client {
this.setStatus(state.sequence); this.setStatus(state.sequence);
this.setExposure(state); this.setExposure(state);
this.setDisplay(state); this.setDisplay(state);
document.getElementById('sequence').value = state.sequence.hash; this.set('sequence', state.sequence.hash);
this.removeClass('sequence', 'edited');
} }
setUpdate(state) { setUpdate(state) {
this.setProgress(state.sequence); this.setProgress(state.sequence);
this.setFrame(state.sequence); this.setFrame(state.sequence);
this.setStatus(state.sequence); this.setStatus(state.sequence);
this.setExposure(state); this.setExposure(state);
this.setStatistics(state.statistics);
this.display.updateImage(); this.display.updateImage();
} }
setStatus(sequence) { setStatus(sequence) {
@ -190,37 +194,54 @@ class Client {
} }
setProgress(sequence) { setProgress(sequence) {
const percent = sequence.progress * 100.0; const percent = sequence.progress * 100.0;
if (this.progress !== null) {
this.progress.value = sequence.progress; this.progress.value = sequence.progress;
this.progressText.innerText = `Progress: ${Math.floor(percent)}%`; this.progressText.innerText = `Progress: ${Math.floor(percent)}%`;
} }
}
setFrame(sequence) { setFrame(sequence) {
if (typeof sequence.current !== 'undefined') { if (typeof sequence.current !== 'undefined') {
document.getElementById('frame').value = `${sequence.current}`.padStart(5, '0'); this.set('frame', `${sequence.current}`.padStart(5, '0'));
this.removeClass('frame', 'edited');
} }
} }
setExposure(state) { setExposure(state) {
if (typeof state.exposure !== 'undefined') { if (typeof state.exposure !== 'undefined') {
const el = document.getElementById('exposure');
this.enableClass('exposureCtrl'); this.enableClass('exposureCtrl');
document.getElementById('exposure').value = `${state.exposure}`; this.set('exposure', `${state.exposure}`);
this.removeClass('exposure', 'edited');
} }
} }
setDisplay(state) { setDisplay(state) {
const widthEl = document.getElementById('displayWidth');
const heightEl = document.getElementById('displayHeight');
const srcWidthEl = document.getElementById('sourceWidth');
const srcHeightEl = document.getElementById('sourceHeight');
if (typeof state.display !== 'undefined') { if (typeof state.display !== 'undefined') {
widthEl.value = state.display.width; this.set('displayWidth', state.display.width.toString());
heightEl.value = state.display.height; this.set('displayHeight', state.display.height.toString());
srcWidthEl.value = state.source.width; this.set('sourceWidth', state.source.width.toString());
srcHeightEl.value = state.source.height; this.set('sourceHeight', state.source.height.toString());
widthEl.readOnly = false;
heightEl.readOnly = false;
this.display.set(state); this.display.set(state);
} }
} }
edited(el) {
el.classList.add('edited');
}
setStatistics(stats) {
if (stats !== null) {
this.set('statsFrameTotalAvg', stats.totalFrameAvg.toString());
this.set('statsFrameTotalLast', stats.totalFrameLast.toString());
this.set('statsFrameTotalMargin', stats.totalFrameMargin.toString());
this.set('statsFPS', stats.fps.toString());
this.set('statsFrameLoadAvg', stats.loadAvg.toString());
this.set('statsFrameLoadMargin', stats.loadMargin.toString());
this.set('statsFrameOpenLast', stats.openLast.toString());
this.set('statsFrameOpenAvg', stats.openAvg.toString());
this.set('statsFrameOpenMargin', stats.openMargin.toString());
this.set('statsFrameCloseLast', stats.closeLast.toString());
this.set('statsFrameCloseAvg', stats.closeAvg.toString());
this.set('statsFrameCloseMargin', stats.closeMargin.toString());
this.set('statsExposureLast', stats.exposureLast.toString());
this.set('statsExposureAvg', stats.exposureAvg.toString());
this.set('statsExposureMargin', stats.exposureMargin.toString());
}
}
cmd(msg) { cmd(msg) {
switch (msg.cmd) { switch (msg.cmd) {
case 'ping': case 'ping':
@ -318,12 +339,10 @@ class Client {
return; return;
} }
msg = { cmd: 'select', state: { sequence: { hash } } }; msg = { cmd: 'select', state: { sequence: { hash } } };
console.log('send select'); console.log(`send select ${hash}`);
console.log(hash);
this.client.send(JSON.stringify(msg)); this.client.send(JSON.stringify(msg));
} }
receiveSelect(msg) { receiveSelect(msg) {
console.log('got select');
this.enableClass('sequenceCtrl'); this.enableClass('sequenceCtrl');
this.setSequence(msg.state); this.setSequence(msg.state);
} }
@ -334,7 +353,7 @@ class Client {
this.client.send(JSON.stringify({ cmd: 'stop' })); this.client.send(JSON.stringify({ cmd: 'stop' }));
} }
sendExposure() { sendExposure() {
const exposure = parseInt(document.getElementById('exposure').value); const exposure = parseInt(this.get('exposure'));
this.client.send(JSON.stringify({ cmd: 'exposure', state: { exposure } })); this.client.send(JSON.stringify({ cmd: 'exposure', state: { exposure } }));
} }
receiveUpdate(msg) { receiveUpdate(msg) {
@ -354,11 +373,28 @@ class Client {
} }
} }
active() { active() {
document.getElementById('overlay').classList.add('active'); this.addClass('overlay', 'active');
} }
inactive() { inactive() {
document.getElementById('overlay').classList.remove('active'); this.removeClass('overlay', 'active');
}
addClass(id, className) {
document.getElementById(id).classList.add(className);
}
removeClass(id, className) {
document.getElementById(id).classList.remove(className);
}
set(id, value) {
try {
document.getElementById(id).value = value;
}
catch (err) {
console.warn(`Element ${id} does not exist or cannot be set`);
}
}
get(id) {
return document.getElementById(id).value;
} }
} }
const client = new Client(); client = new Client();
//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@ -68,7 +68,7 @@
<fieldset id="sequenceSelect" class="inline half"> <fieldset id="sequenceSelect" class="inline half">
<legend>Sequence</legend> <legend>Sequence</legend>
<form id="sequenceSelectForm" onsubmit="return false;"> <form id="sequenceSelectForm" onsubmit="return false;">
<select name="sequence" id="sequence"> <select name="sequence" id="sequence" oninput="client.edited(this);">
<option> - Select Image Sequence - </option> <option> - Select Image Sequence - </option>
{{#each sequences}} {{#each sequences}}
<option value="{{this.hash}}">{{this.name}}</option> <option value="{{this.hash}}">{{this.name}}</option>
@ -80,41 +80,30 @@
<fieldset id="exposureCtrl" class="inline half"> <fieldset id="exposureCtrl" class="inline half">
<legend>Exposure</legend> <legend>Exposure</legend>
<form id="exposureCtrlForm" onsubmit="return false;"> <form id="exposureCtrlForm" onsubmit="return false;">
<input id="exposure" class="medium exposureCtrl" type="number" value="0" disabled /> <input id="exposure" class="large exposureCtrl" type="number" value="0" oninput="client.edited(this);" disabled />
<span>ms </span> <span>ms </span>
<button id="exposureUpdate" class="exposureCtrl" onclick="client.sendExposure();">Update</button> <button id="exposureUpdate" class="exposureCtrl" onclick="client.sendExposure();">Update</button>
</form> </form>
</fieldset> </fieldset>
</div> </div>
<div class="flex"> <div class="flex">
<fieldset class="inline half" id="sequenceCtrl"> <div class="half">
<div>
<fieldset id="sequenceCtrl">
<legend>Sequence Controls</legend> <legend>Sequence Controls</legend>
<form id="sequenceCtrlForm" onsubmit="return false;"> <form id="sequenceCtrlForm" onsubmit="return false;">
<button id="start" class="medium sequenceCtrl" onclick="client.sendStart();" disabled>Start</button> <button id="start" class="medium sequenceCtrl" onclick="client.sendStart();" disabled>Start</button>
<button id="stop" class="medium sequenceCtrl" onclick="client.sendStop();" disabled>Stop</button> <button id="stop" class="medium sequenceCtrl" onclick="client.sendStop();" disabled>Stop</button>
<button id="rewind" class="small sequenceCtrl" onclick="client.sendToStart();" disabled><<</button> <button id="rewind" class="small sequenceCtrl" onclick="client.sendToStart();" disabled><<</button>
<button id="rewind" class="small sequenceCtrl" onclick="client.sendRewind();" disabled><</button> <button id="rewind" class="small sequenceCtrl" onclick="client.sendRewind();" disabled><</button>
<input id="frame" type="number" value="00000" class="sequenceCtrl medium" onchange="client.sendFrameSet();" disabled /> <input id="frame" type="number" value="00000" class="sequenceCtrl large" oninput="client.edited(this);" onchange="client.sendFrameSet();" disabled />
<button id="advance" class="small sequenceCtrl" onclick="client.sendAdvance()" disabled>></button> <button id="advance" class="small sequenceCtrl" onclick="client.sendAdvance()" disabled>></button>
<button id="advance" class="small sequenceCtrl" onclick="client.sendToEnd()" disabled>>></button> <button id="advance" class="small sequenceCtrl" onclick="client.sendToEnd()" disabled>>></button>
</form> </form>
</fieldset> </fieldset>
<fieldset id="displayAdjust" class="inline half">
<legend>Display Adjust</legend>
<form id="displayAdjustForm" onsubmit="return false;">
<button class="small" id="offsetXPlus" disabled>X +</button>
<button class="small" id="offsetXMinus" disabled>X -</button>
<button class="small" id="offsetYPlus" disabled>Y +</button>
<button class="small" id="offsetYMinus" disabled>Y -</button>
<button class="small" id="widthPlus" disabled>W +</button>
<button class="small" id="widthMinus" disabled>W -</button>
<button class="small" id="heightPlus" disabled>H +</button>
<button class="small" id="heightMinus" disabled>H -</button>
</form>
</fieldset>
</div> </div>
<div> <div>
<fieldset id="manualCtrl" class="inline half"> <fieldset id="manualCtrl">
<legend>Manual Controls</legend> <legend>Manual Controls</legend>
<form id="manualCtrlForm" onsubmit="return false;" > <form id="manualCtrlForm" onsubmit="return false;" >
<button id="open" class="manualCtrl" onclick="client.sendCameraOpen()">Open</button> <button id="open" class="manualCtrl" onclick="client.sendCameraOpen()">Open</button>
@ -124,6 +113,77 @@
</form> </form>
</fieldset> </fieldset>
</div> </div>
</div>
<div class="half">
<fieldset id="displayAdjust" class="inline half">
<legend>Display Adjust</legend>
<form id="displayAdjustForm" onsubmit="return false;">
<button class="small manualCtrl" id="offsetXPlus" disabled>X +</button>
<button class="small manualCtrl" id="offsetXMinus" disabled>X -</button>
<button class="small manualCtrl" id="offsetYPlus" disabled>Y +</button>
<button class="small manualCtrl" id="offsetYMinus" disabled>Y -</button>
<button class="small manualCtrl" id="widthPlus" disabled>W +</button>
<button class="small manualCtrl" id="widthMinus" disabled>W -</button>
<button class="small manualCtrl" id="heightPlus" disabled>H +</button>
<button class="small manualCtrl" id="heightMinus" disabled>H -</button>
</form>
</fieldset>
</div>
</div>
<div>
<fieldset id="statistics" class="inline">
<legend>Statistics</legend>
<form id="statisticsForm" onsubmit="return false;" class="flex">
<div id="statisticsFrame" class="quarter">
<div>
<label for="statsFrameTotalAvg">Frame Total Avg</label><input id="statsFrameTotalAvg" class="large" type="text" readonly value="0" /><span> ms +/-</span>
<input id="statsFrameTotalMargin" class="small" type="text" readonly value="0"/><span>%</span>
</div>
<div>
<label for="statsFrameTotalLast">Frame Total Last</label><input id="statsFrameTotalLast" class="large" type="text" readonly value="0" /><span> ms</span>
</div>
<div>
<label for="statsFPS">FPS</label><input id="statsFPS" class="large" type="text" readonly value="0" />
</div>
<div>
<label for="statsFrameLoadAvg">Frame Load Avg</label><input id="statsFrameLoadAvg" class="large" type="text" readonly value="0" /><span> ms +/-</span>
<input id="statsFrameLoadMargin" class="small" type="text" readonly value="0"/><span>%</span>
</div>
</div>
<div id="statisticsOpenClose" class="quarter">
<div>
<label for="statsFrameOpenAvg">Cam Open Avg</label><input id="statsFrameOpenAvg" class="large" type="text" readonly value="0" /><span> ms +/-</span>
<input id="statsFrameOpenMargin" class="small" type="text" readonly value="0"/><span>%</span>
</div>
<div>
<label for="statsFrameOpenLast">Frame Open Last</label><input id="statsFrameOpenLast" class="large" type="text" readonly value="0" /><span> ms</span>
</div>
<div>
<label for="statsFrameCloseAvg">Cam Close Avg</label><input id="statsFrameCloseAvg" class="large" type="text" readonly value="0" /><span> ms +/-</span>
<input id="statsFrameCloseMargin" class="small" type="text" readonly value="0"/><span>%</span>
</div>
<div>
<label for="statsFrameCloseLast">Frame Close Last</label><input id="statsFrameCloseLast" class="large" type="text" readonly value="0" /><span> ms</span>
</div>
</div>
<div id="statisticsExposure" class="quarter">
<div>
<label for="statsExposureAvg">Exposure Avg</label><input id="statsExposureAvg" class="large" type="text" readonly value="0" /><span> ms +/-</span>
<input id="statsExposureMargin" class="small" type="text" readonly value="0"/><span>%</span>
</div>
<div>
<label for="statsExposureLast">Exposure Last</label><input id="statsExposureLast" class="large" type="text" readonly value="0" /><span> ms</span>
</div>
<div>
<label for="statsExposureLast">Equivalent</label><input id="statsExposureEquivalent" class="large" type="text" readonly value="1s" /><span> ms</span>
</div>
</div>
<div id="statisticsTiming" class="quarter">
</div>
</form>
</fieldset>
</div>
<div> <div>
<progress id="progress"></progress> <progress id="progress"></progress>
</div> </div>