From 69f600a0c082772fd10018399ac7db1c51e22a3a Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Wed, 4 Sep 2024 00:24:42 +0200 Subject: [PATCH] make it more explicit when changing mp4 to mov and show a notification #1075 --- issues.md | 4 +-- src/renderer/src/App.tsx | 10 ++++-- src/renderer/src/components/ConcatDialog.tsx | 4 +-- src/renderer/src/ffmpeg.ts | 33 ++++++++++---------- src/renderer/src/util.ts | 2 +- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/issues.md b/issues.md index 1f1b1518..f6f9f89a 100644 --- a/issues.md +++ b/issues.md @@ -87,9 +87,9 @@ Smart cut is experimental, so don't expect too much. But if you're having proble - If Smart cut gives you repeated (duplicate) segments, you can try to enable the Export Option "Shift all start times". - Sometimes it helps to convert (remux) your videos [to mp4 first](https://github.com/mifi/lossless-cut/discussions/1292#discussioncomment-10425084) (e.g. from mkv) using LosslessCut, before smart cutting them. -## My file changes from MP4 to MOV +## MP4/MOV issues -Some MP4 files ffmpeg is not able to export as MP4 and therefore needs to use MOV instead. Unfortunately I don't know any way to fix this. +Some MP4 files FFmpeg is not able to export as MP4 and MOV needs to be selected instead. Unfortunately I don't know any way to fix this. Sometimes certain players are not able to play back certain exported `.mov` files ([Adobe Premiere](https://github.com/mifi/lossless-cut/issues/1075#issuecomment-2327459890) 👀). You can try to rename the exported MOV file extension to `.mp4` and see if it helps. Or vice versa, rename an exported MP4 file to `.mov`. ## Output file name is missing characters diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index dc0bfeb0..f168fa85 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -58,6 +58,7 @@ import { isIphoneHevc, isProblematicAvc1, tryMapChaptersToEdl, getDuration, getTimecodeFromStreams, createChaptersFromSegments, RefuseOverwriteError, extractSubtitleTrackToSegments, + mapRecommendedDefaultFormat, } from './ffmpeg'; import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, isAudioDefinitelyNotSupported, willPlayerProperlyHandleVideo, doesPlayerSupportHevcPlayback, getSubtitleStreams, getVideoTrackForStreamIndex, getAudioTrackForStreamIndex, enableVideoTrack, enableAudioTrack } from './util/streams'; import { exportEdlFile, readEdlFile, loadLlcProject, askForEdlImport } from './edlStore'; @@ -1305,7 +1306,6 @@ function App() { // console.log('file meta read', fileMeta); const fileFormatNew = await getDefaultOutFormat({ filePath: fp, fileMeta }); - if (!fileFormatNew) throw new Error('Unable to determine file format'); const timecode = autoLoadTimecode ? getTimecodeFromStreams(fileMeta.streams) : undefined; @@ -1373,8 +1373,14 @@ function App() { if (!haveVideoStream) setWaveformMode('big-waveform'); setMainFileMeta({ streams: fileMeta.streams, formatData: fileMeta.format, chapters: fileMeta.chapters }); setCopyStreamIdsForPath(fp, () => copyStreamIdsForPathNew); - setFileFormat(outFormatLocked || fileFormatNew); setDetectedFileFormat(fileFormatNew); + if (outFormatLocked) { + setFileFormat(outFormatLocked); + } else { + const recommendedDefaultFormat = mapRecommendedDefaultFormat({ sourceFormat: fileFormatNew, streams: fileMeta.streams }); + if (recommendedDefaultFormat.message) showNotification({ icon: 'info', text: recommendedDefaultFormat.message }); + setFileFormat(recommendedDefaultFormat.format); + } // only show one toast, or else we will only show the last one if (existingHtml5FriendlyFile) { diff --git a/src/renderer/src/components/ConcatDialog.tsx b/src/renderer/src/components/ConcatDialog.tsx index 6fea86f7..a270bb7c 100644 --- a/src/renderer/src/components/ConcatDialog.tsx +++ b/src/renderer/src/components/ConcatDialog.tsx @@ -8,7 +8,7 @@ import invariant from 'tiny-invariant'; import Checkbox from './Checkbox'; import { ReactSwal } from '../swal'; -import { readFileMeta, getDefaultOutFormat } from '../ffmpeg'; +import { readFileMeta, getDefaultOutFormat, mapRecommendedDefaultFormat } from '../ffmpeg'; import useFileFormatState from '../hooks/useFileFormatState'; import OutputFormatSelect from './OutputFormatSelect'; import useUserSettings from '../hooks/useUserSettings'; @@ -69,8 +69,8 @@ function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFi const fileFormatNew = await getDefaultOutFormat({ filePath: firstPath, fileMeta: fileMetaNew }); if (aborted) return; setFileMeta(fileMetaNew); - setFileFormat(fileFormatNew); setDetectedFileFormat(fileFormatNew); + setFileFormat(mapRecommendedDefaultFormat({ sourceFormat: fileFormatNew, streams: fileMetaNew.streams }).format); setUniqueSuffix(Date.now()); })().catch(console.error); diff --git a/src/renderer/src/ffmpeg.ts b/src/renderer/src/ffmpeg.ts index f6541c4a..bfd34493 100644 --- a/src/renderer/src/ffmpeg.ts +++ b/src/renderer/src/ffmpeg.ts @@ -236,27 +236,28 @@ export async function createChaptersFromSegments({ segmentPaths, chapterNames }: } /** - * ffmpeg only supports encoding certain formats, and some of the detected input - * formats are not the same as the muxer name used for encoding. + * Some of the detected input formats are not the same as the muxer name used for encoding. * Therefore we have to map between detected input format and encode format * See also ffmpeg -formats */ -function mapDefaultFormat({ streams, requestedFormat }: { streams: FFprobeStream[], requestedFormat: string | undefined }) { - if (requestedFormat === 'mp4') { - // Only MOV supports these codecs, so default to MOV instead https://github.com/mifi/lossless-cut/issues/948 - // eslint-disable-next-line unicorn/no-lonely-if - if (streams.some((stream) => pcmAudioCodecs.includes(stream.codec_name))) { - return 'mov'; - } - } - - // see sample.aac +function mapInputToOutputFormat(requestedFormat: string | undefined) { + // see file aac raw adts.aac if (requestedFormat === 'aac') return 'adts'; return requestedFormat; } -async function determineOutputFormat(ffprobeFormatsStr: string | undefined, filePath: string) { +export function mapRecommendedDefaultFormat({ streams, sourceFormat }: { streams: FFprobeStream[], sourceFormat: string | undefined }) { + // Certain codecs cannot be muxed by ffmpeg into mp4, but in MOV they can + // so we default to MOV instead in those cases https://github.com/mifi/lossless-cut/issues/948 + if (sourceFormat === 'mp4' && streams.some((stream) => pcmAudioCodecs.includes(stream.codec_name))) { + return { format: 'mov', message: i18n.t('This file contains an audio track that FFmpeg is unable to mux into the MP4 format, so MOV has been auto-selected as the default output format.') }; + } + + return { format: sourceFormat }; +} + +async function determineSourceFileFormat(ffprobeFormatsStr: string | undefined, filePath: string) { const ffprobeFormats = (ffprobeFormatsStr || '').split(',').map((str) => str.trim()).filter(Boolean); const [firstFfprobeFormat] = ffprobeFormats; @@ -342,10 +343,10 @@ async function determineOutputFormat(ffprobeFormatsStr: string | undefined, file } } -export async function getDefaultOutFormat({ filePath, fileMeta: { format, streams } }: { filePath: string, fileMeta: { format: Pick, streams: FFprobeStream[] } }) { - const assumedFormat = await determineOutputFormat(format.format_name, filePath); +export async function getDefaultOutFormat({ filePath, fileMeta: { format } }: { filePath: string, fileMeta: { format: Pick } }) { + const assumedFormat = await determineSourceFileFormat(format.format_name, filePath); - return mapDefaultFormat({ streams, requestedFormat: assumedFormat }); + return mapInputToOutputFormat(assumedFormat); } export async function readFileMeta(filePath: string) { diff --git a/src/renderer/src/util.ts b/src/renderer/src/util.ts index 723c590a..44bd435d 100644 --- a/src/renderer/src/util.ts +++ b/src/renderer/src/util.ts @@ -221,7 +221,7 @@ export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePat // https://github.com/mifi/lossless-cut/issues/1075#issuecomment-1072084286 const hasMovIncorrectExtension = outFormat === 'mov' && inputExt.toLowerCase() !== '.mov'; - // OK, just keep the current extension. Because most players will not care about the extension + // OK, just keep the current extension. Because most other players will not care about the extension if (!hasMovIncorrectExtension) return inputExt; }