show warning when ffmpeg vtag issue

also improve cut finished dialog
closes #1406
closes #280
respect "hide all notifications" more
pull/1413/head
Mikael Finstad 3 years ago
parent 95e6a5d198
commit cac788c441
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -53,7 +53,7 @@ import {
getStreamFps, isCuttingStart, isCuttingEnd,
readFileMeta, getSmarterOutFormat, renderThumbnails as ffmpegRenderThumbnails,
extractStreams, runStartupCheck, setCustomFfPath as ffmpegSetCustomFfPath,
isIphoneHevc, tryMapChaptersToEdl, blackDetect, silenceDetect, detectSceneChanges as ffmpegDetectSceneChanges,
isIphoneHevc, isProblematicAvc1, tryMapChaptersToEdl, blackDetect, silenceDetect, detectSceneChanges as ffmpegDetectSceneChanges,
getDuration, getTimecodeFromStreams, createChaptersFromSegments, extractSubtitleTrack,
getFfmpegPath, RefuseOverwriteError, readFrames, mapTimesToSegments,
} from './ffmpeg';
@ -62,14 +62,14 @@ import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, askForEdlIm
import { formatYouTube, getFrameCountRaw } from './edlFormats';
import {
getOutPath, getSuffixedOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, getFileDir,
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer,
checkDirWriteAccess, dirExists, isMasBuild, isStoreBuild, dragPreventer,
filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
deleteFiles, isOutOfSpaceError, shuffleArray,
} from './util';
import { formatDuration } from './util/duration';
import { adjustRate } from './util/rate-calculator';
import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages, showRefuseToOverwrite, showParametersDialog } from './dialogs';
import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages, showRefuseToOverwrite, showParametersDialog, openDirToast, openCutFinishedToast } from './dialogs';
import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
import { createSegment, getCleanCutSegments, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, convertSegmentsToChapters, hasAnySegmentOverlap, combineOverlappingSegments as combineOverlappingSegments2, isDurationValid } from './segments';
@ -1110,7 +1110,7 @@ const App = memo(() => {
const metadataFromPath = paths[0];
await concatFiles({ paths, outPath, outDir, outFormat, metadataFromPath, includeAllStreams, streams, ffmpegExperimental, onProgress: setCutProgress, preserveMovData, movFastStart, preserveMetadataOnMerge, chapters: chaptersFromSegments, appendFfmpegCommandLog });
if (clearBatchFilesAfterConcat) closeBatch();
openDirToast({ icon: 'success', filePath: outPath, text: i18n.t('Files merged!') });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: i18n.t('Files merged!') });
} catch (err) {
if (isOutOfSpaceError(err)) {
showDiskFull();
@ -1122,7 +1122,7 @@ const App = memo(() => {
setWorking();
setCutProgress();
}
}, [setWorking, ensureAccessibleDirectories, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch]);
}, [setWorking, ensureAccessibleDirectories, segmentsToChapters, concatFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, closeBatch, hideAllNotifications]);
const cleanupFilesDialog = useCallback(async () => {
if (!isFileOpened) return;
@ -1343,22 +1343,26 @@ const App = memo(() => {
});
}
const msgs = [i18n.t('Done! Note: cutpoints may be inaccurate. Make sure you test the output files in your desired player/editor before you delete the source. If output does not look right, see the HELP page.')];
const notices = [];
const warnings = [];
// https://github.com/mifi/lossless-cut/issues/329
if (isIphoneHevc(mainFileFormatData, mainStreams)) msgs.push(i18n.t('There is a known issue with cutting iPhone HEVC videos. The output file may not work in all players.'));
if (isIphoneHevc(mainFileFormatData, mainStreams)) warnings.push(i18n.t('There is a known issue with cutting iPhone HEVC videos. The output file may not work in all players.'));
// https://github.com/mifi/lossless-cut/issues/280
if (!ffmpegExperimental && isProblematicAvc1(fileFormat, mainStreams)) warnings.push(i18n.t('There is a known problem with this file type, and the output might not be playable. You can work around this problem by enabling the "Experimental flag" under Settings.'));
if (exportExtraStreams) {
try {
await extractStreams({ filePath, customOutDir, streams: nonCopiedExtraStreams, enableOverwriteOutput });
msgs.push(i18n.t('Unprocessable streams were exported as separate files.'));
notices.push(i18n.t('Unprocessable streams were exported as separate files.'));
} catch (err) {
console.error('Extra stream export failed', err);
}
}
const revealPath = concatOutPath || outFiles[0];
if (!hideAllNotifications) openDirToast({ filePath: revealPath, text: msgs.join(' '), timer: 15000 });
if (!hideAllNotifications) openCutFinishedToast({ filePath: revealPath, warnings, notices });
} catch (err) {
if (err instanceof RefuseOverwriteError) {
showRefuseToOverwrite();
@ -1406,7 +1410,7 @@ const App = memo(() => {
? await captureFramesFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, enableTransferTimestamps, numFrames: 1 })
: await captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, enableTransferTimestamps });
if (!hideAllNotifications) openDirToast({ filePath: outPath, text: `${i18n.t('Screenshot captured to:')} ${outPath}` });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: `${i18n.t('Screenshot captured to:')} ${outPath}` });
} catch (err) {
console.error(err);
errorToast(i18n.t('Failed to capture frame'));
@ -1423,7 +1427,7 @@ const App = memo(() => {
try {
setWorking(i18n.t('Extracting frames'));
const outPath = await captureFramesFfmpeg({ customOutDir, filePath, fromTime: start, captureFormat, enableTransferTimestamps, numFrames });
if (!hideAllNotifications) openDirToast({ filePath: outPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
} catch (err) {
handleError(err);
} finally {
@ -1739,7 +1743,7 @@ const App = memo(() => {
setWorking(i18n.t('Extracting all streams'));
setStreamsSelectorShown(false);
const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainStreams, enableOverwriteOutput });
openDirToast({ filePath: extractedPaths[0], text: i18n.t('All streams have been extracted as separate files') });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: extractedPaths[0], text: i18n.t('All streams have been extracted as separate files') });
} catch (err) {
if (err instanceof RefuseOverwriteError) {
showRefuseToOverwrite();
@ -1750,7 +1754,7 @@ const App = memo(() => {
} finally {
setWorking();
}
}, [customOutDir, enableOverwriteOutput, filePath, mainStreams, setWorking]);
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainStreams, setWorking]);
const detectSegments = useCallback(async ({ name, workingText, errorText, fn }) => {
if (!filePath) return;
@ -2182,7 +2186,7 @@ const App = memo(() => {
setWorking(i18n.t('Extracting track'));
// setStreamsSelectorShown(false);
const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput });
openDirToast({ filePath: extractedPaths[0], text: i18n.t('Track has been extracted') });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: extractedPaths[0], text: i18n.t('Track has been extracted') });
} catch (err) {
if (err instanceof RefuseOverwriteError) {
showRefuseToOverwrite();
@ -2193,7 +2197,7 @@ const App = memo(() => {
} finally {
setWorking();
}
}, [customOutDir, enableOverwriteOutput, filePath, mainStreams, setWorking]);
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainStreams, setWorking]);
const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]);

@ -1,5 +1,5 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Button, TextInputField, Checkbox, RadioGroup, Paragraph, LinkIcon } from 'evergreen-ui';
import { UnorderedList, ListItem, WarningSignIcon, InfoSignIcon, Button, TextInputField, Checkbox, RadioGroup, Paragraph, LinkIcon } from 'evergreen-ui';
import Swal from 'sweetalert2';
import i18n from 'i18next';
import { Trans } from 'react-i18next';
@ -9,11 +9,12 @@ import { tomorrow as style } from 'react-syntax-highlighter/dist/esm/styles/hljs
import JSON5 from 'json5';
import { parseDuration, formatDuration } from './util/duration';
import { swalToastOptions, toast } from './util';
import { parseYouTube } from './edlFormats';
import CopyClipboardButton from './components/CopyClipboardButton';
const { dialog, app } = window.require('@electron/remote');
const electron = window.require('electron');
const { shell } = window.require('electron');
const ReactSwal = withReactContent(Swal);
@ -429,7 +430,7 @@ const ParametersInput = ({ description, parameters: parametersIn, onChange, onSu
<div style={{ textAlign: 'left' }}>
{description && <p>{description}</p>}
{docUrl && <p><Button iconBefore={LinkIcon} onClick={() => electron.shell.openExternal(docUrl)}>Read more</Button></p>}
{docUrl && <p><Button iconBefore={LinkIcon} onClick={() => shell.openExternal(docUrl)}>Read more</Button></p>}
<form onSubmit={handleSubmit}>
{Object.entries(parametersIn).map(([key, parameter], i) => (
@ -594,3 +595,34 @@ export function showJson5Dialog({ title, json }) {
html,
});
}
export async function openDirToast({ filePath, text, html, ...props }) {
const swal = text ? toast : ReactSwal;
const { value } = await swal.fire({
...swalToastOptions,
showConfirmButton: true,
confirmButtonText: i18n.t('Show'),
showCancelButton: true,
cancelButtonText: i18n.t('Close'),
text,
html,
...props,
});
if (value) shell.showItemInFolder(filePath);
}
export async function openCutFinishedToast({ filePath, warnings, notices }) {
const html = (
<>
<div>
{i18n.t('Done! Note: cutpoints may be inaccurate. Make sure you test the output files in your desired player/editor before you delete the source. If output does not look right, see the HELP page.')}
</div>
<UnorderedList>
{warnings.map((msg) => <ListItem key={msg} icon={WarningSignIcon} iconColor="warning">{msg}</ListItem>)}
{notices.map((msg) => <ListItem key={msg} icon={InfoSignIcon} iconColor="info">{msg}</ListItem>)}
</UnorderedList>
</>
);
await openDirToast({ filePath, html, width: '90%', position: 'center', timer: 15000 });
}

@ -4,7 +4,7 @@ import moment from 'moment';
import i18n from 'i18next';
import Timecode from 'smpte-timecode';
import { pcmAudioCodecs, getMapStreamsArgs } from './util/streams';
import { pcmAudioCodecs, getMapStreamsArgs, isMov } from './util/streams';
import { getSuffixedOutPath, getExtensionForFormat, isWindows, isMac, platform, arch } from './util';
import { isDurationValid } from './segments';
@ -708,6 +708,10 @@ export function isIphoneHevc(format, streams) {
return (makeTag === 'Apple' && modelTag.startsWith('iPhone'));
}
export function isProblematicAvc1(outFormat, streams) {
return isMov(outFormat) && streams.some((s) => s.codec_name === 'h264' && s.codec_tag === '0x31637661');
}
export function getStreamFps(stream) {
const match = typeof stream.avg_frame_rate === 'string' && stream.avg_frame_rate.match(/^([0-9]+)\/([0-9]+)$/);
if (stream.codec_type === 'video' && match) {

@ -90,7 +90,7 @@ export async function transferTimestamps(inPath, outPath, offset = 0) {
}
}
export const toast = Swal.mixin({
export const swalToastOptions = {
toast: true,
position: 'top',
showConfirmButton: false,
@ -101,7 +101,9 @@ export const toast = Swal.mixin({
self.addEventListener('mouseenter', Swal.stopTimer);
self.addEventListener('mouseleave', Swal.resumeTimer);
},
});
};
export const toast = Swal.mixin(swalToastOptions);
export const errorToast = (text) => toast.fire({
icon: 'error',
@ -126,12 +128,6 @@ export function handleError(arg1, arg2) {
});
}
export const openDirToast = async ({ filePath, ...props }) => {
const { value } = await toast.fire({ icon: 'success', timer: 5000, showConfirmButton: true, confirmButtonText: i18n.t('Show'), showCancelButton: true, cancelButtonText: i18n.t('Close'), ...props });
if (value) shell.showItemInFolder(filePath);
};
export function setFileNameTitle(filePath) {
const appName = 'LosslessCut';
document.title = filePath ? `${appName} - ${basename(filePath)}` : appName;

Loading…
Cancel
Save