Improvements

- Fix issue with html5ify on new mac ffmpeg
- Improve output directory logic on mac
pull/304/head
Mikael Finstad 6 years ago
parent 03c2da353d
commit e66acc7f98

@ -46,7 +46,7 @@ import configStore from './store';
import { save as edlStoreSave, load as edlStoreLoad } from './edlStore'; import { save as edlStoreSave, load as edlStoreLoad } from './edlStore';
import { import {
getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle, getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle,
promptTimeOffset, generateColor, getOutDir, withBlur, checkDirWriteAccess, promptTimeOffset, generateColor, getOutDir, withBlur, checkDirWriteAccess, dirExists,
} from './util'; } from './util';
@ -474,17 +474,31 @@ const App = memo(() => {
} }
}, [setCutTime]); }, [setCutTime]);
const setOutputDir = useCallback(async () => { const outputDir = getOutDir(customOutDir, filePath);
const { filePaths } = await dialog.showOpenDialog({ properties: ['openDirectory'] });
setCustomOutDir((filePaths && filePaths.length === 1) ? filePaths[0] : undefined); const askForOutDir = useCallback(async (defaultPath) => {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory'],
title: i18n.t('Where do you want to save output files?'),
message: i18n.t('Where do you want to save output files? Make sure there is enough free space in this folder'),
defaultPath,
buttonLabel: i18n.t('Select output folder'),
});
return (filePaths && filePaths.length === 1) ? filePaths[0] : undefined;
}, []); }, []);
const changeOutDir = useCallback(async () => {
const newOutDir = await askForOutDir(outputDir);
// We cannot allow exporting to a directory which has not yet been confirmed by an open dialog
// because of sandox restrictions
if (isMasBuild && !newOutDir) return;
// Else it's OK, we allow clearing the dir too
setCustomOutDir(newOutDir);
}, [askForOutDir, outputDir]);
const effectiveFilePath = dummyVideoPath || html5FriendlyPath || filePath; const effectiveFilePath = dummyVideoPath || html5FriendlyPath || filePath;
const fileUri = effectiveFilePath ? filePathToUrl(effectiveFilePath) : ''; const fileUri = effectiveFilePath ? filePathToUrl(effectiveFilePath) : '';
const outputDir = getOutDir(customOutDir, filePath);
const getEdlFilePath = useCallback((fp) => getOutPath(customOutDir, fp, 'llc-edl.csv'), [customOutDir]); const getEdlFilePath = useCallback((fp) => getOutPath(customOutDir, fp, 'llc-edl.csv'), [customOutDir]);
const edlFilePath = getEdlFilePath(filePath); const edlFilePath = getEdlFilePath(filePath);
@ -774,26 +788,26 @@ const App = memo(() => {
toast.fire({ timer: 10000, icon: 'warning', title: i18n.t('This video is not natively supported'), text: i18n.t('This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!') }); toast.fire({ timer: 10000, icon: 'warning', title: i18n.t('This video is not natively supported'), text: i18n.t('This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!') });
} }
const createDummyVideo = useCallback(async (fp) => { const createDummyVideo = useCallback(async (cod, fp) => {
const html5ifiedDummyPathDummy = getOutPath(customOutDir, fp, 'html5ified-dummy.mkv'); const html5ifiedDummyPathDummy = getOutPath(cod, fp, 'html5ified-dummy.mkv');
await html5ifyDummy(fp, html5ifiedDummyPathDummy); await html5ifyDummy(fp, html5ifiedDummyPathDummy);
setDummyVideoPath(html5ifiedDummyPathDummy); setDummyVideoPath(html5ifiedDummyPathDummy);
setHtml5FriendlyPath(); setHtml5FriendlyPath();
showUnsupportedFileMessage(); showUnsupportedFileMessage();
}, [customOutDir]); }, []);
const tryCreateDummyVideo = useCallback(async () => { const tryCreateDummyVideo = useCallback(async () => {
try { try {
if (working) return; if (working) return;
setWorking(true); setWorking(true);
await createDummyVideo(filePath); await createDummyVideo(customOutDir, filePath);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
errorToast(i18n.t('Failed to playback this file. Try to convert to friendly format from the menu')); errorToast(i18n.t('Failed to playback this file. Try to convert to friendly format from the menu'));
} finally { } finally {
setWorking(false); setWorking(false);
} }
}, [createDummyVideo, filePath, working]); }, [createDummyVideo, filePath, working, customOutDir]);
const togglePlay = useCallback((resetPlaybackRate) => { const togglePlay = useCallback((resetPlaybackRate) => {
if (!filePath) return; if (!filePath) return;
@ -950,17 +964,7 @@ const App = memo(() => {
} }
}, [playing]); }, [playing]);
const getHtml5ifiedPath = useCallback((fp, type) => getOutPath(customOutDir, fp, `html5ified-${type}.mp4`), [customOutDir]); const getHtml5ifiedPath = useCallback((cod, fp, type) => getOutPath(cod, fp, `html5ified-${type}.mp4`), []);
const checkExistingHtml5FriendlyFile = useCallback(async (fp, speed) => {
const existing = getHtml5ifiedPath(fp, speed);
const ret = existing && await exists(existing);
if (ret) {
setHtml5FriendlyPath(existing);
showUnsupportedFileMessage();
}
return ret;
}, [getHtml5ifiedPath]);
const loadEdlFile = useCallback(async (edlPath) => { const loadEdlFile = useCallback(async (edlPath) => {
try { try {
@ -982,8 +986,8 @@ const App = memo(() => {
} }
}, [cutSegmentsHistory, setCutSegments]); }, [cutSegmentsHistory, setCutSegments]);
const load = useCallback(async (fp, html5FriendlyPathRequested) => { const load = useCallback(async ({ filePath: fp, customOutDir: cod, html5FriendlyPathRequested }) => {
console.log('Load', { fp, html5FriendlyPathRequested }); console.log('Load', { fp, cod, html5FriendlyPathRequested });
if (working) { if (working) {
errorToast(i18n.t('Tried to load file while busy')); errorToast(i18n.t('Tried to load file while busy'));
return; return;
@ -993,6 +997,16 @@ const App = memo(() => {
setWorking(true); setWorking(true);
async function checkExistingHtml5FriendlyFile(speed) {
const existing = getHtml5ifiedPath(cod, fp, speed);
const ret = existing && await exists(existing);
if (ret) {
setHtml5FriendlyPath(existing);
showUnsupportedFileMessage();
}
return ret;
}
try { try {
const fd = await getFormatData(fp); const fd = await getFormatData(fp);
@ -1028,10 +1042,10 @@ const App = memo(() => {
setHtml5FriendlyPath(html5FriendlyPathRequested); setHtml5FriendlyPath(html5FriendlyPathRequested);
showUnsupportedFileMessage(); showUnsupportedFileMessage();
} else if ( } else if (
!(await checkExistingHtml5FriendlyFile(fp, 'slow-audio') || await checkExistingHtml5FriendlyFile(fp, 'slow') || await checkExistingHtml5FriendlyFile(fp, 'fast')) !(await checkExistingHtml5FriendlyFile('slow-audio') || await checkExistingHtml5FriendlyFile('slow') || await checkExistingHtml5FriendlyFile('fast'))
&& !doesPlayerSupportFile(streams) && !doesPlayerSupportFile(streams)
) { ) {
await createDummyVideo(fp); await createDummyVideo(cod, fp);
} }
await loadEdlFile(getEdlFilePath(fp)); await loadEdlFile(getEdlFilePath(fp));
@ -1045,10 +1059,7 @@ const App = memo(() => {
} finally { } finally {
setWorking(false); setWorking(false);
} }
}, [ }, [resetState, working, createDummyVideo, loadEdlFile, getEdlFilePath, getHtml5ifiedPath]);
resetState, working, createDummyVideo, checkExistingHtml5FriendlyFile, loadEdlFile,
getEdlFilePath,
]);
const toggleHelp = useCallback(() => setHelpVisible(val => !val), []); const toggleHelp = useCallback(() => setHelpVisible(val => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []); const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []);
@ -1169,34 +1180,28 @@ const App = memo(() => {
const firstFile = filePaths[0]; const firstFile = filePaths[0];
const outDirPath = getOutDir(customOutDir, firstFile); const customOutDirExists = await dirExists(customOutDir);
if (!customOutDirExists) setCustomOutDir(undefined);
const newCustomOutDir = customOutDirExists ? customOutDir : undefined;
const outDirPath = getOutDir(newCustomOutDir, firstFile);
const hasDirWriteAccess = await checkDirWriteAccess(outDirPath); const hasDirWriteAccess = await checkDirWriteAccess(outDirPath);
if (!hasDirWriteAccess) { if (!hasDirWriteAccess) {
if (isMasBuild) { if (isMasBuild) {
await Swal.fire({ const newOutDir = await askForOutDir(outDirPath);
title: i18n.t('Mac OS file security'), // User cancelled open dialog, refuse to open file, because we will get permission denied from sandbox
icon: 'info', if (!newOutDir) return;
text: i18n.t('Mac OS requires you to choose the folder where the output files should be saved. This is only required the first time for each folder. Simply press "Open" in the next dialog to allow access to the default folder.'), setCustomOutDir(newOutDir);
});
// TODO check that correct dir is selected
// TODO also when customoutdir changes
// eslint-disable-next-line no-unused-vars
const { canceled, filePaths: filePaths2 } = await dialog.showOpenDialog({
title: 'Select file to open',
defaultPath: outDirPath,
properties: ['openDirectory'],
});
if (canceled) return;
} else { } else {
errorToast(i18n.t('You have no write access to the directory of this file, please select a custom working dir')); errorToast(i18n.t('You have no write access to the directory of this file, please select a custom working dir'));
} }
} }
if (!isFileOpened) { if (!isFileOpened) {
load(firstFile); load({ filePath: firstFile, customOutDir: newCustomOutDir });
return; return;
} }
const { value } = await Swal.fire({ const { value } = await Swal.fire({
title: i18n.t('You opened a new file. What do you want to do?'), title: i18n.t('You opened a new file. What do you want to do?'),
icon: 'question', icon: 'question',
@ -1211,12 +1216,12 @@ const App = memo(() => {
}); });
if (value === 'open') { if (value === 'open') {
load(firstFile); load({ filePath: firstFile, customOutDir: newCustomOutDir });
} else if (value === 'add') { } else if (value === 'add') {
addStreamSourceFile(firstFile); addStreamSourceFile(firstFile);
setStreamsSelectorShown(true); setStreamsSelectorShown(true);
} }
}, [addStreamSourceFile, isFileOpened, load, mergeFiles, customOutDir]); }, [addStreamSourceFile, isFileOpened, load, mergeFiles, customOutDir, askForOutDir]);
const onDrop = useCallback(async (ev) => { const onDrop = useCallback(async (ev) => {
ev.preventDefault(); ev.preventDefault();
@ -1249,13 +1254,13 @@ const App = memo(() => {
try { try {
setWorking(true); setWorking(true);
if (['fast', 'slow', 'slow-audio'].includes(speed)) { if (['fast', 'slow', 'slow-audio'].includes(speed)) {
const html5FriendlyPathNew = getHtml5ifiedPath(filePath, speed); const html5FriendlyPathRequested = getHtml5ifiedPath(customOutDir, filePath, speed);
const encodeVideo = ['slow', 'slow-audio'].includes(speed); const encodeVideo = ['slow', 'slow-audio'].includes(speed);
const encodeAudio = speed === 'slow-audio'; const encodeAudio = speed === 'slow-audio';
await ffmpegHtml5ify(filePath, html5FriendlyPathNew, encodeVideo, encodeAudio); await ffmpegHtml5ify(filePath, html5FriendlyPathRequested, encodeVideo, encodeAudio);
load(filePath, html5FriendlyPathNew); load({ filePath, html5FriendlyPathRequested, customOutDir });
} else { } else {
await createDummyVideo(filePath); await createDummyVideo(customOutDir, filePath);
} }
} catch (err) { } catch (err) {
errorToast(i18n.t('Failed to html5ify file')); errorToast(i18n.t('Failed to html5ify file'));
@ -1373,7 +1378,7 @@ const App = memo(() => {
useEffect(() => { useEffect(() => {
document.body.addEventListener('drop', onDrop); document.body.addEventListener('drop', onDrop);
return () => document.body.removeEventListener('drop', onDrop); return () => document.body.removeEventListener('drop', onDrop);
}, [load, mergeFiles, onDrop]); }, [onDrop]);
const commonFormatsMap = useMemo(() => fromPairs(commonFormats.map(format => [format, allOutFormats[format]]) const commonFormatsMap = useMemo(() => fromPairs(commonFormats.map(format => [format, allOutFormats[format]])
@ -1433,7 +1438,7 @@ const App = memo(() => {
const renderSettings = useCallback(() => ( const renderSettings = useCallback(() => (
<Settings <Settings
setOutputDir={setOutputDir} changeOutDir={changeOutDir}
customOutDir={customOutDir} customOutDir={customOutDir}
autoMerge={autoMerge} autoMerge={autoMerge}
setAutoMerge={setAutoMerge} setAutoMerge={setAutoMerge}
@ -1455,7 +1460,7 @@ const App = memo(() => {
renderCaptureFormatButton={renderCaptureFormatButton} renderCaptureFormatButton={renderCaptureFormatButton}
onWheelTunerRequested={onWheelTunerRequested} onWheelTunerRequested={onWheelTunerRequested}
/> />
), [AutoExportToggler, askBeforeClose, autoMerge, autoSaveProjectFile, customOutDir, invertCutSegments, keyframeCut, renderCaptureFormatButton, renderOutFmt, timecodeShowFrames, setOutputDir, onWheelTunerRequested, language]); ), [AutoExportToggler, askBeforeClose, autoMerge, autoSaveProjectFile, customOutDir, invertCutSegments, keyframeCut, renderCaptureFormatButton, renderOutFmt, timecodeShowFrames, changeOutDir, onWheelTunerRequested, language]);
useEffect(() => { useEffect(() => {
if (!isStoreBuild) loadMifiLink().then(setMifiLink); if (!isStoreBuild) loadMifiLink().then(setMifiLink);
@ -1463,7 +1468,7 @@ const App = memo(() => {
useEffect(() => { useEffect(() => {
// Testing: // Testing:
// if (isDev) load('/Users/mifi/Downloads/inp.MOV'); // if (isDev) load({ filePath: '/Users/mifi/Downloads/inp.MOV', customOutDir });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@ -1527,7 +1532,7 @@ const App = memo(() => {
copyAnyAudioTrack={copyAnyAudioTrack} copyAnyAudioTrack={copyAnyAudioTrack}
toggleStripAudio={toggleStripAudio} toggleStripAudio={toggleStripAudio}
customOutDir={customOutDir} customOutDir={customOutDir}
setOutputDir={setOutputDir} changeOutDir={changeOutDir}
renderOutFmt={renderOutFmt} renderOutFmt={renderOutFmt}
outSegments={outSegments} outSegments={outSegments}
autoMerge={autoMerge} autoMerge={autoMerge}

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
const Settings = memo(({ const Settings = memo(({
setOutputDir, customOutDir, autoMerge, setAutoMerge, keyframeCut, setKeyframeCut, invertCutSegments, setInvertCutSegments, changeOutDir, customOutDir, autoMerge, setAutoMerge, keyframeCut, setKeyframeCut, invertCutSegments, setInvertCutSegments,
autoSaveProjectFile, setAutoSaveProjectFile, timecodeShowFrames, setTimecodeShowFrames, askBeforeClose, setAskBeforeClose, autoSaveProjectFile, setAutoSaveProjectFile, timecodeShowFrames, setTimecodeShowFrames, askBeforeClose, setAskBeforeClose,
renderOutFmt, AutoExportToggler, renderCaptureFormatButton, onWheelTunerRequested, language, setLanguage, renderOutFmt, AutoExportToggler, renderCaptureFormatButton, onWheelTunerRequested, language, setLanguage,
}) => { }) => {
@ -44,7 +44,7 @@ const Settings = memo(({
{t('This is where working files, exported files, project files (CSV) are stored.')} {t('This is where working files, exported files, project files (CSV) are stored.')}
</KeyCell> </KeyCell>
<Table.TextCell> <Table.TextCell>
<Button onClick={setOutputDir}> <Button onClick={changeOutDir}>
{customOutDir ? t('Custom working directory') : t('Same directory as input file')} {customOutDir ? t('Custom working directory') : t('Same directory as input file')}
</Button> </Button>
<div>{customOutDir}</div> <div>{customOutDir}</div>

@ -8,7 +8,7 @@ import { withBlur } from './util';
const TopMenu = memo(({ const TopMenu = memo(({
filePath, copyAnyAudioTrack, toggleStripAudio, customOutDir, setOutputDir, filePath, copyAnyAudioTrack, toggleStripAudio, customOutDir, changeOutDir,
renderOutFmt, outSegments, autoMerge, toggleAutoMerge, keyframeCut, toggleKeyframeCut, toggleHelp, renderOutFmt, outSegments, autoMerge, toggleAutoMerge, keyframeCut, toggleKeyframeCut, toggleHelp,
numStreamsToCopy, numStreamsTotal, setStreamsSelectorShown, toggleSettings, numStreamsToCopy, numStreamsTotal, setStreamsSelectorShown, toggleSettings,
}) => { }) => {
@ -42,7 +42,7 @@ const TopMenu = memo(({
<Button <Button
iconBefore={customOutDir ? 'folder-open' : undefined} iconBefore={customOutDir ? 'folder-open' : undefined}
height={20} height={20}
onClick={withBlur(setOutputDir)} onClick={withBlur(changeOutDir)}
title={customOutDir} title={customOutDir}
> >
{customOutDir ? t('Working dir set') : t('Working dir unset')} {customOutDir ? t('Working dir set') : t('Working dir unset')}

@ -309,16 +309,22 @@ export async function cutMultiple({
export async function html5ify(filePath, outPath, encodeVideo, encodeAudio) { export async function html5ify(filePath, outPath, encodeVideo, encodeAudio) {
console.log('Making HTML5 friendly version', { filePath, outPath, encodeVideo }); console.log('Making HTML5 friendly version', { filePath, outPath, encodeVideo });
const videoArgs = encodeVideo let videoArgs;
? ['-vf', 'scale=-2:400,format=yuv420p', '-sws_flags', 'neighbor', '-vcodec', 'libx264', '-profile:v', 'baseline', '-x264opts', 'level=3.0', '-preset:v', 'ultrafast', '-crf', '28'] if (!encodeVideo) videoArgs = ['-vcodec', 'copy'];
: ['-vcodec', 'copy']; else if (os.platform() === 'darwin') {
videoArgs = ['-vf', 'scale=-2:400,format=yuv420p', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k'];
} else {
videoArgs = ['-vf', 'scale=-2:400,format=yuv420p', '-sws_flags', 'neighbor', '-vcodec', 'libx264', '-profile:v', 'baseline', '-x264opts', 'level=3.0', '-preset:v', 'ultrafast', '-crf', '28'];
}
const audioArgs = encodeAudio ? ['-acodec', 'aac', '-b:a', '96k'] : ['-an']; const audioArgs = encodeAudio ? ['-acodec', 'aac', '-b:a', '96k'] : ['-an'];
const ffmpegArgs = [ const ffmpegArgs = [
'-hide_banner', '-hide_banner',
'-i', filePath, ...videoArgs, ...audioArgs, '-i', filePath,
...videoArgs,
...audioArgs,
'-y', outPath, '-y', outPath,
]; ];

@ -23,7 +23,8 @@ i18n
// for all options read: https://www.i18next.com/overview/configuration-options // for all options read: https://www.i18next.com/overview/configuration-options
.init({ .init({
fallbackLng: 'en', fallbackLng: 'en',
debug: isDev, // debug: isDev,
debug: false,
// saveMissing: isDev, // saveMissing: isDev,
// updateMissing: isDev, // updateMissing: isDev,
// saveMissingTo: 'all', // saveMissingTo: 'all',

@ -63,6 +63,10 @@ export async function checkDirWriteAccess(dirPath) {
return true; return true;
} }
export async function dirExists(dirPath) {
return (await fs.exists(dirPath)) && (await fs.lstat(dirPath)).isDirectory();
}
export async function transferTimestamps(inPath, outPath) { export async function transferTimestamps(inPath, outPath) {
try { try {
const stat = await fs.stat(inPath); const stat = await fs.stat(inPath);

Loading…
Cancel
Save