From cac788c441bbf37b07980685c8ca87ef6b9d89aa Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 31 Dec 2022 16:20:04 +0800 Subject: [PATCH] show warning when ffmpeg vtag issue also improve cut finished dialog closes #1406 closes #280 respect "hide all notifications" more --- src/App.jsx | 34 +++++++++++++++++++--------------- src/dialogs.jsx | 38 +++++++++++++++++++++++++++++++++++--- src/ffmpeg.js | 6 +++++- src/util.js | 12 ++++-------- 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index d3b1d491..150bf52b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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]); diff --git a/src/dialogs.jsx b/src/dialogs.jsx index c1e3efc6..9e09a55c 100644 --- a/src/dialogs.jsx +++ b/src/dialogs.jsx @@ -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
{description &&

{description}

} - {docUrl &&

} + {docUrl &&

}
{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 = ( + <> +
+ {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.')} +
+ + {warnings.map((msg) => {msg})} + {notices.map((msg) => {msg})} + + + ); + await openDirToast({ filePath, html, width: '90%', position: 'center', timer: 15000 }); +} diff --git a/src/ffmpeg.js b/src/ffmpeg.js index a7bfc84d..08281d9c 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -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) { diff --git a/src/util.js b/src/util.js index 8ac3a5df..1e6b7f9f 100644 --- a/src/util.js +++ b/src/util.js @@ -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;