Fixed frame counting optimization, resolving issue #17. Also added a Siemens Star focusing screen, a middle gray metering screen and a field guide screen for use with the filmout feature. TODO: actually change monitors when selected.

This commit is contained in:
mmcwilliams 2019-06-25 12:13:15 -04:00
parent 872b46d4a5
commit f239f862e8
21 changed files with 324 additions and 78 deletions

View File

@ -739,7 +739,7 @@ button:focus {
background: black;
color: #fff;
}
#video input[type=text],
#video input,
#video select {
display: block;
border-radius: 5px;
@ -755,25 +755,25 @@ button:focus {
font-size: 21px;
min-width: 300px;
}
#video input[type=text] span,
#video input span,
#video select span {
display: block;
font-size: 16px;
font-weight: 200;
}
#video input[type=text]:active,
#video input:active,
#video select:active,
#video input[type=text] .active,
#video input .active,
#video select .active {
background: #fff;
color: #272b30;
outline: none;
}
#video input[type=text]:focus,
#video input:focus,
#video select:focus {
outline: none;
}
#video input[type=text].active,
#video input.active,
#video select.active {
border-color: #DAE035;
color: #DAE035;
@ -805,6 +805,7 @@ button:focus {
box-sizing: border-box;
height: 360px;
margin: 0 auto;
position: relative;
}
#filmout_monitor.on {
display: block;
@ -815,10 +816,14 @@ button:focus {
height: 360px;
}
#filmout {
height: 360px;
min-height: 360px;
width: auto;
margin: 0 auto;
position: absolute;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
opacity: 0;
}

View File

@ -57,30 +57,75 @@
}
img.style.backgroundImage = `url('${src}')`;
//img.onload = () => {
//document.body.appendChild(img)
//return resolve(img)
//}
//img.onerror = reject
//img.src = src
})
});
}
async function onMeter () {
console.log('meter')
const body = document.querySelector('body')
if (!body.classList.contains('meter')) {
body.classList.add('meter')
}
}
async function onField () {
async function onFocus () {
console.log('focus')
const can = document.getElementById('can')
const ctx = can.getContext('2d')
const dpr = window.devicePixelRatio || 1
let ctx;
if (!can.classList.contains('show')) {
can.classList.add('show')
}
can.width = window.innerWidth
can.height = window.innerHeight
can.width = window.innerWidth * dpr
can.height = window.innerHeight * dpr
can.style.width = `${window.innerWidth}px`
can.style.height = `${window.innerHeight}px`
ctx = can.getContext('2d')
ctx.scale(dpr, dpr)
try{
await drawFocus(can, ctx)
} catch (err) {
alert(JSON.stringify(err))
}
}
async function drawFocus (can, ctx) {
const count = 20
const half = Math.round(count / 2)
const dpr = window.devicePixelRatio || 1
const w = can.width / dpr
const h = can.height / dpr
const opp = (Math.tan(360 / count) * ((h / 2) + h - w) / 1.5)
console.log(opp)
for (let i = 0; i < count; i++) {
ctx.beginPath()
ctx.moveTo(w / 2, h / 2)
ctx.lineTo((w / 2) + opp, h - w)
ctx.lineTo((w / 2) - opp, h - w)
ctx.fill()
ctx.translate(w / 2, h / 2);
ctx.rotate((360 / count) * Math.PI / 180)
ctx.translate(- w / 2, -h / 2)
}
}
async function onField () {
console.log('field guide')
const can = document.getElementById('can')
const dpr = window.devicePixelRatio || 1
let ctx;
if (!can.classList.contains('show')) {
can.classList.add('show')
}
can.width = window.innerWidth * dpr
can.height = window.innerHeight * dpr
can.style.width = `${window.innerWidth}px`
can.style.height = `${window.innerHeight}px`
ctx = can.getContext('2d')
ctx.scale(dpr, dpr)
try{
await drawField(can, ctx)
} catch (err) {
@ -91,10 +136,13 @@
async function drawField (can, ctx) {
const count = 20
const half = Math.round(count / 2)
const w = can.width
const h = can.height
const dpr = window.devicePixelRatio || 1
const w = can.width / dpr
const h = can.height / dpr
const wsec = w / count
const hsec = h / count
const spacer = 12
const fontSize = 18;
ctx.moveTo(w / 2, 0)
ctx.lineTo(w / 2, h)
@ -111,34 +159,21 @@
ctx.lineTo(w - (wsec * i), hsec * i)
ctx.stroke()
}
ctx.font = '30px Arial'
ctx.font = `${fontSize}px Arial`
for (let i = 0; i < half; i++) {
ctx.fillText(`${(half - i)}`, (wsec * i) + 15, (h / 2) - 15)
ctx.fillText(`${(half - i)}`, (w - (wsec * i)) - 25, (h / 2) - 15)
ctx.fillText(`${(half - i)}`, (w / 2) + 15, (hsec * i) + 25 )
ctx.fillText(`${(half - i)}`, (w / 2) + 15, (h - (hsec * i)) - 15)
ctx.fillText(`${(half - i)}`, (wsec * i) + spacer, (h / 2) - spacer)
//ctx.fillText(`${(half - i)}`, (w - (wsec * i)) - spacer, (h / 2) - spacer)
ctx.fillText(`${(half - i)}`, (w / 2) + spacer, (hsec * i) + spacer + (fontSize / 2) )
//ctx.fillText(`${(half - i)}`, (w / 2) + spacer, (h - (hsec * i)) - spacer)
}
}
async function onDigital (event, arg) {
console.log('called')
if (arg.src) {
try {
await setImage(arg.src)
} catch (err) {
console.error(err)
}
} else if (arg.meter) {
try {
await setMeter()
} catch (err) {
console.error(err)
}
} else if (arg.grid) {
try {
await setGrid()
} catch (err) {
console.error(err)
}
}
return event.returnValue = true
}
@ -160,6 +195,7 @@
ipcRenderer.on('display', onDigital)
ipcRenderer.on('field', onField)
ipcRenderer.on('meter', onMeter)
ipcRenderer.on('focus', onFocus)
document.onkeydown = onEscape
</script>
</body>

View File

@ -353,7 +353,7 @@
<div id="filmout_preview_wrap">
<div id="filmout_monitor">
<img src="#" id="filmout">
<div id="filmout"></div>
</div>
</div>
<div id="filmout_stats">
@ -372,17 +372,17 @@
</div>
<div id="filmout_position_wrap">
<div>
<button id="filmout_rewind" title="Rewind 1 Frame"><</button>
<input id="filmout_position" class="count" type="text" value="00000"/>
<button id="filmout_advance" title="Advance 1 Frame">></button>
<button id="filmout_rewind" title="Rewind 1 Frame" onclick="filmout.rewind();"><</button>
<input id="filmout_position" class="count" type="number" value="00000" onchange="gui.counterFormat(this, this.value);" />
<button id="filmout_advance" title="Advance 1 Frame" onclick="filmout.advance();">></button>
</div>
</div>
<div id="filmout_functions">
<div>
<button id="filmout_preview">PREVIEW</button>
<button id="filmout_meter">METER</button>
<button id="filmout_focus">FOCUS</button>
<button id="filmout_field">FIELD GUIDE</button>
<button id="filmout_meter" onclick="filmout.meter();">METER</button>
<button id="filmout_focus" onclick="filmout.focus();">FOCUS</button>
<button id="filmout_field" onclick="filmout.field();">FIELD GUIDE</button>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
#video{
input[type=text],select{
input,select{
.button();
display: inline-block;
padding: 6px 12px;
@ -41,6 +41,7 @@
box-sizing: border-box;
height: 360px;
margin: 0 auto;
position: relative;
&.on{
display: block;
}
@ -51,10 +52,14 @@
height: 360px;
}
#filmout {
height: 360px;
min-height: 360px;
width: auto;
margin: 0 auto;
position: absolute;
background-repeat:no-repeat;
background-size: contain;
background-position: center;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
opacity: 0;
&.on {

View File

@ -60,7 +60,7 @@ class WebView {
console.error(err);
}
this.showing = true;
await delay_1.delay(100);
await delay_1.delay(200);
return true;
}
async focus() {
@ -68,6 +68,7 @@ class WebView {
console.warn(`Cannot show focus screen because window does not exist`);
return false;
}
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('focus', { focus: true });
}
@ -80,7 +81,7 @@ class WebView {
console.warn(`Cannot show field guide because window does not exist`);
return false;
}
//aspect ratio
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('field', { field: true });
}
@ -93,6 +94,7 @@ class WebView {
console.warn(`Cannot show meter screen because window does not exist`);
return false;
}
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('meter', { meter: true });
}

File diff suppressed because one or more lines are too long

View File

@ -41,6 +41,8 @@ class FilmOut {
this.ipc.on('field', this.field.bind(this));
this.ipc.on('meter', this.meter.bind(this));
this.ipc.on('filmout_close', this.close.bind(this));
//preview
this.ipc.on('preview_frame', this.previewFrame.bind(this));
}
/**
*
@ -127,6 +129,21 @@ class FilmOut {
this.state.enabled = true;
return await this.ui.send(this.id, { valid: true, state: JSON.stringify(this.state) });
}
async previewFrame(evt, arg) {
const state = JSON.parse(JSON.stringify(this.state));
let path;
state.frame = arg.frame;
try {
path = await this.ffmpeg.frame(state, { color: [255, 255, 255] });
}
catch (err) {
console.error(err);
throw err;
}
this.ui.send('preview_frame', { path, frame: arg.frame });
}
async preview(evt, arg) {
}
async focus(evt, arg) {
try {
await this.display.open();

File diff suppressed because one or more lines are too long

View File

@ -35,6 +35,9 @@ class FilmOut {
constructor() {
this.id = 'filmout';
this.displays = [];
this.state = {
frame: 0
};
}
init() {
this.listen();
@ -42,6 +45,7 @@ class FilmOut {
listen() {
ipcRenderer.on(this.id, this.onFilmout.bind(this));
ipcRenderer.on('system', this.onSystem.bind(this));
ipcRenderer.on('preview_frame', this.onFrame.bind(this));
}
onSystem(evt, args) {
let option;
@ -58,6 +62,7 @@ class FilmOut {
if (args.displays.length > 1) {
$('#filmout_displays').on('change', this.onChange.bind(this));
}
$('#filmout_position').on('change', this.previewFrame.bind(this));
}
onChange() {
const val = $('#filmout_displays').val();
@ -89,6 +94,7 @@ class FilmOut {
elem.addClass('on');
$('#filmout_stats_monitor_size').text(`${display.width} x ${display.height}`);
$('#filmout_stats_monitor_aspect').text(`${aspect}`);
$('#filmout_stats_monitor_scale').text(`${parseFloat(display.scale).toFixed(1)} scale factor`);
console.dir(display);
}
selectFile() {
@ -156,8 +162,8 @@ class FilmOut {
if (args.valid && args.valid === true) {
//success state
state = JSON.parse(args.state);
console.dir(args);
console.dir(state);
//console.dir(args)
//console.dir(state)
$('#digital').addClass('active');
$('#projector_type_digital').prop('checked', 'checked');
gui.notify('DEVICES', `Using video ${state.fileName}`);
@ -169,17 +175,61 @@ class FilmOut {
if (light.disabled) {
light.enable();
}
this.state.frame = 0;
this.state.frames = state.frames;
this.state.width = state.info.width;
this.state.height = state.info.height;
this.state.name = state.fileName;
$('#seq_loop').val(`${state.frames - 1}`).trigger('change');
$('#filmout_stats_video_name').text(state.fileName);
$('#filmout_stats_video_size').text(`${state.info.width} x ${state.info.height}`);
$('#filmout_stats_video_frames').text(`${state.frames} frames`);
gui.updateState();
this.previewFrame();
}
else {
$('#projector_type_digital').prop('checked', 'checked');
$('#digital').removeClass('active');
}
}
previewFrame() {
const frameStr = $('#filmout_position').val();
const frame = parseInt(frameStr, 10);
this.state.frame = frame;
ipcRenderer.send('preview_frame', { frame });
}
onFrame(evt, args) {
const elem = $('#filmout');
elem[0].style.backgroundImage = `url('${args.path}')`;
elem.addClass('on');
}
advance() {
this.state.frame++;
if (this.state.frame >= this.state.frames) {
this.state.frame = 0;
}
$('#filmout_position').val(this.state.frame).trigger('change');
}
rewind() {
this.state.frame--;
if (this.state.frame < 0) {
this.state.frame = this.state.frames - 1;
}
$('#filmout_position').val(this.state.frame).trigger('change');
}
preview(evt, arg) {
}
focus() {
ipcRenderer.send('focus', { focus: true });
}
field() {
ipcRenderer.send('field', { field: true });
}
meter() {
ipcRenderer.send('meter', { meter: true });
}
close(evt, arg) {
}
}
filmout = new FilmOut();
module.exports = filmout;

View File

@ -50,6 +50,9 @@ let filmout : FilmOut;
class FilmOut {
private id : string = 'filmout';
private displays : any[] = [];
private state : any = {
frame : 0
}
constructor () {
}
@ -59,9 +62,11 @@ class FilmOut {
listen () {
ipcRenderer.on(this.id, this.onFilmout.bind(this));
ipcRenderer.on('system', this.onSystem.bind(this));
ipcRenderer.on('preview_frame', this.onFrame.bind(this));
}
onSystem (evt : Event, args : any) {
let option : any;
for (let display of args.displays) {
this.displays.push(display);
option = $('<option>');
@ -76,6 +81,7 @@ class FilmOut {
if (args.displays.length > 1) {
$('#filmout_displays').on('change', this.onChange.bind(this));
}
$('#filmout_position').on('change', this.previewFrame.bind(this));
}
onChange () {
@ -109,6 +115,7 @@ class FilmOut {
elem.addClass('on');
$('#filmout_stats_monitor_size').text(`${display.width} x ${display.height}`);
$('#filmout_stats_monitor_aspect').text(`${aspect}`);
$('#filmout_stats_monitor_scale').text(`${parseFloat(display.scale).toFixed(1)} scale factor`);
console.dir(display);
}
selectFile () {
@ -170,13 +177,15 @@ class FilmOut {
onFilmout (evt : any, args : any) {
let state : any;
let color : number[] = [255, 255, 255];
gui.spinner(false);
gui.overlay(false);
if (args.valid && args.valid === true) {
//success state
state = JSON.parse(args.state);
console.dir(args)
console.dir(state)
//console.dir(args)
//console.dir(state)
$('#digital').addClass('active');
$('#projector_type_digital').prop('checked', 'checked');
gui.notify('DEVICES', `Using video ${state.fileName}`);
@ -192,17 +201,65 @@ class FilmOut {
light.enable();
}
this.state.frame = 0;
this.state.frames = state.frames;
this.state.width = state.info.width;
this.state.height = state.info.height;
this.state.name = state.fileName;
$('#seq_loop').val(`${state.frames - 1}`).trigger('change');
$('#filmout_stats_video_name').text(state.fileName);
$('#filmout_stats_video_size').text(`${state.info.width} x ${state.info.height}`);
$('#filmout_stats_video_frames').text(`${state.frames} frames`);
gui.updateState();
this.previewFrame();
} else {
$('#projector_type_digital').prop('checked', 'checked');
$('#digital').removeClass('active');
}
}
previewFrame () {
const frameStr : string = $('#filmout_position').val() as string;
const frame : number = parseInt(frameStr, 10);
this.state.frame = frame;
ipcRenderer.send('preview_frame', { frame });
}
onFrame (evt : any, args : any) {
const elem : any = $('#filmout');
elem[0].style.backgroundImage = `url('${args.path}')`;
elem.addClass('on');
}
advance () {
this.state.frame++;
if (this.state.frame >= this.state.frames) {
this.state.frame = 0;
}
$('#filmout_position').val(this.state.frame).trigger('change');
}
rewind () {
this.state.frame--;
if (this.state.frame < 0) {
this.state.frame = this.state.frames - 1;
}
$('#filmout_position').val(this.state.frame).trigger('change');
}
preview (evt : any, arg : any) {
}
focus () {
ipcRenderer.send('focus', { focus : true });
}
field () {
ipcRenderer.send('field', { field : true });
}
meter () {
ipcRenderer.send('meter', { meter : true });
}
close (evt : any, arg : any) {
}
}
filmout = new FilmOut();

View File

@ -60,7 +60,7 @@ class WebView {
console.error(err);
}
this.showing = true;
await delay_1.delay(100);
await delay_1.delay(200);
return true;
}
async focus() {
@ -68,6 +68,7 @@ class WebView {
console.warn(`Cannot show focus screen because window does not exist`);
return false;
}
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('focus', { focus: true });
}
@ -80,7 +81,7 @@ class WebView {
console.warn(`Cannot show field guide because window does not exist`);
return false;
}
//aspect ratio
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('field', { field: true });
}
@ -93,6 +94,7 @@ class WebView {
console.warn(`Cannot show meter screen because window does not exist`);
return false;
}
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('meter', { meter: true });
}

File diff suppressed because one or more lines are too long

View File

@ -41,6 +41,8 @@ class FilmOut {
this.ipc.on('field', this.field.bind(this));
this.ipc.on('meter', this.meter.bind(this));
this.ipc.on('filmout_close', this.close.bind(this));
//preview
this.ipc.on('preview_frame', this.previewFrame.bind(this));
}
/**
*
@ -127,6 +129,21 @@ class FilmOut {
this.state.enabled = true;
return await this.ui.send(this.id, { valid: true, state: JSON.stringify(this.state) });
}
async previewFrame(evt, arg) {
const state = JSON.parse(JSON.stringify(this.state));
let path;
state.frame = arg.frame;
try {
path = await this.ffmpeg.frame(state, { color: [255, 255, 255] });
}
catch (err) {
console.error(err);
throw err;
}
this.ui.send('preview_frame', { path, frame: arg.frame });
}
async preview(evt, arg) {
}
async focus(evt, arg) {
try {
await this.display.open();

File diff suppressed because one or more lines are too long

View File

@ -60,7 +60,7 @@ class WebView {
console.error(err);
}
this.showing = true;
await delay_1.delay(100);
await delay_1.delay(200);
return true;
}
async focus() {
@ -68,6 +68,7 @@ class WebView {
console.warn(`Cannot show focus screen because window does not exist`);
return false;
}
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('focus', { focus: true });
}
@ -80,7 +81,7 @@ class WebView {
console.warn(`Cannot show field guide because window does not exist`);
return false;
}
//aspect ratio
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('field', { field: true });
}
@ -93,6 +94,7 @@ class WebView {
console.warn(`Cannot show meter screen because window does not exist`);
return false;
}
await delay_1.delay(500);
try {
this.digitalWindow.webContents.send('meter', { meter: true });
}

File diff suppressed because one or more lines are too long

View File

@ -41,6 +41,8 @@ class FilmOut {
this.ipc.on('field', this.field.bind(this));
this.ipc.on('meter', this.meter.bind(this));
this.ipc.on('filmout_close', this.close.bind(this));
//preview
this.ipc.on('preview_frame', this.previewFrame.bind(this));
}
/**
*
@ -127,6 +129,21 @@ class FilmOut {
this.state.enabled = true;
return await this.ui.send(this.id, { valid: true, state: JSON.stringify(this.state) });
}
async previewFrame(evt, arg) {
const state = JSON.parse(JSON.stringify(this.state));
let path;
state.frame = arg.frame;
try {
path = await this.ffmpeg.frame(state, { color: [255, 255, 255] });
}
catch (err) {
console.error(err);
throw err;
}
this.ui.send('preview_frame', { path, frame: arg.frame });
}
async preview(evt, arg) {
}
async focus(evt, arg) {
try {
await this.display.open();

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
ffprobe: Query the container
ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of default=nokey=1:noprint_wrappers=1 input.mp4
This is a fast method.
Not all formats (such as Matroska) will report the number of frames resulting in the output of N/A. See the other methods listed below.
ffprobe: Count the number of frames
ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=nokey=1:noprint_wrappers=1 input.mkv
This is a slow method.
Add the -skip_frame nokey option to only count key frames.
ffmpeg: Count the number of frames
If you do not have ffprobe you can use ffmpeg instead:
ffmpeg -i input.mkv -map 0:v:0 -c copy -f null -
This is a somewhat fast method.
Refer to frame= near the end of the console output.
Add the -discard nokey input option (before -i) to only count key frames.

View File

@ -65,7 +65,7 @@ class WebView {
console.error(err);
}
this.showing = true;
await delay(100);
await delay(200);
return true;
}
async focus () {
@ -73,6 +73,7 @@ class WebView {
console.warn(`Cannot show focus screen because window does not exist`);
return false;
}
await delay(500);
try {
this.digitalWindow.webContents.send('focus', { focus : true });
} catch (err) {
@ -84,7 +85,7 @@ class WebView {
console.warn(`Cannot show field guide because window does not exist`);
return false;
}
//aspect ratio
await delay(500);
try {
this.digitalWindow.webContents.send('field', { field : true });
} catch (err) {
@ -96,6 +97,7 @@ class WebView {
console.warn(`Cannot show meter screen because window does not exist`);
return false;
}
await delay(500);
try {
this.digitalWindow.webContents.send('meter', { meter : true });
} catch (err) {

View File

@ -49,6 +49,8 @@ class FilmOut {
this.ipc.on('field', this.field.bind(this));
this.ipc.on('meter', this.meter.bind(this));
this.ipc.on('filmout_close', this.close.bind(this));
//preview
this.ipc.on('preview_frame', this.previewFrame.bind(this));
}
/**
*
@ -134,6 +136,23 @@ class FilmOut {
this.state.enabled = true;
return await this.ui.send(this.id, { valid : true, state : JSON.stringify(this.state) });
}
async previewFrame (evt : any, arg : any) {
const state : any = JSON.parse(JSON.stringify(this.state));
let path : string;
state.frame = arg.frame;
try {
path = await this.ffmpeg.frame(state, { color : [255, 255, 255] });
} catch (err) {
console.error(err);
throw err;
}
this.ui.send('preview_frame', { path, frame : arg.frame })
}
async preview (evt : any, arg : any) {
}
async focus (evt : any, arg : any) {
try {
await this.display.open();