Improvements

- Simplify and streamline preview file logic
- Allow remember preview choice (convert to supported format) #829
- Improve error handling
pull/841/head
Mikael Finstad 4 years ago
parent 96086d307f
commit b7678510c7
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -54,10 +54,10 @@ import {
import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, readEdl } from './edlStore';
import { formatYouTube } from './edlFormats';
import {
getOutPath, toast, errorToast, handleError, showFfmpegFail, setFileNameTitle, getOutDir, withBlur,
getOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, withBlur,
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer, doesPlayerSupportFile,
isDurationValid, isWindows, filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
hasDuplicates, havePermissionToReadFile, isMac, getFileBaseName, resolvePathIfNeeded, pathExists,
hasDuplicates, havePermissionToReadFile, isMac, resolvePathIfNeeded, pathExists, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
} from './util';
import { formatDuration } from './util/duration';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, showMergeDialog, showOpenAndMergeDialog, openAbout, showEditableJsonDialog } from './dialogs';
@ -65,14 +65,20 @@ import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
import { createSegment, createInitialCutSegments, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags } from './segments';
import loadingLottie from './7077-magic-flow.json';
function getHtml5ifiedPath(cod, fp, type) {
// See also inside ffmpegHtml5ify
const ext = (isMac && ['slowest', 'slow', 'slow-audio'].includes(type)) ? 'mp4' : 'mkv';
return getOutPath(cod, fp, `${html5ifiedPrefix}${type}.${ext}`);
}
const isDev = window.require('electron-is-dev');
const electron = window.require('electron'); // eslint-disable-line
const trash = window.require('trash');
const { unlink, exists, readdir } = window.require('fs-extra');
const { unlink, exists } = window.require('fs-extra');
const { extname, parse: parsePath, sep: pathSep, join: pathJoin, normalize: pathNormalize, basename, dirname } = window.require('path');
const { dialog } = electron.remote;
@ -134,6 +140,7 @@ const App = memo(() => {
const [showSideBar, setShowSideBar] = useState(true);
const [hideCanvasPreview, setHideCanvasPreview] = useState(false);
const [cleanupChoices, setCleanupChoices] = useState({ tmpFiles: true });
const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState();
// Segment related state
const [currentSegIndex, setCurrentSegIndex] = useState(0);
@ -275,10 +282,6 @@ const App = memo(() => {
seekRel((1 / (detectedFps || 60)) * dir);
}, [seekRel, detectedFps]);
/* useEffect(() => () => {
if (usingDummyVideo && previewFilePath) unlink(previewFilePath).catch(console.error);
}, [usingDummyVideo, previewFilePath]); */
// 360 means we don't modify rotation
const isRotationSet = rotation !== 360;
const effectiveRotation = isRotationSet ? rotation : (mainVideoStream && mainVideoStream.tags && mainVideoStream.tags.rotate && parseInt(mainVideoStream.tags.rotate, 10));
@ -502,6 +505,7 @@ const App = memo(() => {
setCustomOutDir();
}, [setCustomOutDir]);
const usingPreviewFile = !!previewFilePath;
const effectiveFilePath = previewFilePath || filePath;
const fileUri = effectiveFilePath ? filePathToUrl(effectiveFilePath) : '';
@ -600,6 +604,7 @@ const App = memo(() => {
}, [customOutDir, setCustomOutDir]);
const mergeFiles = useCallback(async ({ paths, allStreams, segmentsToChapters: segmentsToChapters2 }) => {
if (working) return;
try {
setWorking(i18n.t('Merging'));
@ -627,7 +632,7 @@ const App = memo(() => {
setWorking();
setCutProgress();
}
}, [assureOutDirAccess, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, customOutDir, ffmpegMergeFiles]);
}, [assureOutDirAccess, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, customOutDir, ffmpegMergeFiles, working]);
const toggleCaptureFormat = useCallback(() => setCaptureFormat(f => (f === 'png' ? 'jpeg' : 'png')), [setCaptureFormat]);
@ -755,10 +760,10 @@ const App = memo(() => {
video.currentTime = 0;
video.playbackRate = 1;
// setWorking();
setFileNameTitle();
setPreviewFilePath();
setUsingDummyVideo(false);
setWorking();
setPlaying(false);
setDuration();
cutSegmentsHistory.go(0);
@ -802,36 +807,62 @@ const App = memo(() => {
if (!hideAllNotifications) toast.fire({ text: i18n.t('Loaded existing preview file: {{ fileName }}', { fileName }) });
}, [hideAllNotifications]);
const html5ifiedPrefix = 'html5ified-';
const html5dummySuffix = 'dummy';
const html5ify = useCallback(async ({ customOutDir: cod, filePath: fp, speed, hasAudio: ha, hasVideo: hv }) => {
const path = getHtml5ifiedPath(cod, fp, speed);
let audio;
if (ha) {
if (speed === 'slowest') audio = 'hq';
else if (['slow-audio', 'fastest-audio'].includes(speed)) audio = 'lq';
else if (['fast-audio', 'fastest-audio-remux'].includes(speed)) audio = 'copy';
}
let video;
if (hv) {
if (speed === 'slowest') video = 'hq';
else if (['slow-audio', 'slow'].includes(speed)) video = 'lq';
else video = 'copy';
}
const createDummyVideo = useCallback(async (cod, fp) => {
const html5ifiedDummyPath = getOutPath(cod, fp, `${html5ifiedPrefix}${html5dummySuffix}.mkv`);
try {
setCutProgress(0);
await html5ifyDummy({ filePath: fp, outPath: html5ifiedDummyPath, onProgress: setCutProgress });
await ffmpegHtml5ify({ filePath: fp, outPath: path, video, audio, onProgress: setCutProgress });
} finally {
setCutProgress();
}
setUsingDummyVideo(true);
setPreviewFilePath(html5ifiedDummyPath);
showUnsupportedFileMessage();
}, [html5ifyDummy, showUnsupportedFileMessage]);
return path;
}, [ffmpegHtml5ify]);
const showPlaybackFailedMessage = () => errorToast(i18n.t('Unable to playback this file. Try to convert to supported format from the menu'));
const html5ifyAndLoad = useCallback(async (cod, fp, speed, hv, ha) => {
const usesDummyVideo = ['fastest-audio', 'fastest-audio-remux', 'fastest'].includes(speed);
console.log('html5ifyAndLoad', { speed, hasVideo: hv, hasAudio: ha, usesDummyVideo });
const tryCreateDummyVideo = useCallback(async () => {
try {
if (working) return;
setWorking(i18n.t('Converting to supported format'));
await createDummyVideo(customOutDir, filePath);
} catch (err) {
console.error(err);
showPlaybackFailedMessage();
} finally {
setWorking();
async function doHtml5ify() {
if (speed === 'fastest') {
const path = getOutPath(cod, fp, `${html5ifiedPrefix}${html5dummySuffix}.mkv`);
try {
setCutProgress(0);
await html5ifyDummy({ filePath: fp, outPath: path, onProgress: setCutProgress });
} finally {
setCutProgress();
}
return path;
}
if (['fastest-audio', 'fastest-audio-remux', 'fast-audio', 'fast', 'slow', 'slow-audio', 'slowest'].includes(speed)) {
const shouldIncludeVideo = !usesDummyVideo && hv;
const path = await html5ify({ customOutDir: cod, filePath: fp, speed, hasAudio: ha, hasVideo: shouldIncludeVideo });
return path;
}
return undefined;
}
}, [createDummyVideo, filePath, working, customOutDir]);
const path = await doHtml5ify();
if (!path) return;
setPreviewFilePath(path);
setUsingDummyVideo(usesDummyVideo);
showUnsupportedFileMessage();
}, [html5ify, html5ifyDummy, showUnsupportedFileMessage]);
const showPlaybackFailedMessage = () => errorToast(i18n.t('Unable to playback this file. Try to convert to supported format from the menu'));
const togglePlay = useCallback((resetPlaybackRate) => {
if (!filePath) return;
@ -860,6 +891,8 @@ const App = memo(() => {
}, [askBeforeClose, isFileOpened, resetState, working]);
const cleanupFiles = useCallback(async () => {
if (working) return;
// Because we will reset state before deleting files
const saved = { previewFilePath, filePath, edlFilePath };
@ -908,7 +941,7 @@ const App = memo(() => {
} finally {
setWorking();
}
}, [filePath, previewFilePath, closeFile, edlFilePath, cleanupChoices]);
}, [filePath, previewFilePath, closeFile, edlFilePath, cleanupChoices, working]);
const outSegments = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments),
[invertCutSegments, inverseCutSegments, apparentCutSegments]);
@ -1087,7 +1120,7 @@ const App = memo(() => {
return;
}
showFfmpegFail(err);
handleError(err);
} finally {
setWorking();
setCutProgress();
@ -1145,12 +1178,6 @@ const App = memo(() => {
}
}, [playing, canvasPlayerEnabled]);
const getHtml5ifiedPath = useCallback((cod, fp, type) => {
// See also inside ffmpegHtml5ify
const ext = (isMac && ['slowest', 'slow', 'slow-audio'].includes(type)) ? 'mp4' : 'mkv';
return getOutPath(cod, fp, `${html5ifiedPrefix}${type}.${ext}`);
}, []);
const firstSegmentAtCursorIndex = useMemo(() => {
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, commandedTime);
return segmentsAtCursorIndexes[0];
@ -1196,66 +1223,51 @@ const App = memo(() => {
}, [setCutSegments]);
const loadEdlFile = useCallback(async (path, type) => {
try {
const edl = await readEdlFile({ type, path });
loadCutSegments(edl);
} catch (err) {
console.error('EDL load failed', err);
errorToast(`${i18n.t('Failed to load segments')} (${err.message})`);
}
console.log('Loading EDL file', type, path);
loadCutSegments(await readEdlFile({ type, path }));
}, [loadCutSegments]);
const load = useCallback(async ({ filePath: fp, customOutDir: cod, html5FriendlyPathRequested, dummyVideoPathRequested, projectPath }) => {
console.log('Load', { fp, cod, html5FriendlyPathRequested, dummyVideoPathRequested, projectPath });
if (working) return;
const load = useCallback(async ({ filePath: fp, customOutDir: cod, projectPath }) => {
console.log('Load', { fp, cod, projectPath });
resetState();
setWorking(i18n.t('Loading file'));
async function checkAndSetExistingHtml5FriendlyFile() {
const speeds = ['slowest', 'slow-audio', 'slow', 'fast-audio', 'fast', 'fastest-audio', 'fastest-audio-remux', html5dummySuffix];
const prefix = `${getFileBaseName(fp)}-${html5ifiedPrefix}`;
const outDir = getOutDir(cod, fp);
const dirEntries = await readdir(outDir);
let speed;
let path;
// eslint-disable-next-line no-restricted-syntax
for (const entry of dirEntries) {
const prefixMatch = entry.startsWith(prefix);
if (prefixMatch) {
const speedMatch = speeds.find((s) => new RegExp(`${s}\\..*$`).test(entry.replace(prefix, '')));
if (!speedMatch || speedMatch !== html5dummySuffix) { // skip dummy, as it's not very useful
path = pathJoin(outDir, entry);
if (speedMatch) speed = speedMatch; // We want to also capture any custom user suffix (but NOT dummy)
break;
}
const res = await findExistingHtml5FriendlyFile(fp, cod);
if (!res) return false;
console.log('Found existing supported file', res.path);
setUsingDummyVideo(res.usingDummyVideo);
setPreviewFilePath(res.path);
showPreviewFileLoadedMessage(basename(res.path));
return true;
}
async function getFileMeta() {
try {
const fd = await getFormatData(fp);
const ff = await getDefaultOutFormat(fp, fd);
const allStreamsResponse = await getAllStreams(fp);
const { streams } = allStreamsResponse;
// console.log(streams, fd, ff);
return { fd, ff, streams };
} catch (err) {
// Windows will throw error with code ENOENT if format detection fails.
if (err.exitCode === 1 || (isWindows && err.code === 'ENOENT')) {
const err2 = new Error(`Unsupported file: ${err.message}`);
err2.code = 'LLC_FFPROBE_UNSUPPORTED_FILE';
throw err2;
}
throw err;
}
if (!path) return false;
console.log('Found existing supported file', path, speed);
setUsingDummyVideo(['fastest-audio', 'fastest-audio-remux'].includes(speed));
setPreviewFilePath(path);
showPreviewFileLoadedMessage(basename(path));
return true;
}
try {
const fd = await getFormatData(fp);
const { ff, fd, streams } = await getFileMeta();
const ff = await getDefaultOutFormat(fp, fd);
if (!ff) {
errorToast(i18n.t('Unable to determine file format'));
return;
}
const { streams } = await getAllStreams(fp);
// console.log('streams', streamsNew);
if (!ff) throw new Error('Unable to determine file format');
if (autoLoadTimecode) {
const timecode = getTimecodeFromStreams(streams);
@ -1294,40 +1306,38 @@ const App = memo(() => {
}
const validDuration = isDurationValid(parseFloat(fd.duration));
const hasLoadedExistingHtml5FriendlyFile = await checkAndSetExistingHtml5FriendlyFile();
if (html5FriendlyPathRequested) {
setUsingDummyVideo(false);
setPreviewFilePath(html5FriendlyPathRequested);
showUnsupportedFileMessage();
} else if (dummyVideoPathRequested) {
setUsingDummyVideo(true);
setPreviewFilePath(dummyVideoPathRequested);
showUnsupportedFileMessage();
} else if (
!(await checkAndSetExistingHtml5FriendlyFile())
&& !doesPlayerSupportFile(streams)
&& validDuration
) {
await createDummyVideo(cod, fp);
// 'fastest' works with almost all video files
if (!hasLoadedExistingHtml5FriendlyFile && !doesPlayerSupportFile(streams) && validDuration) {
setWorking(i18n.t('Converting to supported format'));
await html5ifyAndLoad(cod, fp, rememberConvertToSupportedFormat || 'fastest', !!videoStream, !!audioStream);
}
const openedFileEdlPath = getEdlFilePath(fp);
const openedFileEdlPathOld = getEdlFilePathOld(fp);
if (projectPath) {
await loadEdlFile(projectPath, 'llc');
} else if (await exists(openedFileEdlPath)) {
await loadEdlFile(openedFileEdlPath, 'llc');
} else if (await exists(openedFileEdlPathOld)) {
await loadEdlFile(openedFileEdlPathOld, 'csv');
} else {
const edl = await tryReadChaptersToEdl(fp);
if (edl.length > 0 && enableAskForImportChapters && (await askForImportChapters())) {
console.log('Read chapters', edl);
loadCutSegments(edl);
try {
const openedFileEdlPath = getEdlFilePath(fp);
const openedFileEdlPathOld = getEdlFilePathOld(fp);
if (projectPath) {
await loadEdlFile(projectPath, 'llc');
} else if (await exists(openedFileEdlPath)) {
await loadEdlFile(openedFileEdlPath, 'llc');
} else if (await exists(openedFileEdlPathOld)) {
await loadEdlFile(openedFileEdlPathOld, 'csv');
} else {
const edl = await tryReadChaptersToEdl(fp);
if (edl.length > 0 && enableAskForImportChapters && (await askForImportChapters())) {
console.log('Read chapters', edl);
loadCutSegments(edl);
}
}
} catch (err) {
console.error('EDL load failed, but continuing', err);
errorToast(`${i18n.t('Failed to load segments')} (${err.message})`);
}
// throw new Error('test');
if (!validDuration) toast.fire({ icon: 'warning', timer: 10000, text: i18n.t('This file does not have a valid duration. This may cause issues. You can try to fix the file\'s duration from the File menu') });
// This needs to be last, because it triggers <video> to load the video
@ -1335,17 +1345,10 @@ const App = memo(() => {
// https://github.com/mifi/lossless-cut/issues/515
setFilePath(fp);
} catch (err) {
// Windows will throw error with code ENOENT if format detection fails.
if (err.exitCode === 1 || (isWindows && err.code === 'ENOENT')) {
errorToast(i18n.t('Unsupported file'));
console.error(err);
return;
}
showFfmpegFail(err);
} finally {
setWorking();
resetState();
throw err;
}
}, [resetState, working, createDummyVideo, loadEdlFile, getEdlFilePath, getEdlFilePathOld, loadCutSegments, enableAskForImportChapters, showUnsupportedFileMessage, autoLoadTimecode, outFormatLocked, showPreviewFileLoadedMessage]);
}, [resetState, html5ifyAndLoad, loadEdlFile, getEdlFilePath, getEdlFilePathOld, loadCutSegments, enableAskForImportChapters, autoLoadTimecode, outFormatLocked, showPreviewFileLoadedMessage, rememberConvertToSupportedFormat]);
const toggleHelp = useCallback(() => setHelpVisible(val => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []);
@ -1504,7 +1507,7 @@ const App = memo(() => {
}, [customOutDir, filePath, mainStreams, outputDir, working]);
const extractAllStreams = useCallback(async () => {
if (!filePath) return;
if (!filePath || working) return;
if (!(await confirmExtractAllStreamsDialog())) return;
@ -1519,7 +1522,7 @@ const App = memo(() => {
} finally {
setWorking();
}
}, [customOutDir, filePath, mainStreams, outputDir]);
}, [customOutDir, filePath, mainStreams, outputDir, working]);
const addStreamSourceFile = useCallback(async (path) => {
if (externalStreamFiles[path]) return;
@ -1544,7 +1547,6 @@ const App = memo(() => {
projectPath = path;
path = pathJoin(dirname(path), mediaFileName);
}
// Because Apple is being nazi about the ability to open "copy protected DVD files"
const disallowVob = isMasBuild;
if (disallowVob && /\.vob$/i.test(path)) {
@ -1576,7 +1578,7 @@ const App = memo(() => {
const userOpenFiles = useCallback(async (filePaths) => {
try {
if (!filePaths || filePaths.length < 1) return;
if (!filePaths || filePaths.length < 1 || working) return;
console.log('userOpenFiles');
console.log(filePaths.join('\n'));
@ -1591,6 +1593,8 @@ const App = memo(() => {
const filePathLowerCase = firstFilePath.toLowerCase();
setWorking(i18n.t('Loading file'));
// Import CSV project for existing video
if (filePathLowerCase.endsWith('.csv')) {
if (!checkFileOpened()) return;
@ -1628,66 +1632,42 @@ const App = memo(() => {
await userOpenSingleFile({ path: firstFilePath, isLlcProject });
} catch (err) {
console.error('userOpenFiles', err);
errorToast(i18n.t('Failed to open file'));
if (err.code === 'LLC_FFPROBE_UNSUPPORTED_FILE') {
errorToast(i18n.t('Unsupported file'));
} else {
handleError(i18n.t('Failed to open file'), err);
}
} finally {
setWorking();
}
}, [addStreamSourceFile, checkFileOpened, enableAskForFileOpenAction, isFileOpened, loadEdlFile, mergeFiles, userOpenSingleFile]);
}, [addStreamSourceFile, checkFileOpened, enableAskForFileOpenAction, isFileOpened, loadEdlFile, mergeFiles, userOpenSingleFile, working]);
const html5ify = useCallback(async ({ customOutDir: cod, filePath: fp, speed, hasAudio: ha, hasVideo: hv }) => {
const path = getHtml5ifiedPath(cod, fp, speed);
const userHtml5ifyCurrentFile = useCallback(async () => {
if (!filePath || working) return;
let audio;
if (ha) {
if (speed === 'slowest') audio = 'hq';
else if (['slow-audio', 'fastest-audio'].includes(speed)) audio = 'lq';
else if (['fast-audio', 'fastest-audio-remux'].includes(speed)) audio = 'copy';
}
async function getHtml5ifySpeed() {
const { selectedOption, remember } = await askForHtml5ifySpeed({ allowedOptions: ['fastest', 'fastest-audio', 'fastest-audio-remux', 'fast-audio', 'fast', 'slow', 'slow-audio', 'slowest'], showRemember: true, initialOption: rememberConvertToSupportedFormat });
if (!selectedOption) return undefined;
let video;
if (hv) {
if (speed === 'slowest') video = 'hq';
else if (['slow-audio', 'slow'].includes(speed)) video = 'lq';
else video = 'copy';
}
console.log('Choice', { speed: selectedOption, remember });
try {
await ffmpegHtml5ify({ filePath: fp, outPath: path, video, audio, onProgress: setCutProgress });
} finally {
setCutProgress();
}
return path;
}, [ffmpegHtml5ify, getHtml5ifiedPath]);
setRememberConvertToSupportedFormat(remember ? selectedOption : undefined);
const html5ifyAndLoad = useCallback(async (speed) => {
if (['fastest-audio', 'fastest-audio-remux'].includes(speed)) {
const path = await html5ify({ customOutDir, filePath, speed, hasAudio, hasVideo: false });
load({ filePath, dummyVideoPathRequested: path, customOutDir });
} else {
const path = await html5ify({ customOutDir, filePath, speed, hasAudio, hasVideo });
load({ filePath, html5FriendlyPathRequested: path, customOutDir });
return selectedOption;
}
}, [hasAudio, hasVideo, customOutDir, filePath, html5ify, load]);
const userHtml5ifyCurrentFile = useCallback(async () => {
if (!filePath) return;
try {
setWorking(i18n.t('Converting to supported format'));
const speed = await askForHtml5ifySpeed(['fastest', 'fastest-audio', 'fastest-audio-remux', 'fast-audio', 'fast', 'slow', 'slow-audio', 'slowest']);
const speed = await getHtml5ifySpeed();
if (!speed) return;
if (speed === 'fastest') {
await createDummyVideo(customOutDir, filePath);
} else if (['fastest-audio', 'fastest-audio-remux', 'fast-audio', 'fast', 'slow', 'slow-audio', 'slowest'].includes(speed)) {
await html5ifyAndLoad(speed);
}
setWorking(i18n.t('Converting to supported format'));
await html5ifyAndLoad(customOutDir, filePath, speed, hasVideo, hasAudio);
} catch (err) {
errorToast(i18n.t('Failed to convert file. Try a different conversion'));
console.error('Failed to html5ify file', err);
} finally {
setWorking();
}
}, [createDummyVideo, customOutDir, filePath, html5ifyAndLoad]);
}, [customOutDir, filePath, html5ifyAndLoad, hasVideo, hasAudio, rememberConvertToSupportedFormat, working]);
const onVideoError = useCallback(async () => {
const { error } = videoRef.current;
@ -1696,25 +1676,35 @@ const App = memo(() => {
console.error('onVideoError', error.message, error.code);
function showToast() {
console.log('Trying to create dummy');
if (!hideAllNotifications) toast.fire({ icon: 'info', text: 'This file is not natively supported. Creating a preview file...' });
}
const PIPELINE_ERROR_DECODE = 3; // To reproduce: "RX100VII PCM audio timecode.MP4" or see https://github.com/mifi/lossless-cut/issues/804
const MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
if ([MEDIA_ERR_SRC_NOT_SUPPORTED, PIPELINE_ERROR_DECODE].includes(error.code) && !usingDummyVideo) {
if (hasVideo) {
try {
const PIPELINE_ERROR_DECODE = 3; // To reproduce: "RX100VII PCM audio timecode.MP4" or see https://github.com/mifi/lossless-cut/issues/804
const MEDIA_ERR_SRC_NOT_SUPPORTED = 4; // Test: issue-668-3.20.1.m2ts - NOTE: DEMUXER_ERROR_COULD_NOT_OPEN is also 4
if ([MEDIA_ERR_SRC_NOT_SUPPORTED, PIPELINE_ERROR_DECODE].includes(error.code) && !usingPreviewFile && filePath && !working) {
if (isDurationValid(await getDuration(filePath))) {
showToast();
await tryCreateDummyVideo();
console.log('Trying to create preview');
if (!hideAllNotifications) toast.fire({ icon: 'info', text: 'This file is not natively supported. Creating a preview file...' });
try {
setWorking(i18n.t('Converting to supported format'));
if (hasVideo) {
// "fastest" is the most likely type not to fail for video (but it is muted).
await html5ifyAndLoad(customOutDir, filePath, rememberConvertToSupportedFormat || 'fastest', hasVideo, hasAudio);
} else if (hasAudio) {
// For audio do a fast re-encode
await html5ifyAndLoad(customOutDir, filePath, rememberConvertToSupportedFormat || 'fastest-audio', hasVideo, hasAudio);
}
} catch (err) {
console.error(err);
showPlaybackFailedMessage();
} finally {
setWorking();
}
}
} else if (hasAudio) {
showToast();
await html5ifyAndLoad('fastest-audio');
}
} catch (err) {
handleError(err);
}
}, [tryCreateDummyVideo, fileUri, usingDummyVideo, hasVideo, hasAudio, html5ifyAndLoad, hideAllNotifications, filePath]);
}, [fileUri, usingPreviewFile, hasVideo, hasAudio, html5ifyAndLoad, hideAllNotifications, customOutDir, filePath, working, rememberConvertToSupportedFormat]);
useEffect(() => {
function showOpenAndMergeDialog2() {
@ -1762,6 +1752,7 @@ const App = memo(() => {
}
async function batchConvertFriendlyFormat() {
if (working) return;
const title = i18n.t('Select files to batch convert to supported format');
const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'], title, message: title });
if (canceled || filePaths.length < 1) return;
@ -1769,7 +1760,7 @@ const App = memo(() => {
const failedFiles = [];
let i = 0;
const speed = await askForHtml5ifySpeed(['fastest-audio', 'fastest-audio-remux', 'fast-audio', 'fast', 'slow', 'slow-audio', 'slowest']);
const { selectedOption: speed } = await askForHtml5ifySpeed({ allowedOptions: ['fastest-audio', 'fastest-audio-remux', 'fast-audio', 'fast', 'slow', 'slow-audio', 'slowest'] });
if (!speed) return;
try {
@ -1820,12 +1811,12 @@ const App = memo(() => {
}
async function fixInvalidDuration2() {
if (!checkFileOpened()) return;
if (!checkFileOpened() || working) return;
try {
setWorking(i18n.t('Fixing file duration'));
const path = await fixInvalidDuration({ fileFormat, customOutDir });
load({ filePath: path, customOutDir });
toast.fire({ icon: 'info', text: i18n.t('Duration has been fixed') });
await load({ filePath: path, customOutDir });
} catch (err) {
errorToast(i18n.t('Failed to fix file duration'));
console.error('Failed to fix file duration', err);
@ -1886,8 +1877,8 @@ const App = memo(() => {
};
}, [
mergeFiles, outputDir, filePath, customOutDir, startTimeOffset, userHtml5ifyCurrentFile,
createDummyVideo, extractAllStreams, userOpenFiles, openSendReportDialogWithState,
loadEdlFile, cutSegments, apparentCutSegments, edlFilePath, toggleHelp, toggleSettings, assureOutDirAccess, html5ifyAndLoad, html5ify,
extractAllStreams, userOpenFiles, openSendReportDialogWithState,
loadEdlFile, cutSegments, apparentCutSegments, edlFilePath, toggleHelp, toggleSettings, assureOutDirAccess, html5ifyAndLoad, working, html5ify,
loadCutSegments, duration, checkFileOpened, load, fileFormat, reorderSegsByStartTime, closeFile, clearSegments, fixInvalidDuration, invertAllCutSegments,
]);

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Checkbox } from 'evergreen-ui';
import { Checkbox, RadioGroup, Paragraph } from 'evergreen-ui';
import Swal from 'sweetalert2';
import i18n from 'i18next';
import { Trans } from 'react-i18next';
@ -43,7 +43,7 @@ export async function promptTimeOffset(inputValue) {
}
export async function askForHtml5ifySpeed(allowedOptions) {
export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initialOption }) {
const availOptions = {
fastest: i18n.t('Fastest: Low playback speed (no audio)'),
'fastest-audio': i18n.t('Fastest: Low playback speed'),
@ -59,18 +59,43 @@ export async function askForHtml5ifySpeed(allowedOptions) {
inputOptions[allowedOption] = availOptions[allowedOption];
});
const { value } = await Swal.fire({
let selectedOption = inputOptions[initialOption] ? initialOption : Object.keys(inputOptions)[0];
let rememberChoice = !!initialOption;
const Html = () => {
const [option, setOption] = useState(selectedOption);
const [remember, setRemember] = useState(rememberChoice);
function onOptionChange(e) {
selectedOption = e.target.value;
setOption(selectedOption);
}
function onRememberChange(e) {
rememberChoice = e.target.checked;
setRemember(rememberChoice);
}
return (
<div style={{ textAlign: 'left' }}>
<Paragraph>{i18n.t('These options will let you convert files to a format that is supported by the player. You can try different options and see which works with your file. Note that the conversion is for preview only. When you run an export, the output will still be lossless with full quality')}</Paragraph>
<RadioGroup
options={Object.entries(inputOptions).map(([value, label]) => ({ label, value }))}
value={option}
onChange={onOptionChange}
/>
{showRemember && <Checkbox checked={remember} onChange={onRememberChange} label={i18n.t('Use this for all files until LosslessCut is restarted?')} />}
</div>
);
};
const { value: response } = await ReactSwal.fire({
title: i18n.t('Convert to supported format'),
input: 'radio',
inputValue: 'fastest',
text: i18n.t('These options will let you convert files to a format that is supported by the player. You can try different options and see which works with your file. Note that the conversion is for preview only. When you run an export, the output will still be lossless with full quality'),
html: <Html />,
showCancelButton: true,
customClass: { input: 'swal2-losslesscut-radio' },
inputOptions,
inputValidator: (v) => !v && i18n.t('You need to choose something!'),
});
return value;
return {
selectedOption: response && selectedOption,
remember: rememberChoice,
};
}
export async function askForYouTubeInput() {

@ -2,26 +2,28 @@ import Swal from 'sweetalert2';
import i18n from 'i18next';
import lodashTemplate from 'lodash/template';
const path = window.require('path');
const { dirname, parse: parsePath, join, basename, extname, isAbsolute, resolve } = window.require('path');
const fs = window.require('fs-extra');
const open = window.require('open');
const os = window.require('os');
const { readdir } = fs;
export function getOutDir(customOutDir, filePath) {
if (customOutDir) return customOutDir;
if (filePath) return path.dirname(filePath);
if (filePath) return dirname(filePath);
return undefined;
}
export function getFileBaseName(filePath) {
if (!filePath) return undefined;
const parsed = path.parse(filePath);
const parsed = parsePath(filePath);
return parsed.name;
}
export function getOutPath(customOutDir, filePath, nameSuffix) {
if (!filePath) return undefined;
return path.join(getOutDir(customOutDir, filePath), `${getFileBaseName(filePath)}-${nameSuffix}`);
return join(getOutDir(customOutDir, filePath), `${getFileBaseName(filePath)}-${nameSuffix}`);
}
export async function havePermissionToReadFile(filePath) {
@ -51,7 +53,7 @@ export async function checkDirWriteAccess(dirPath) {
}
export async function pathExists(pathIn) {
return fs.exists(pathIn);
return fs.pathExists(pathIn);
}
export async function dirExists(dirPath) {
@ -80,16 +82,26 @@ export const toast = Swal.mixin({
},
});
export const errorToast = (title) => toast.fire({
export const errorToast = (text) => toast.fire({
icon: 'error',
title,
text,
});
export function handleError(error) {
console.error('handleError', error);
export function handleError(arg1, arg2) {
console.error('handleError', arg1, arg2);
let msg;
let errorMsg;
if (typeof arg1 === 'string') msg = arg1;
else if (typeof arg2 === 'string') msg = arg2;
if (arg1 instanceof Error) errorMsg = arg1.message;
if (arg2 instanceof Error) errorMsg = arg2.message;
toast.fire({
icon: 'error',
text: i18n.t('An error has occurred. {{message}}', { message: error && typeof error.message === 'string' ? error.message.substr(0, 300) : '' }),
title: msg || i18n.t('An error has occurred.'),
text: errorMsg ? errorMsg.substr(0, 300) : undefined,
});
}
@ -99,14 +111,9 @@ export const openDirToast = async ({ dirPath, ...props }) => {
if (value) open(dirPath);
};
export async function showFfmpegFail(err) {
console.error(err);
return errorToast(`${i18n.t('Failed to run ffmpeg:')} ${err.stack}`);
}
export function setFileNameTitle(filePath) {
const appName = 'LosslessCut';
document.title = filePath ? `${appName} - ${path.basename(filePath)}` : appName;
document.title = filePath ? `${appName} - ${basename(filePath)}` : appName;
}
export function filenamify(name) {
@ -155,7 +162,7 @@ export function getExtensionForFormat(format) {
}
export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }) {
return isCustomFormatSelected ? `.${getExtensionForFormat(outFormat)}` : path.extname(filePath);
return isCustomFormatSelected ? `.${getExtensionForFormat(outFormat)}` : extname(filePath);
}
// This is used as a fallback and so it has to always generate unique file names
@ -180,4 +187,39 @@ export function generateSegFileName({ template, inputFileNameWithoutExt, segSuff
export const hasDuplicates = (arr) => new Set(arr).size !== arr.length;
// Need to resolve relative paths from the command line https://github.com/mifi/lossless-cut/issues/639
export const resolvePathIfNeeded = (inPath) => (path.isAbsolute(inPath) ? inPath : path.resolve(inPath));
export const resolvePathIfNeeded = (inPath) => (isAbsolute(inPath) ? inPath : resolve(inPath));
export const html5ifiedPrefix = 'html5ified-';
export const html5dummySuffix = 'dummy';
export async function findExistingHtml5FriendlyFile(fp, cod) {
// The order is the priority we will search:
const suffixes = ['slowest', 'slow-audio', 'slow', 'fast-audio', 'fast', 'fastest-audio', 'fastest-audio-remux', html5dummySuffix];
const prefix = `${getFileBaseName(fp)}-${html5ifiedPrefix}`;
const outDir = getOutDir(cod, fp);
const dirEntries = await readdir(outDir);
const html5ifiedDirEntries = dirEntries.filter((entry) => entry.startsWith(prefix));
let matches = [];
suffixes.forEach((suffix) => {
const entryWithSuffix = html5ifiedDirEntries.find((entry) => new RegExp(`${suffix}\\..*$`).test(entry.replace(prefix, '')));
if (entryWithSuffix) matches = [...matches, { entry: entryWithSuffix, suffix }];
});
const nonMatches = html5ifiedDirEntries.filter((entry) => !matches.some((m) => m.entry === entry)).map((entry) => ({ entry }));
// Allow for non-suffix matches too, e.g. user has a custom html5ified- file but with none of the suffixes above (but last priority)
matches = [...matches, ...nonMatches];
// console.log(matches);
if (matches.length < 1) return undefined;
const { suffix, entry } = matches[0];
return {
path: join(outDir, entry),
usingDummyVideo: ['fastest-audio', 'fastest-audio-remux', html5dummySuffix].includes(suffix),
};
}

Loading…
Cancel
Save