improve export screen

show warning if cutting thumbnail stream
show warning if selected uncommon avoid_negative_ts
pull/1457/head
Mikael Finstad 3 years ago
parent a49a7a80c6
commit 3c46486cf3
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -57,7 +57,7 @@ import {
getDuration, getTimecodeFromStreams, createChaptersFromSegments, extractSubtitleTrack,
RefuseOverwriteError, readFrames, mapTimesToSegments, abortFfmpegs,
} from './ffmpeg';
import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, isAudioDefinitelyNotSupported, willPlayerProperlyHandleVideo, doesPlayerSupportHevcPlayback } from './util/streams';
import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, isAudioDefinitelyNotSupported, willPlayerProperlyHandleVideo, doesPlayerSupportHevcPlayback, isStreamThumbnail } from './util/streams';
import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, askForEdlImport } from './edlStore';
import { formatYouTube, getFrameCountRaw } from './edlFormats';
import {
@ -804,13 +804,14 @@ const App = memo(() => {
}
}, [setWorking, subtitleStreams, subtitlesByStreamId, filePath]);
const mainCopiedStreams = useMemo(() => mainStreams.filter((stream) => isCopyingStreamId(filePath, stream.index)), [filePath, isCopyingStreamId, mainStreams]);
const mainCopiedThumbnailStreams = useMemo(() => mainCopiedStreams.filter(isStreamThumbnail), [mainCopiedStreams]);
// Streams that are not copy enabled by default
const extraStreams = useMemo(() => mainStreams
.filter((stream) => !shouldCopyStreamByDefault(stream)), [mainStreams]);
const extraStreams = useMemo(() => mainStreams.filter((stream) => !shouldCopyStreamByDefault(stream)), [mainStreams]);
// Extra streams that the user has not selected for copy
const nonCopiedExtraStreams = useMemo(() => extraStreams
.filter((stream) => !isCopyingStreamId(filePath, stream.index)), [extraStreams, filePath, isCopyingStreamId]);
const nonCopiedExtraStreams = useMemo(() => extraStreams.filter((stream) => !isCopyingStreamId(filePath, stream.index)), [extraStreams, filePath, isCopyingStreamId]);
const exportExtraStreams = autoExportExtraStreams && nonCopiedExtraStreams.length > 0;
@ -819,14 +820,15 @@ const App = memo(() => {
streamIds: Object.keys(streamIdsMap).filter(index => streamIdsMap[index]).map((streamIdStr) => parseInt(streamIdStr, 10)),
})), [copyStreamIdsByFile]);
const numStreamsToCopy = copyFileStreams
.reduce((acc, { streamIds }) => acc + streamIds.length, 0);
// total number of streams to copy for ALL files
const numStreamsToCopy = copyFileStreams.reduce((acc, { streamIds }) => acc + streamIds.length, 0);
const allFilesMeta = useMemo(() => ({
...externalFilesMeta,
[filePath]: mainFileMeta,
}), [externalFilesMeta, filePath, mainFileMeta]);
// total number of streams for ALL files
const numStreamsTotal = flatMap(Object.values(allFilesMeta), ({ streams }) => streams).length;
const toggleStripAudio = useCallback(() => {
@ -1818,11 +1820,9 @@ const App = memo(() => {
if (workingRef.current) return;
try {
const enabledStreams = mainStreams.filter((stream) => isCopyingStreamId(filePath, stream.index));
setWorking(i18n.t('Extracting all streams'));
setStreamsSelectorShown(false);
const extractedPaths = await extractStreams({ customOutDir, filePath, streams: enabledStreams, enableOverwriteOutput });
const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput });
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) {
@ -1834,7 +1834,7 @@ const App = memo(() => {
} finally {
setWorking();
}
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, isCopyingStreamId, mainStreams, setWorking]);
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainCopiedStreams, setWorking]);
const detectSegments = useCallback(async ({ name, workingText, errorText, fn }) => {
if (!filePath) return;
@ -2702,7 +2702,7 @@ const App = memo(() => {
)}
</SideSheet>
<ExportConfirm filePath={filePath} areWeCutting={areWeCutting} nonFilteredSegments={nonFilteredSegments} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} />
<ExportConfirm filePath={filePath} areWeCutting={areWeCutting} nonFilteredSegments={nonFilteredSegments} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} />
<LastCommandsSheet
visible={lastCommandsVisible}

@ -44,6 +44,7 @@ const ExportConfirm = memo(({
areWeCutting, selectedSegments, segmentsToExport, willMerge, visible, onClosePress, onExportConfirm,
outFormat, renderOutFmt, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown, outSegTemplate,
setOutSegTemplate, generateOutSegFileNames, filePath, currentSegIndexSafe, getOutSegError, nonFilteredSegments,
mainCopiedThumbnailStreams,
}) => {
const { t } = useTranslation();
@ -52,6 +53,9 @@ const ExportConfirm = memo(({
const isMov = ffmpegIsMov(outFormat);
const isIpod = outFormat === 'ipod';
// some thumbnail streams (png,jpg etc) cannot always be cut correctly, so we warn if they try to.
const areWeCuttingProblematicStreams = areWeCutting && mainCopiedThumbnailStreams.length > 0;
const exportModeDescription = useMemo(() => ({
sesgments_to_chapters: t('Don\'t cut the file, but instead export an unmodified original which has chapters generated from segments'),
merge: t('Auto merge segments to one file after export'),
@ -130,13 +134,13 @@ const ExportConfirm = memo(({
<div style={boxStyle}>
<CrossIcon size={24} style={{ position: 'absolute', right: 0, top: 0, padding: 15, boxSizing: 'content-box', cursor: 'pointer' }} role="button" onClick={onClosePress} />
<h2 style={{ marginTop: 0 }}>{t('Export options')}</h2>
<ul>
<h2 style={{ marginTop: 0, marginBottom: '.5em' }}>{t('Export options')}</h2>
<ul style={{ margin: 0 }}>
{selectedSegments.length !== nonFilteredSegments.length && <li><FaRegCheckCircle size={12} style={{ marginRight: 3 }} />{t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: selectedSegments.length, nonFilteredSegments: nonFilteredSegments.length })}</li>}
<li>
{t('Merge {{segments}} cut segments to one file?', { segments: selectedSegments.length })} <ExportModeButton selectedSegments={selectedSegments} />
<HelpIcon onClick={onExportModeHelpPress} />
{effectiveExportMode === 'sesgments_to_chapters' && <WarningSignIcon verticalAlign="middle" color="warning" marginLeft=".3em" />}
{effectiveExportMode === 'sesgments_to_chapters' && <WarningSignIcon verticalAlign="middle" color="warning" marginLeft=".3em" title={i18n.t('Chapters only')} />}
</li>
<li>
{t('Output container format:')} {renderOutFmt({ height: 20, maxWidth: 150 })}
@ -145,6 +149,8 @@ const ExportConfirm = memo(({
<li>
<Trans>Input has {{ numStreamsTotal }} tracks - <HighlightedText style={{ cursor: 'pointer' }} onClick={() => setStreamsSelectorShown(true)}>Keeping {{ numStreamsToCopy }} tracks</HighlightedText></Trans>
<HelpIcon onClick={onTracksHelpPress} />
{areWeCuttingProblematicStreams && <WarningSignIcon verticalAlign="middle" color="warning" marginLeft=".3em" />}
{areWeCuttingProblematicStreams && <div style={warningStyle}><Trans>Warning: Cutting thumbnail tracks is known to cause problems. Consider disabling track {{ trackNumber: mainCopiedThumbnailStreams[0].index + 1 }}.</Trans></div>}
</li>
<li>
{t('Save output to path:')} <span role="button" onClick={changeOutDir} style={outDirStyle}>{outputDir}</span>
@ -156,10 +162,10 @@ const ExportConfirm = memo(({
)}
</ul>
<h3>{t('Advanced options')}</h3>
<h3 style={{ marginBottom: '.5em' }}>{t('Advanced options')}</h3>
{willMerge && (
<ul>
<ul style={{ marginTop: 0, marginBottom: '1em' }}>
<li>
{t('Create chapters from merged segments? (slow)')} <Button height={20} onClick={toggleSegmentsToChapters}>{segmentsToChapters ? t('Yes') : t('No')}</Button>
<HelpIcon onClick={onSegmentsToChaptersHelpPress} />
@ -171,14 +177,15 @@ const ExportConfirm = memo(({
</ul>
)}
<p>{t('Depending on your specific file/player, you may have to try different options for best results.')}</p>
<p style={{ margin: '.5em 0' }}>{t('Depending on your specific file/player, you may have to try different options for best results.')}</p>
<ul>
<ul style={{ margin: 0 }}>
{areWeCutting && (
<>
<li>
{t('Smart cut (experimental):')} <Button height={20} onClick={() => setEnableSmartCut((v) => !v)}>{enableSmartCut ? t('Yes') : t('No')}</Button>
<HelpIcon onClick={onSmartCutHelpPress} />
{enableSmartCut && <WarningSignIcon verticalAlign="middle" color="warning" marginLeft=".3em" title={i18n.t('Experimental functionality has been activated!')} />}
</li>
{!enableSmartCut && (
<li>
@ -204,14 +211,15 @@ const ExportConfirm = memo(({
{!enableSmartCut && (
<li>
{t('Shift timestamps (avoid_negative_ts)')}
&quot;avoid_negative_ts&quot;
<Select height={20} value={avoidNegativeTs} onChange={(e) => setAvoidNegativeTs(e.target.value)} style={{ marginLeft: 5 }}>
<option value="auto">auto</option>
<option value="make_zero">make_zero</option>
<option value="make_non_negative">make_non_negative</option>
<option value="auto">auto</option>
<option value="disabled">disabled</option>
</Select>
<HelpIcon onClick={onAvoidNegativeTsHelpPress} />
{!['make_zero', 'auto'].includes(avoidNegativeTs) && <div style={warningStyle}>{t('It\'s generally recommended to set this to one of: {{values}}', { values: '"auto", "make_zero"' })}</div>}
</li>
)}
</ul>

@ -1,6 +1,6 @@
import React, { memo, useState, useMemo, useCallback } from 'react';
import { FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa';
import { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa';
import { GoFileBinary } from 'react-icons/go';
import { FiEdit, FiCheck, FiTrash } from 'react-icons/fi';
import { MdSubtitles } from 'react-icons/md';
@ -13,7 +13,7 @@ import { askForMetadataKey, showJson5Dialog } from './dialogs';
import { formatDuration } from './util/duration';
import { getStreamFps } from './ffmpeg';
import { deleteDispositionValue } from './util';
import { getActiveDisposition } from './util/streams';
import { getActiveDisposition, attachedPicDisposition } from './util/streams';
const activeColor = '#429777';
@ -197,8 +197,13 @@ const Stream = memo(({ dispositionByStreamId, setDispositionByStreamId, filePath
Icon = copyStream ? FaVolumeUp : FaVolumeMute;
codecTypeHuman = t('audio');
} else if (stream.codec_type === 'video') {
Icon = copyStream ? FaVideo : FaVideoSlash;
codecTypeHuman = t('video');
if (effectiveDisposition === attachedPicDisposition) {
Icon = copyStream ? FaImage : FaBan;
codecTypeHuman = t('thumbnail');
} else {
Icon = copyStream ? FaVideo : FaVideoSlash;
codecTypeHuman = t('video');
}
} else if (stream.codec_type === 'subtitle') {
Icon = copyStream ? MdSubtitles : FaBan;
codecTypeHuman = t('subtitle');

@ -402,7 +402,7 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
if (!needsSmartCut) await checkOverwrite(smartCutMainPartOutPath);
// for smart cut we need to use keyframe cut here
// for smart cut we need to use keyframe cut here, and no avoid_negative_ts
await cutSingle({
cutFrom: encodeCutTo, cutTo, chaptersPath, outPath: smartCutMainPartOutPath, copyFileStreams: copyFileStreamsFiltered, keyframeCut: true, avoidNegativeTs: false, videoDuration, rotation, allFilesMeta, outFormat, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, customTagsByFile, customTagsByStreamId, dispositionByStreamId, videoTimebase, onProgress: onCutProgress,
});

@ -207,8 +207,10 @@ export function shouldCopyStreamByDefault(stream) {
return true;
}
export const attachedPicDisposition = 'attached_pic';
export function isStreamThumbnail(stream) {
return stream && stream.disposition && stream.disposition.attached_pic === 1;
return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1;
}
export const getAudioStreams = (streams) => streams.filter(stream => stream.codec_type === 'audio');

Loading…
Cancel
Save