diff --git a/src/HelpSheet.jsx b/src/HelpSheet.jsx index 89ce451c..1d487853 100644 --- a/src/HelpSheet.jsx +++ b/src/HelpSheet.jsx @@ -34,8 +34,17 @@ const HelpSheet = ({ visible, onTogglePress, renderSettings }) => (

Hover mouse over buttons to see which function they have.

-

Settings

- {renderSettings()} + + + + + + + + + {renderSettings()} + +
SettingsCurrent setting
)} diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 39bff1e7..f33ea19f 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -361,24 +361,21 @@ function getPreferredCodecFormat(codec, type) { } // https://stackoverflow.com/questions/32922226/extract-every-audio-and-subtitles-from-a-video-with-ffmpeg -async function extractAllStreams({ customOutDir, filePath }) { - const { streams } = await getAllStreams(filePath); - console.log('streams', streams); - - const outStreams = streams.map((s, i) => ({ - i, +async function extractStreams({ filePath, customOutDir, streams }) { + const outStreams = streams.map((s) => ({ + index: s.index, codec: s.codec_name || s.codec_tag_string || s.codec_type, type: s.codec_type, format: getPreferredCodecFormat(s.codec_name, s.codec_type), })) - .filter(it => it && it.format); + .filter(it => it && it.format && it.index != null); // console.log(outStreams); const streamArgs = flatMap(outStreams, ({ - i, codec, type, format: { format, ext }, + index, codec, type, format: { format, ext }, }) => [ - '-map', `0:${i}`, '-c', 'copy', '-f', format, '-y', getOutPath(customOutDir, filePath, `${i}-${type}-${codec}.${ext}`), + '-map', `0:${index}`, '-c', 'copy', '-f', format, '-y', getOutPath(customOutDir, filePath, `stream-${index}-${type}-${codec}.${ext}`), ]); const ffmpegArgs = [ @@ -448,7 +445,7 @@ module.exports = { html5ifyDummy, mergeAnyFiles, autoMergeSegments, - extractAllStreams, + extractStreams, renderFrame, getAllStreams, defaultProcessedCodecTypes, diff --git a/src/renderer.jsx b/src/renderer.jsx index aed978bc..f3cfe2f4 100644 --- a/src/renderer.jsx +++ b/src/renderer.jsx @@ -80,6 +80,7 @@ function doesPlayerSupportFile(streams) { const queue = new PQueue({ concurrency: 1 }); const App = memo(() => { + // Per project state const [framePath, setFramePath] = useState(); const [html5FriendlyPath, setHtml5FriendlyPath] = useState(); const [working, setWorking] = useState(false); @@ -106,7 +107,7 @@ const App = memo(() => { const [streamsSelectorShown, setStreamsSelectorShown] = useState(false); const [zoom, setZoom] = useState(1); - // Global state + // Global state & preferences const [captureFormat, setCaptureFormat] = useState('jpeg'); const [customOutDir, setCustomOutDir] = useState(); const [keyframeCut, setKeyframeCut] = useState(true); @@ -115,6 +116,7 @@ const App = memo(() => { const [timecodeShowFrames, setTimecodeShowFrames] = useState(false); const [mifiLink, setMifiLink] = useState(); const [invertCutSegments, setInvertCutSegments] = useState(false); + const [autoExportExtraStreams, setAutoExportExtraStreams] = useState(true); const videoRef = useRef(); const timelineWrapperRef = useRef(); @@ -429,6 +431,14 @@ const App = memo(() => { const copyAnyAudioTrack = mainStreams.some(stream => isCopyingStreamId(filePath, stream.index) && stream.codec_type === 'audio'); + // Streams that are not copy enabled by default + const extraStreams = mainStreams + .filter((stream) => !defaultProcessedCodecTypes.includes(stream.codec_type)); + + // Extra streams that the user has not selected for copy + const nonCopiedExtraStreams = extraStreams + .filter((stream) => !isCopyingStreamId(filePath, stream.index)); + const copyStreamIds = Object.entries(copyStreamIdsByFile).map(([path, streamIdsMap]) => ({ path, streamIds: Object.keys(streamIdsMap).filter(index => streamIdsMap[index]), @@ -618,7 +628,18 @@ const App = memo(() => { }); } - toast.fire({ timer: 5000, icon: 'success', title: `Export completed! Output file(s) can be found at: ${getOutDir(customOutDir, filePath)}. You can change the output directory in settings` }); + const exportExtraStreams = autoExportExtraStreams && nonCopiedExtraStreams.length > 0; + if (exportExtraStreams) { + try { + await ffmpeg.extractStreams({ + filePath, customOutDir, streams: nonCopiedExtraStreams, + }); + } catch (err) { + console.error('Extra stream export failed', err); + } + } + + toast.fire({ timer: 5000, icon: 'success', title: `Export completed! Output file(s) can be found at: ${getOutDir(customOutDir, filePath)}.${exportExtraStreams ? ' Extra unprocessable stream(s) exported as separate files.' : ''}` }); } catch (err) { console.error('stdout:', err.stdout); console.error('stderr:', err.stderr); @@ -634,7 +655,7 @@ const App = memo(() => { } }, [ effectiveRotation, apparentCutSegments, invertCutSegments, inverseCutSegments, - working, duration, filePath, keyframeCut, detectedFileFormat, + working, duration, filePath, keyframeCut, detectedFileFormat, extraStreams, autoMerge, customOutDir, fileFormat, haveInvalidSegs, copyStreamIds, numStreamsToCopy, ]); @@ -676,7 +697,6 @@ const App = memo(() => { return ret; }, [getHtml5ifiedPath]); - const load = useCallback(async (fp, html5FriendlyPathRequested) => { console.log('Load', { fp, html5FriendlyPathRequested }); if (working) { @@ -792,7 +812,7 @@ const App = memo(() => { try { setWorking(true); - await ffmpeg.extractAllStreams({ customOutDir, filePath }); + await ffmpeg.extractStreams({ customOutDir, filePath, streams: mainStreams }); toast.fire({ icon: 'success', title: `All streams can be found as separate files at: ${getOutDir(customOutDir, filePath)}` }); } catch (err) { errorToast('Failed to extract all streams'); @@ -1034,100 +1054,113 @@ const App = memo(() => { function renderSettings() { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Output format (default autodetected){renderOutFmt()}
Output directory - -
{customOutDir}
-
Auto merge segments to one file after export? - -
keyframe cut mode - -
- Discard (cut away) or keep selected segments from video when exporting - - -
- Discard audio? - - -
- Snapshot capture format - - {renderCaptureFormatButton()} -
In timecode show - -
+ + + Output format (default autodetected) + {renderOutFmt()} + + + + Output directory + + +
{customOutDir}
+ + + + + Auto merge segments to one file after export? + + + + + + + keyframe cut mode + + + + + + + + Discard (cut away) or keep selected segments from video when exporting + + + + + + + + + Discard audio? + + + + + + + + + Extract unprocessable tracks to separate files?
+ (data tracks such as GoPro GPS, telemetry etc.) + + + + + + + + + Snapshot capture format + + + {renderCaptureFormatButton()} + + + + + In timecode show + + + + +
); }