improve error msg

and refactor
pull/2715/head
Mikael Finstad 6 months ago
parent 2601103eef
commit 260110d9c1
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -0,0 +1,4 @@
// eslint-disable-next-line import/prefer-default-export
export const parseFfprobeDuration = (durationStr: string | undefined) => (
durationStr != null ? parseFloat(durationStr) : undefined
);

@ -10,9 +10,11 @@ import { app, clipboard, nativeImage } from 'electron';
import { platform, arch, isWindows, isLinux } from './util.js';
import type { CaptureFormat, Waveform } from '../common/types.js';
import type { FFprobeFormat } from '../common/ffprobe.js';
import isDev from './isDev.js';
import logger from './logger.js';
import { parseFfmpegProgressLine } from './progress.js';
import { parseFfprobeDuration } from '../common/util.js';
// cannot use process.kill: https://github.com/sindresorhus/execa/issues/1177
const runningFfmpegs = new Set<{
@ -552,7 +554,7 @@ export async function captureFrameToFile({ timestamp, videoPath, outPath, qualit
}
async function readFormatData(filePath: string) {
async function readFormatData(filePath: string): Promise<FFprobeFormat> {
logger.info('readFormatData', filePath);
const { stdout } = await runFfprobe([
@ -562,7 +564,7 @@ async function readFormatData(filePath: string) {
}
export async function getDuration(filePath: string) {
return parseFloat((await readFormatData(filePath)).duration);
return parseFfprobeDuration((await readFormatData(filePath)).duration);
}
const enableLog = false;

@ -58,7 +58,7 @@ import {
readFileFfprobeMeta, getDefaultOutFormat,
setCustomFfPath as ffmpegSetCustomFfPath,
isIphoneHevc, isProblematicAvc1, tryMapChaptersToEdl,
getDuration, getTimecodeFromStreams, createChaptersFromSegments,
getTimecodeFromStreams, createChaptersFromSegments,
RefuseOverwriteError, extractSubtitleTrackToSegments,
mapRecommendedDefaultFormat,
getFfCommandLine,
@ -95,6 +95,8 @@ import type { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType,
import { goToTimecodeDirectArgsSchema, openFilesActionArgsSchema } from './types';
import type { CaptureFormat, KeyboardAction, ApiActionRequest } from '../../common/types.js';
import type { FFprobeChapter, FFprobeStream } from '../../common/ffprobe.js';
import { parseFfprobeDuration } from '../../common/util.js';
import useLoading from './hooks/useLoading';
import useVideo from './hooks/useVideo';
import useTimecode from './hooks/useTimecode';
@ -670,7 +672,7 @@ function App() {
}, [videoRef, setCommandedTime, setPlaybackRate, setPreviewFilePath, setUsingDummyVideo, setPlaying, playingRef, setPlaybackMode, cutSegmentsHistory, setDetectedFileFormat, setCopyStreamIdsByFile, setThumbnails, setSubtitlesByStreamId, setOutputPlaybackRateState]);
const showUnsupportedFileMessage = useCallback(() => {
const showNotNativelySupportedMessage = useCallback(() => {
showNotification({ timer: 13000, text: i18n.t('File is not natively supported. Preview playback may be slow and of low quality, but the final export will be lossless. You may convert the file from the menu for a better preview.') });
}, [showNotification]);
@ -1479,7 +1481,7 @@ function App() {
if (existingHtml5FriendlyFile && !existingHtml5FriendlyFile.usingDummyVideo) {
showPreviewFileLoadedMessage(basename(existingHtml5FriendlyFile.path));
} else if (needsAutoHtml5ify) {
showUnsupportedFileMessage();
showNotNativelySupportedMessage();
} else if (isAudioDefinitelyNotSupported(ffprobeMeta.streams)) {
showNotification({ icon: 'info', text: i18n.t('The audio track is not supported while previewing. You can convert to a supported format from the menu') });
} else if (!validDuration) {
@ -1495,7 +1497,7 @@ function App() {
resetState();
throw err;
}
}, [storeProjectInWorkingDir, setWorking, loadEdlFile, getEdlFilePath, enableAskForImportChapters, ensureAccessToSourceDir, loadCutSegments, autoLoadTimecode, enableNativeHevc, ensureWritableOutDir, customOutDir, resetState, clearSegColorCounter, setCopyStreamIdsForPath, setDetectedFileFormat, outFormatLocked, setUsingDummyVideo, setPreviewFilePath, html5ifyAndLoadWithPreferences, setFileFormat, showNotification, showPreviewFileLoadedMessage, showUnsupportedFileMessage]);
}, [storeProjectInWorkingDir, setWorking, loadEdlFile, getEdlFilePath, enableAskForImportChapters, ensureAccessToSourceDir, loadCutSegments, autoLoadTimecode, enableNativeHevc, ensureWritableOutDir, customOutDir, resetState, clearSegColorCounter, setCopyStreamIdsForPath, setDetectedFileFormat, outFormatLocked, setUsingDummyVideo, setPreviewFilePath, html5ifyAndLoadWithPreferences, setFileFormat, showNotification, showPreviewFileLoadedMessage, showNotNativelySupportedMessage]);
const toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
@ -2177,7 +2179,7 @@ function App() {
)
|| error.code === PIPELINE_ERROR_DECODE
)
&& !usingPreviewFile
&& !usingPreviewFile // if we are already using preview file, we shouldn't try to do it again
&& filePath
&& !(error.code === MEDIA_ERR_SRC_NOT_SUPPORTED && error.message?.startsWith('DEMUXER_ERROR_COULD_NOT_PARSE'))
) {
@ -2185,15 +2187,21 @@ function App() {
try {
setWorking({ text: i18n.t('Converting to supported format') });
console.log('Trying to create preview');
console.log('Trying to convert to supported format');
if (!isDurationValid(await getDuration(filePath))) throw new UserFacingError(i18n.t('Invalid duration'));
// A valid duration is needed to create a html5ified dummy (`fastest`).
if (!isDurationValid(parseFfprobeDuration(mainFileFormat?.duration))) {
throw new UserFacingError(i18n.t('Invalid duration'));
}
if (hasVideo || hasAudio) {
await html5ifyAndLoadWithPreferences(customOutDir, filePath, 'fastest', hasVideo, hasAudio);
showUnsupportedFileMessage();
showNotNativelySupportedMessage();
}
} catch (err) {
if (err instanceof UserFacingError) {
throw err;
}
console.error(err);
showPlaybackFailedMessage();
} finally {
@ -2205,7 +2213,7 @@ function App() {
} catch (err) {
toastError(err);
}
}, [videoRef, fileUri, usingPreviewFile, filePath, workingRef, setWorking, hasVideo, hasAudio, html5ifyAndLoadWithPreferences, customOutDir, showUnsupportedFileMessage]);
}, [videoRef, fileUri, usingPreviewFile, filePath, workingRef, setWorking, mainFileFormat?.duration, hasVideo, hasAudio, html5ifyAndLoadWithPreferences, customOutDir, showNotNativelySupportedMessage]);
const onVideoFocus = useCallback<FocusEventHandler<HTMLVideoElement>>((e) => {
// prevent video element from stealing focus in fullscreen mode https://github.com/mifi/lossless-cut/issues/543#issuecomment-1868167775

@ -219,7 +219,7 @@ export function tryMapChaptersToEdl(chapters: FFprobeChapter[]) {
export async function createChaptersFromSegments({ segmentPaths, chapterNames }: { segmentPaths: string[], chapterNames?: (string | undefined)[] | undefined }) {
if (!chapterNames) return undefined;
try {
const durations = await pMap(segmentPaths, (segmentPath) => getDuration(segmentPath), { concurrency: 3 });
const durations = await pMap(segmentPaths, async (segmentPath) => (await getDuration(segmentPath)) ?? 0, { concurrency: 3 });
let timeAt = 0;
return durations.map((duration, i) => {
const ret = { start: timeAt, end: timeAt + duration, name: chapterNames[i] };

@ -5,7 +5,7 @@ import pMap from 'p-map';
import invariant from 'tiny-invariant';
import i18n from 'i18next';
import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration, isMac } from '../util';
import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration, isMac, html5ifiedPrefix, html5dummySuffix } from '../util';
import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileFfprobeMeta, getExperimentalArgs, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat, RefuseOverwriteError, runFfmpeg } from '../ffmpeg';
import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams';
import { needsSmartCut, getCodecParams } from '../smartcut';
@ -132,7 +132,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
console.log('Merging files', { paths }, 'to', outPath);
const durations = await pMap(paths, getDuration, { concurrency: 1 });
const durations = await pMap(paths, async (path) => (await getDuration(path)) ?? 0, { concurrency: 1 });
const totalDuration = sum(durations);
let chaptersPath: string | undefined;
@ -682,9 +682,50 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
await concatFiles({ paths: segmentPaths, outDir, outPath: mergedOutFilePath, metadataFromPath, outFormat, includeAllStreams: true, streams, ffmpegExperimental, onProgress, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge });
}, [concatFiles, filePath, shouldSkipExistingFile]);
// This is just used to load something into the player with correct duration,
// so that the user can seek and then we render frames using ffmpeg & MediaSource
const html5ifyDummy = useCallback(async ({ filePath: filePathArg, outPath, onProgress }: {
filePath: string,
outPath: string,
onProgress: (p: number) => void,
}) => {
console.log('Making ffmpeg-assisted dummy file', { filePathArg, outPath });
const duration = await getDuration(filePathArg);
const ffmpegArgs = [
'-hide_banner',
// This is just a fast way of generating an empty dummy file
'-f', 'lavfi', '-i', 'anullsrc=channel_layout=stereo:sample_rate=44100',
'-t', String(duration),
'-acodec', 'flac',
'-y', outPath,
];
appendFfmpegCommandLog(ffmpegArgs);
const result = await runFfmpegWithProgress({ ffmpegArgs, duration, onProgress });
logStdoutStderr(result);
await transferTimestamps({ inPath: filePathArg, outPath, duration, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart });
}, [appendFfmpegCommandLog, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
const html5ify = useCallback(async ({ customOutDir, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }: {
customOutDir: string | undefined, filePath: string, speed: Html5ifyMode, hasAudio: boolean, hasVideo: boolean, onProgress: (p: number) => void,
customOutDir: string | undefined,
filePath: string,
speed: Html5ifyMode,
hasAudio: boolean,
hasVideo: boolean,
onProgress: (p: number) => void,
}) => {
console.log('html5ifyAndLoad', { speed, hasVideo, hasAudio });
if (speed === 'fastest') {
const path = getSuffixedOutPath({ customOutDir, filePath: filePathArg, nameSuffix: `${html5ifiedPrefix}${html5dummySuffix}.mkv` });
await html5ifyDummy({ filePath: filePathArg, outPath: path, onProgress });
return path;
}
const outPath = getHtml5ifiedPath(customOutDir, filePathArg, speed);
invariant(outPath != null);
@ -795,35 +836,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
invariant(outPath != null);
await transferTimestamps({ inPath: filePathArg, outPath, duration, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart });
return outPath;
}, [appendFfmpegCommandLog, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
// This is just used to load something into the player with correct duration,
// so that the user can seek and then we render frames using ffmpeg & MediaSource
const html5ifyDummy = useCallback(async ({ filePath: filePathArg, outPath, onProgress }: {
filePath: string,
outPath: string,
onProgress: (p: number) => void,
}) => {
console.log('Making ffmpeg-assisted dummy file', { filePathArg, outPath });
const duration = await getDuration(filePathArg);
const ffmpegArgs = [
'-hide_banner',
// This is just a fast way of generating an empty dummy file
'-f', 'lavfi', '-i', 'anullsrc=channel_layout=stereo:sample_rate=44100',
'-t', String(duration),
'-acodec', 'flac',
'-y', outPath,
];
appendFfmpegCommandLog(ffmpegArgs);
const result = await runFfmpegWithProgress({ ffmpegArgs, duration, onProgress });
logStdoutStderr(result);
await transferTimestamps({ inPath: filePathArg, outPath, duration, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart });
}, [appendFfmpegCommandLog, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
}, [appendFfmpegCommandLog, html5ifyDummy, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
// https://stackoverflow.com/questions/34118013/how-to-determine-webm-duration-using-ffprobe
const fixInvalidDuration = useCallback(async ({ fileFormat, customOutDir, onProgress }: {

@ -7,7 +7,6 @@ import type { Html5ifyMode } from '../../../common/types';
import { DirectoryAccessDeclinedError } from '../../errors';
import getSwal from '../swal';
import Checkbox from '../components/Checkbox';
import { getSuffixedOutPath, html5dummySuffix, html5ifiedPrefix } from '../util';
import type { SetWorking } from './useLoading';
import type { WithErrorHandling } from './useErrorHandling';
import type { FfmpegOperations } from './useFfmpegOperations';
@ -18,7 +17,7 @@ import { ButtonRow } from '../components/Dialog';
import { DialogButton } from '../components/Button';
export default function useHtml5ify({ filePath, hasVideo, hasAudio, workingRef, setWorking, ensureWritableOutDir, customOutDir, batchFiles, enableAutoHtml5ify, setProgress, html5ify, html5ifyDummy, withErrorHandling, showGenericDialog }: {
export default function useHtml5ify({ filePath, hasVideo, hasAudio, workingRef, setWorking, ensureWritableOutDir, customOutDir, batchFiles, enableAutoHtml5ify, setProgress, html5ify, withErrorHandling, showGenericDialog }: {
filePath: string | undefined,
hasVideo: boolean,
hasAudio: boolean,
@ -39,36 +38,17 @@ export default function useHtml5ify({ filePath, hasVideo, hasAudio, workingRef,
const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState<Html5ifyMode>();
const html5ifyAndLoad = useCallback(async (cod: string | undefined, fp: string, speed: Html5ifyMode, hv: boolean, ha: boolean) => {
const usesDummyVideo = speed === 'fastest';
console.log('html5ifyAndLoad', { speed, hasVideo: hv, hasAudio: ha, usesDummyVideo });
async function doHtml5ify() {
if (speed == null) return undefined;
if (speed === 'fastest') {
const path = getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${html5dummySuffix}.mkv` });
try {
setProgress(0);
await html5ifyDummy({ filePath: fp, outPath: path, onProgress: setProgress });
} finally {
setProgress(undefined);
}
return path;
}
try {
setProgress(0);
const path = await html5ify({ customOutDir: cod, filePath: fp, speed, hasAudio: ha, hasVideo: hv, onProgress: setProgress });
if (!path) return;
try {
const shouldIncludeVideo = !usesDummyVideo && hv;
return await html5ify({ customOutDir: cod, filePath: fp, speed, hasAudio: ha, hasVideo: shouldIncludeVideo, onProgress: setProgress });
} finally {
setProgress(undefined);
}
setPreviewFilePath(path);
setUsingDummyVideo(speed === 'fastest');
} finally {
setProgress(undefined);
}
const path = await doHtml5ify();
if (!path) return;
setPreviewFilePath(path);
setUsingDummyVideo(usesDummyVideo);
}, [html5ify, html5ifyDummy, setProgress]);
}, [html5ify, setProgress]);
const askForHtml5ifySpeed = useCallback(async ({ allowedOptions, showRemember, initialOption }: {
allowedOptions: Html5ifyMode[],
@ -211,13 +191,10 @@ export default function useHtml5ify({ filePath, hasVideo, hasAudio, workingRef,
try {
await withErrorHandling(async () => {
// eslint-disable-next-line no-restricted-syntax
for (const path of filePaths) {
try {
// eslint-disable-next-line no-await-in-loop
const newCustomOutDir = await ensureWritableOutDir({ inputPath: path, outDir: customOutDir });
// eslint-disable-next-line no-await-in-loop
await html5ify({ customOutDir: newCustomOutDir, filePath: path, speed, hasAudio: true, hasVideo: true, onProgress: setTotalProgress });
} catch (err2) {
if (err2 instanceof DirectoryAccessDeclinedError) return;

@ -256,7 +256,7 @@ export async function findExistingHtml5FriendlyFile(fp: string, cod: string | un
};
}
export function getHtml5ifiedPath(cod: string | undefined, fp: string, type: Html5ifyMode) {
export function getHtml5ifiedPath(cod: string | undefined, fp: string, type: Exclude<Html5ifyMode, 'fastest'>) {
// See also inside ffmpegHtml5ify
const ext = (isMac && ['slowest', 'slow', 'slow-audio'].includes(type)) ? 'mp4' : 'mkv';
return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` });

Loading…
Cancel
Save