Have seemingly added the ability to use image sequences with the filmout feature with a few caveats. File selection is working on mac but was not on Linux. Also using this method only jpeg and png sequences can be used. This is not unacceptable, but the UI will have to be made more explicit about this limitation. I would like to support TIFF files but even now with single images they are rendered to PNG using ffmpeg.

This commit is contained in:
Matt McWilliams 2021-02-24 00:22:08 -05:00
parent 66639e951b
commit c0121bcfe7
9 changed files with 232 additions and 37 deletions

View File

@ -106,7 +106,7 @@ class FFMPEG {
**/
async frame(state, light) {
const frameNum = state.frame;
const video = state.path;
const video = state.directory ? state.files[frameNum] : state.path;
const w = state.info.width;
const h = state.info.height;
const padded = this.padded_frame(frameNum);
@ -118,8 +118,12 @@ class FFMPEG {
let output;
let fileExists = false;
let scale = '';
if (state.directory) {
return video;
}
if (w && h) {
scale = `,scale=${w}:${h}`;
[];
}
tmpoutput = path_1.join(this.TMPDIR, `${state.hash}-export-${padded}.${ext}`);
try {

File diff suppressed because one or more lines are too long

View File

@ -37,7 +37,8 @@ class FilmOut {
directory: false,
info: {},
dir: true,
enabled: false
enabled: false,
files: []
};
this.display = display;
this.ffmpeg = ffmpeg;
@ -140,8 +141,21 @@ class FilmOut {
let isAnimated = false;
let info;
let ext;
let stats;
let frameList;
try {
stats = await fs_extra_1.lstat(arg.path);
}
catch (err) {
this.log.error(err, 'FILMOUT', true, true);
return false;
}
ext = path_1.extname(arg.fileName.toLowerCase());
if (ext === this.gifExtension) {
if (stats.isDirectory()) {
this.state.directory = true;
this.state.still = false;
}
else if (ext === this.gifExtension) {
try {
isAnimated = await this.isGifAnimated(arg.path);
}
@ -169,7 +183,29 @@ class FilmOut {
this.log.error(err, 'FILMOUT', true, true);
throw err;
}
if (this.state.still) {
if (this.state.directory) {
try {
frameList = await this.dirList(arg.path);
}
catch (err) {
this.log.error(err, 'FILMOUT', true, true);
this.state.enabled = false;
await this.ui.send(this.id, { valid: false });
return false;
}
try {
info = await this.dirInfo(frameList);
}
catch (err) {
this.log.error(err, 'FILMOUT', true, true);
this.state.enabled = false;
await this.ui.send(this.id, { valid: false });
return false;
}
frames = frameList.length;
this.state.files = frameList;
}
else if (this.state.still) {
try {
info = await this.stillInfo(arg.path);
}
@ -206,13 +242,16 @@ class FilmOut {
this.state.fileName = arg.fileName;
this.state.frames = frames;
this.state.info = info;
//this.state.hash = this.hash(arg.path);
this.state.hash = this.hash(arg.path);
if (info.seconds) {
this.state.seconds = info.seconds;
}
else if (info.fps && frames) {
this.state.seconds = frames / info.fps;
}
else if (this.state.directory) {
this.state.seconds = frames / 24;
}
this.log.info(`Opened ${this.state.fileName}`, 'FILMOUT', true, true);
this.log.info(`Frames : ${frames}`, 'FILMOUT', true, true);
this.state.enabled = true;
@ -255,7 +294,7 @@ class FilmOut {
return animated_gif_detector_1.default(gifBuffer);
}
/**
* Return information on a still image using the sharp module
* Return information on a still image using the Jimp module
*
* @param {string} pathStr Path to gif to check
*
@ -271,6 +310,53 @@ class FilmOut {
}
return info;
}
/**
* Return information on the first still image found in a
* directory using the Jimp module.
*
* @param {array} images List of image paths
*
* @returns {object} Info about first image
**/
async dirInfo(images) {
let info;
try {
info = await this.stillInfo(images[0]);
}
catch (err) {
this.log.error(err, 'FILMOUT', true, true);
}
return info;
}
/**
* Returns a list of images within a directory, filtered
* for supported types and sorted.
*
* @param {string} pathStr Path to directory
*
* @returns {array} Array of image paths
**/
async dirList(pathStr) {
let frameList = [];
try {
frameList = await fs_extra_1.readdir(pathStr);
}
catch (err) {
this.log.error(err, 'FILMOUT', true, true);
}
frameList = frameList.filter((fileName) => {
let ext = path_1.extname(fileName);
if (this.stillExtensions.indexOf(ext) !== -1) {
return true;
}
return false;
});
frameList.sort();
frameList = frameList.map((fileName) => {
return path_1.join(pathStr, fileName);
});
return frameList;
}
/**
* Preview a frame from the selected video.
*

File diff suppressed because one or more lines are too long

View File

@ -48,7 +48,7 @@ class FilmOut {
this.id = 'filmout';
this.videoExtensions = ['.mpg', '.mpeg', '.mov', '.mkv', '.avi', '.mp4',
'.gif'];
this.imageExtensions = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp'];
this.stillExtensions = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp'];
this.displays = [];
this.state = {
frame: 0,
@ -129,7 +129,7 @@ class FilmOut {
const elem = $('#digital');
const options = {
title: `Select video or image sequence`,
properties: [`multiSelection`],
properties: [`openFile`, `openDirectory`],
defaultPath: 'c:/',
filters: [
{
@ -171,6 +171,10 @@ class FilmOut {
/**
* Validate the selection to be of an approved selection or a directory
* containing images of an approved extension.
*
* @param {array} files List of files to validate their types
*
* @returns {boolean} Whether or not the selection is valid
**/
validateSelection(files) {
let ext;
@ -186,7 +190,7 @@ class FilmOut {
fileList = fs.readdirSync(pathStr);
fileList = fileList.filter((file) => {
let ext = path.extname(file).toLowerCase();
if (this.imageExtensions.indexOf(ext)) {
if (this.stillExtensions.indexOf(ext)) {
return true;
}
return false;
@ -198,11 +202,10 @@ class FilmOut {
ext = path.extname(pathStr.toLowerCase());
valid = this.videoExtensions.indexOf(ext) === -1;
if (!valid) {
valid = this.imageExtensions.indexOf(ext) === -1;
valid = this.stillExtensions.indexOf(ext) === -1;
}
return valid;
}
return false;
return valid;
}
/**
* Prompt the user to use the selected file/files or cancel
@ -254,7 +257,6 @@ class FilmOut {
if (light.disabled) {
//light.enable();
}
//console.dir(state);
this.state.frame = 0;
this.state.frames = state.frames;
this.state.seconds = state.seconds;
@ -263,13 +265,16 @@ class FilmOut {
this.state.height = state.info.height;
this.state.name = state.fileName;
this.state.path = state.path;
this.state.directory = state.directory;
$('#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();
this.preExport();
if (!this.state.directory) {
this.preExport();
}
}
else {
$('#projector_type_digital').prop('checked', 'checked');

File diff suppressed because one or more lines are too long

View File

@ -46,7 +46,7 @@ class FilmOut {
private id : string = 'filmout';
private videoExtensions : string[] = ['.mpg', '.mpeg', '.mov', '.mkv', '.avi', '.mp4',
'.gif'];
private imageExtensions : string[] = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp'];
private stillExtensions : string[] = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp'];
private displays : any[] = [];
private state : any = {
frame : 0,
@ -133,7 +133,7 @@ class FilmOut {
const elem : any = $('#digital');
const options : any = {
title : `Select video or image sequence`,
properties : [`multiSelection`], // openDirectory, multiSelection, openFile
properties : [`openFile`, `openDirectory`], // openDirectory, multiSelection, openFile
defaultPath: 'c:/',
filters : [
{
@ -176,8 +176,12 @@ class FilmOut {
/**
* Validate the selection to be of an approved selection or a directory
* containing images of an approved extension.
*
* @param {array} files List of files to validate their types
*
* @returns {boolean} Whether or not the selection is valid
**/
validateSelection (files : any) {
validateSelection (files : any) : boolean {
let ext : string;
let pathStr : string;
let dir : boolean = false;
@ -191,7 +195,7 @@ class FilmOut {
fileList = fs.readdirSync(pathStr);
fileList = fileList.filter((file : string) => {
let ext : string = path.extname(file).toLowerCase();
if (this.imageExtensions.indexOf(ext)) {
if (this.stillExtensions.indexOf(ext)) {
return true;
}
return false;
@ -203,11 +207,10 @@ class FilmOut {
ext = path.extname(pathStr.toLowerCase());
valid = this.videoExtensions.indexOf(ext) === -1;
if (!valid) {
valid = this.imageExtensions.indexOf(ext) === -1;
valid = this.stillExtensions.indexOf(ext) === -1;
}
return valid;
}
return false;
return valid;
}
/**
@ -263,7 +266,6 @@ class FilmOut {
if (light.disabled) {
//light.enable();
}
//console.dir(state);
this.state.frame = 0;
this.state.frames = state.frames;
this.state.seconds = state.seconds;
@ -272,6 +274,7 @@ class FilmOut {
this.state.height = state.info.height;
this.state.name = state.fileName;
this.state.path = state.path;
this.state.directory = state.directory;
$('#seq_loop').val(`${state.frames - 1}`).trigger('change');
$('#filmout_stats_video_name').text(state.fileName);
@ -280,7 +283,9 @@ class FilmOut {
gui.updateState();
this.previewFrame();
this.preExport();
if (!this.state.directory) {
this.preExport();
}
} else {
$('#projector_type_digital').prop('checked', 'checked');
$('#digital').removeClass('active');

View File

@ -15,6 +15,8 @@ interface FilmoutState {
hash : string;
info : any;
frames?: number;
directory?: boolean;
files?: string[];
}
interface StdErr {
@ -139,7 +141,7 @@ class FFMPEG {
**/
public async frame (state : FilmoutState, light : any) {
const frameNum : number = state.frame;
const video : string = state.path;
const video : string = state.directory ? state.files[frameNum] : state.path;
const w : number = state.info.width;
const h : number = state.info.height;
const padded : string = this.padded_frame(frameNum);
@ -149,11 +151,15 @@ class FFMPEG {
let tmpoutput : string;
let cmd : string;
let output : any;
let fileExists = false;
let fileExists : boolean = false;
let scale : string = '';
if (state.directory) {
return video;
}
if (w && h) {
scale = `,scale=${w}:${h}`;
scale = `,scale=${w}:${h}`;[]
}
tmpoutput = join(this.TMPDIR, `${state.hash}-export-${padded}.${ext}`);

View File

@ -1,8 +1,8 @@
'use strict';
import { default as animated } from 'animated-gif-detector';
import { extname } from 'path';
import { readFile, lstat } from 'fs-extra';
import { extname, join } from 'path';
import { readFile, lstat, readdir } from 'fs-extra';
import { delay } from 'delay';
import { createHash } from 'crypto';
import Jimp from 'jimp';
@ -26,7 +26,8 @@ class FilmOut {
directory : false,
info : {},
dir : true,
enabled : false
enabled : false,
files : []
};
private display : any;
private ffmpeg : any;
@ -150,10 +151,22 @@ class FilmOut {
let isAnimated : boolean = false;
let info : any;
let ext : string;
let stats : any;
let frameList : string[];
try {
stats = await lstat(arg.path);
} catch (err) {
this.log.error(err, 'FILMOUT', true, true);
return false;
}
ext = extname(arg.fileName.toLowerCase());
if (ext === this.gifExtension) {
if (stats.isDirectory()) {
this.state.directory = true;
this.state.still = false;
} else if (ext === this.gifExtension) {
try {
isAnimated = await this.isGifAnimated(arg.path);
} catch (err) {
@ -178,7 +191,27 @@ class FilmOut {
throw err;
}
if (this.state.still) {
if (this.state.directory) {
try {
frameList = await this.dirList(arg.path);
} catch (err) {
this.log.error(err, 'FILMOUT', true, true);
this.state.enabled = false;
await this.ui.send(this.id, { valid : false });
return false;
}
try {
info = await this.dirInfo(frameList);
} catch (err) {
this.log.error(err, 'FILMOUT', true, true);
this.state.enabled = false;
await this.ui.send(this.id, { valid : false });
return false;
}
frames = frameList.length;
this.state.files = frameList;
} else if (this.state.still) {
try {
info = await this.stillInfo(arg.path);
} catch (err) {
@ -213,12 +246,14 @@ class FilmOut {
this.state.fileName = arg.fileName;
this.state.frames = frames;
this.state.info = info;
//this.state.hash = this.hash(arg.path);
this.state.hash = this.hash(arg.path);
if (info.seconds) {
this.state.seconds = info.seconds;
} else if (info.fps && frames) {
this.state.seconds = frames / info.fps;
} else if (this.state.directory) {
this.state.seconds = frames / 24;
}
this.log.info(`Opened ${this.state.fileName}`, 'FILMOUT', true, true);
@ -265,7 +300,7 @@ class FilmOut {
return animated(gifBuffer);
}
/**
* Return information on a still image using the sharp module
* Return information on a still image using the Jimp module
*
* @param {string} pathStr Path to gif to check
*
@ -282,6 +317,60 @@ class FilmOut {
return info;
}
/**
* Return information on the first still image found in a
* directory using the Jimp module.
*
* @param {array} images List of image paths
*
* @returns {object} Info about first image
**/
async dirInfo (images : string[]) {
let info : any;
try {
info = await this.stillInfo(images[0]);
} catch (err) {
this.log.error(err, 'FILMOUT', true, true);
}
return info;
}
/**
* Returns a list of images within a directory, filtered
* for supported types and sorted.
*
* @param {string} pathStr Path to directory
*
* @returns {array} Array of image paths
**/
async dirList (pathStr : string) {
let frameList : string[] = [];
try {
frameList = await readdir(pathStr)
} catch (err) {
this.log.error(err, 'FILMOUT', true, true);
}
frameList = frameList.filter((fileName : string) => {
let ext : string = extname(fileName);
if (this.stillExtensions.indexOf(ext) !== -1) {
return true;
}
return false;
});
frameList.sort();
frameList = frameList.map((fileName : string) => {
return join(pathStr, fileName);
});
return frameList;
}
/**
* Preview a frame from the selected video.
*