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

File diff suppressed because one or more lines are too long

View File

@ -37,7 +37,8 @@ class FilmOut {
directory: false, directory: false,
info: {}, info: {},
dir: true, dir: true,
enabled: false enabled: false,
files: []
}; };
this.display = display; this.display = display;
this.ffmpeg = ffmpeg; this.ffmpeg = ffmpeg;
@ -140,8 +141,21 @@ class FilmOut {
let isAnimated = false; let isAnimated = false;
let info; let info;
let ext; 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()); 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 { try {
isAnimated = await this.isGifAnimated(arg.path); isAnimated = await this.isGifAnimated(arg.path);
} }
@ -169,7 +183,29 @@ class FilmOut {
this.log.error(err, 'FILMOUT', true, true); this.log.error(err, 'FILMOUT', true, true);
throw err; 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 { try {
info = await this.stillInfo(arg.path); info = await this.stillInfo(arg.path);
} }
@ -206,13 +242,16 @@ class FilmOut {
this.state.fileName = arg.fileName; this.state.fileName = arg.fileName;
this.state.frames = frames; this.state.frames = frames;
this.state.info = info; this.state.info = info;
//this.state.hash = this.hash(arg.path); this.state.hash = this.hash(arg.path);
if (info.seconds) { if (info.seconds) {
this.state.seconds = info.seconds; this.state.seconds = info.seconds;
} }
else if (info.fps && frames) { else if (info.fps && frames) {
this.state.seconds = frames / info.fps; 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(`Opened ${this.state.fileName}`, 'FILMOUT', true, true);
this.log.info(`Frames : ${frames}`, 'FILMOUT', true, true); this.log.info(`Frames : ${frames}`, 'FILMOUT', true, true);
this.state.enabled = true; this.state.enabled = true;
@ -255,7 +294,7 @@ class FilmOut {
return animated_gif_detector_1.default(gifBuffer); 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 * @param {string} pathStr Path to gif to check
* *
@ -271,6 +310,53 @@ class FilmOut {
} }
return info; 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. * 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.id = 'filmout';
this.videoExtensions = ['.mpg', '.mpeg', '.mov', '.mkv', '.avi', '.mp4', this.videoExtensions = ['.mpg', '.mpeg', '.mov', '.mkv', '.avi', '.mp4',
'.gif']; '.gif'];
this.imageExtensions = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp']; this.stillExtensions = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp'];
this.displays = []; this.displays = [];
this.state = { this.state = {
frame: 0, frame: 0,
@ -129,7 +129,7 @@ class FilmOut {
const elem = $('#digital'); const elem = $('#digital');
const options = { const options = {
title: `Select video or image sequence`, title: `Select video or image sequence`,
properties: [`multiSelection`], properties: [`openFile`, `openDirectory`],
defaultPath: 'c:/', defaultPath: 'c:/',
filters: [ filters: [
{ {
@ -171,6 +171,10 @@ class FilmOut {
/** /**
* Validate the selection to be of an approved selection or a directory * Validate the selection to be of an approved selection or a directory
* containing images of an approved extension. * 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) { validateSelection(files) {
let ext; let ext;
@ -186,7 +190,7 @@ class FilmOut {
fileList = fs.readdirSync(pathStr); fileList = fs.readdirSync(pathStr);
fileList = fileList.filter((file) => { fileList = fileList.filter((file) => {
let ext = path.extname(file).toLowerCase(); let ext = path.extname(file).toLowerCase();
if (this.imageExtensions.indexOf(ext)) { if (this.stillExtensions.indexOf(ext)) {
return true; return true;
} }
return false; return false;
@ -198,11 +202,10 @@ class FilmOut {
ext = path.extname(pathStr.toLowerCase()); ext = path.extname(pathStr.toLowerCase());
valid = this.videoExtensions.indexOf(ext) === -1; valid = this.videoExtensions.indexOf(ext) === -1;
if (!valid) { 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 * Prompt the user to use the selected file/files or cancel
@ -254,7 +257,6 @@ class FilmOut {
if (light.disabled) { if (light.disabled) {
//light.enable(); //light.enable();
} }
//console.dir(state);
this.state.frame = 0; this.state.frame = 0;
this.state.frames = state.frames; this.state.frames = state.frames;
this.state.seconds = state.seconds; this.state.seconds = state.seconds;
@ -263,13 +265,16 @@ class FilmOut {
this.state.height = state.info.height; this.state.height = state.info.height;
this.state.name = state.fileName; this.state.name = state.fileName;
this.state.path = state.path; this.state.path = state.path;
this.state.directory = state.directory;
$('#seq_loop').val(`${state.frames - 1}`).trigger('change'); $('#seq_loop').val(`${state.frames - 1}`).trigger('change');
$('#filmout_stats_video_name').text(state.fileName); $('#filmout_stats_video_name').text(state.fileName);
$('#filmout_stats_video_size').text(`${state.info.width} x ${state.info.height}`); $('#filmout_stats_video_size').text(`${state.info.width} x ${state.info.height}`);
$('#filmout_stats_video_frames').text(`${state.frames} frames`); $('#filmout_stats_video_frames').text(`${state.frames} frames`);
gui.updateState(); gui.updateState();
this.previewFrame(); this.previewFrame();
this.preExport(); if (!this.state.directory) {
this.preExport();
}
} }
else { else {
$('#projector_type_digital').prop('checked', 'checked'); $('#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 id : string = 'filmout';
private videoExtensions : string[] = ['.mpg', '.mpeg', '.mov', '.mkv', '.avi', '.mp4', private videoExtensions : string[] = ['.mpg', '.mpeg', '.mov', '.mkv', '.avi', '.mp4',
'.gif']; '.gif'];
private imageExtensions : string[] = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp']; private stillExtensions : string[] = ['.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp'];
private displays : any[] = []; private displays : any[] = [];
private state : any = { private state : any = {
frame : 0, frame : 0,
@ -133,7 +133,7 @@ class FilmOut {
const elem : any = $('#digital'); const elem : any = $('#digital');
const options : any = { const options : any = {
title : `Select video or image sequence`, title : `Select video or image sequence`,
properties : [`multiSelection`], // openDirectory, multiSelection, openFile properties : [`openFile`, `openDirectory`], // openDirectory, multiSelection, openFile
defaultPath: 'c:/', defaultPath: 'c:/',
filters : [ filters : [
{ {
@ -176,8 +176,12 @@ class FilmOut {
/** /**
* Validate the selection to be of an approved selection or a directory * Validate the selection to be of an approved selection or a directory
* containing images of an approved extension. * 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 ext : string;
let pathStr : string; let pathStr : string;
let dir : boolean = false; let dir : boolean = false;
@ -191,7 +195,7 @@ class FilmOut {
fileList = fs.readdirSync(pathStr); fileList = fs.readdirSync(pathStr);
fileList = fileList.filter((file : string) => { fileList = fileList.filter((file : string) => {
let ext : string = path.extname(file).toLowerCase(); let ext : string = path.extname(file).toLowerCase();
if (this.imageExtensions.indexOf(ext)) { if (this.stillExtensions.indexOf(ext)) {
return true; return true;
} }
return false; return false;
@ -203,11 +207,10 @@ class FilmOut {
ext = path.extname(pathStr.toLowerCase()); ext = path.extname(pathStr.toLowerCase());
valid = this.videoExtensions.indexOf(ext) === -1; valid = this.videoExtensions.indexOf(ext) === -1;
if (!valid) { 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) { if (light.disabled) {
//light.enable(); //light.enable();
} }
//console.dir(state);
this.state.frame = 0; this.state.frame = 0;
this.state.frames = state.frames; this.state.frames = state.frames;
this.state.seconds = state.seconds; this.state.seconds = state.seconds;
@ -272,6 +274,7 @@ class FilmOut {
this.state.height = state.info.height; this.state.height = state.info.height;
this.state.name = state.fileName; this.state.name = state.fileName;
this.state.path = state.path; this.state.path = state.path;
this.state.directory = state.directory;
$('#seq_loop').val(`${state.frames - 1}`).trigger('change'); $('#seq_loop').val(`${state.frames - 1}`).trigger('change');
$('#filmout_stats_video_name').text(state.fileName); $('#filmout_stats_video_name').text(state.fileName);
@ -280,7 +283,9 @@ class FilmOut {
gui.updateState(); gui.updateState();
this.previewFrame(); this.previewFrame();
this.preExport(); if (!this.state.directory) {
this.preExport();
}
} else { } else {
$('#projector_type_digital').prop('checked', 'checked'); $('#projector_type_digital').prop('checked', 'checked');
$('#digital').removeClass('active'); $('#digital').removeClass('active');

View File

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

View File

@ -1,8 +1,8 @@
'use strict'; 'use strict';
import { default as animated } from 'animated-gif-detector'; import { default as animated } from 'animated-gif-detector';
import { extname } from 'path'; import { extname, join } from 'path';
import { readFile, lstat } from 'fs-extra'; import { readFile, lstat, readdir } from 'fs-extra';
import { delay } from 'delay'; import { delay } from 'delay';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import Jimp from 'jimp'; import Jimp from 'jimp';
@ -26,7 +26,8 @@ class FilmOut {
directory : false, directory : false,
info : {}, info : {},
dir : true, dir : true,
enabled : false enabled : false,
files : []
}; };
private display : any; private display : any;
private ffmpeg : any; private ffmpeg : any;
@ -150,10 +151,22 @@ class FilmOut {
let isAnimated : boolean = false; let isAnimated : boolean = false;
let info : any; let info : any;
let ext : string; 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()); 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 { try {
isAnimated = await this.isGifAnimated(arg.path); isAnimated = await this.isGifAnimated(arg.path);
} catch (err) { } catch (err) {
@ -178,7 +191,27 @@ class FilmOut {
throw err; 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 { try {
info = await this.stillInfo(arg.path); info = await this.stillInfo(arg.path);
} catch (err) { } catch (err) {
@ -213,12 +246,14 @@ class FilmOut {
this.state.fileName = arg.fileName; this.state.fileName = arg.fileName;
this.state.frames = frames; this.state.frames = frames;
this.state.info = info; this.state.info = info;
//this.state.hash = this.hash(arg.path); this.state.hash = this.hash(arg.path);
if (info.seconds) { if (info.seconds) {
this.state.seconds = info.seconds; this.state.seconds = info.seconds;
} else if (info.fps && frames) { } else if (info.fps && frames) {
this.state.seconds = frames / info.fps; 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(`Opened ${this.state.fileName}`, 'FILMOUT', true, true);
@ -265,7 +300,7 @@ class FilmOut {
return animated(gifBuffer); 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 * @param {string} pathStr Path to gif to check
* *
@ -282,6 +317,60 @@ class FilmOut {
return info; 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. * Preview a frame from the selected video.
* *