-icon
-auto convert to dummy if cannot playback
pull/276/head
Mikael Finstad 2020-02-20 00:56:33 +08:00
rodzic acbefef259
commit 228adc844c
1 zmienionych plików z 102 dodań i 81 usunięć

Wyświetl plik

@ -1,8 +1,7 @@
import React, { memo, useEffect, useState, useCallback, useRef } from 'react'; import React, { memo, useEffect, useState, useCallback, useRef, Fragment } from 'react';
import { IoIosHelpCircle, IoIosCamera } from 'react-icons/io'; import { IoIosHelpCircle, IoIosCamera } from 'react-icons/io';
import { FaPlus, FaMinus, FaAngleLeft, FaAngleRight, FaTrashAlt, FaVolumeMute, FaVolumeUp, FaYinYang, FaFileExport } from 'react-icons/fa'; import { FaPlus, FaMinus, FaAngleLeft, FaAngleRight, FaTrashAlt, FaVolumeMute, FaVolumeUp, FaYinYang, FaFileExport } from 'react-icons/fa';
import { MdRotate90DegreesCcw, MdCallSplit, MdCallMerge } from 'react-icons/md'; import { MdRotate90DegreesCcw, MdCallSplit, MdCallMerge } from 'react-icons/md';
import { GiYinYang } from 'react-icons/gi';
import { FiScissors } from 'react-icons/fi'; import { FiScissors } from 'react-icons/fi';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
@ -504,6 +503,31 @@ const App = memo(() => {
if (!zoomed) seekRel((e.deltaX + e.deltaY) / 15); if (!zoomed) seekRel((e.deltaX + e.deltaY) / 15);
} }
function showUnsupportedFileMessage() {
toast.fire({ timer: 10000, icon: 'warning', title: 'This video is not natively supported', text: 'This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!' });
}
const createDummyVideo = useCallback(async (fp) => {
const html5ifiedDummyPathDummy = getOutPath(customOutDir, fp, 'html5ified-dummy.mkv');
await ffmpeg.html5ifyDummy(fp, html5ifiedDummyPathDummy);
setDummyVideoPath(html5ifiedDummyPathDummy);
setHtml5FriendlyPath();
showUnsupportedFileMessage();
}, [customOutDir]);
const tryCreateDummyVideo = useCallback(async () => {
try {
if (working) return;
setWorking(true);
await createDummyVideo(filePath);
} catch (err) {
console.error(err);
errorToast('Failed to playback this file. Try to convert to friendly format from the menu');
} finally {
setWorking(false);
}
}, [createDummyVideo, filePath, working]);
const playCommand = useCallback(() => { const playCommand = useCallback(() => {
const video = videoRef.current; const video = videoRef.current;
if (playing) return video.pause(); if (playing) return video.pause();
@ -511,10 +535,11 @@ const App = memo(() => {
return video.play().catch((err) => { return video.play().catch((err) => {
console.error(err); console.error(err);
if (err.name === 'NotSupportedError') { if (err.name === 'NotSupportedError') {
toast.fire({ icon: 'error', title: 'This format/codec is not supported. Try to convert it to a friendly format/codec in the player from the "File" menu.', timer: 10000 }); console.log('NotSupportedError, trying to create dummy');
tryCreateDummyVideo(filePath);
} }
}); });
}, [playing]); }, [playing, filePath, tryCreateDummyVideo]);
const deleteSource = useCallback(async () => { const deleteSource = useCallback(async () => {
if (!filePath) return; if (!filePath) return;
@ -613,10 +638,6 @@ const App = memo(() => {
autoMerge, customOutDir, fileFormat, haveInvalidSegs, copyStreamIds, numStreamsToCopy, autoMerge, customOutDir, fileFormat, haveInvalidSegs, copyStreamIds, numStreamsToCopy,
]); ]);
function showUnsupportedFileMessage() {
toast.fire({ timer: 10000, icon: 'warning', title: 'This video is not natively supported', text: 'This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!' });
}
// TODO use ffmpeg to capture frame // TODO use ffmpeg to capture frame
const capture = useCallback(async () => { const capture = useCallback(async () => {
if (!filePath) return; if (!filePath) return;
@ -645,14 +666,6 @@ const App = memo(() => {
const getHtml5ifiedPath = useCallback((fp, type) => getOutPath(customOutDir, fp, `html5ified-${type}.mp4`), [customOutDir]); const getHtml5ifiedPath = useCallback((fp, type) => getOutPath(customOutDir, fp, `html5ified-${type}.mp4`), [customOutDir]);
const createDummyVideo = useCallback(async (fp) => {
const html5ifiedDummyPathDummy = getOutPath(customOutDir, fp, 'html5ified-dummy.mkv');
await ffmpeg.html5ifyDummy(fp, html5ifiedDummyPathDummy);
setDummyVideoPath(html5ifiedDummyPathDummy);
setHtml5FriendlyPath();
showUnsupportedFileMessage();
}, [customOutDir]);
const checkExistingHtml5FriendlyFile = useCallback(async (fp, speed) => { const checkExistingHtml5FriendlyFile = useCallback(async (fp, speed) => {
const existing = getHtml5ifiedPath(fp, speed); const existing = getHtml5ifiedPath(fp, speed);
const ret = existing && await exists(existing); const ret = existing && await exists(existing);
@ -667,7 +680,7 @@ const App = memo(() => {
const load = useCallback(async (fp, html5FriendlyPathRequested) => { const load = useCallback(async (fp, html5FriendlyPathRequested) => {
console.log('Load', { fp, html5FriendlyPathRequested }); console.log('Load', { fp, html5FriendlyPathRequested });
if (working) { if (working) {
errorToast('I\'m busy'); errorToast('Tried to load file while busy');
return; return;
} }
@ -1135,20 +1148,19 @@ const App = memo(() => {
const CutIcon = areWeCutting ? FiScissors : FaFileExport; const CutIcon = areWeCutting ? FiScissors : FaFileExport;
function renderInvertCutButton() { function renderInvertCutButton() {
const KeepOrDiscardIcon = invertCutSegments ? GiYinYang : FaYinYang;
return ( return (
<div style={{ position: 'relative', width: 26, height: 26, marginLeft: 5 }}> <div style={{ marginLeft: 5 }}>
<AnimatePresence> <motion.div
<motion.div style={{ position: 'absolute' }} key={invertCutSegments} initial={{ opacity: 0, scale: 0 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0 }}> animate={{ rotateX: invertCutSegments ? 0 : 180, width: 26, height: 26 }}
<KeepOrDiscardIcon transition={{ duration: 0.3 }}
size={26} >
role="button" <FaYinYang
title={invertCutSegments ? 'Discard selected segments' : 'Keep selected segments'} size={26}
onClick={withBlur(() => setInvertCutSegments(v => !v))} role="button"
/> title={invertCutSegments ? 'Discard selected segments' : 'Keep selected segments'}
</motion.div> onClick={withBlur(() => setInvertCutSegments(v => !v))}
</AnimatePresence> />
</motion.div>
</div> </div>
); );
} }
@ -1172,64 +1184,73 @@ const App = memo(() => {
return ( return (
<div> <div>
<div style={{ background: '#6b6b6b', height: topBarHeight, display: 'flex', alignItems: 'center', padding: '0 5px', justifyContent: 'space-between' }}> <div style={{ background: '#6b6b6b', height: topBarHeight, display: 'flex', alignItems: 'center', padding: '0 5px', justifyContent: 'space-between' }}>
<SideSheet {filePath && (
containerProps={{ style: { maxWidth: '100%' } }} <Fragment>
position={Position.LEFT} <SideSheet
isShown={streamsSelectorShown} containerProps={{ style: { maxWidth: '100%' } }}
onCloseComplete={() => setStreamsSelectorShown(false)} position={Position.LEFT}
> isShown={streamsSelectorShown}
<StreamsSelector onCloseComplete={() => setStreamsSelectorShown(false)}
mainFilePath={filePath} >
externalFiles={externalStreamFiles} <StreamsSelector
setExternalFiles={setExternalStreamFiles} mainFilePath={filePath}
showAddStreamSourceDialog={showAddStreamSourceDialog} externalFiles={externalStreamFiles}
streams={mainStreams} setExternalFiles={setExternalStreamFiles}
isCopyingStreamId={isCopyingStreamId} showAddStreamSourceDialog={showAddStreamSourceDialog}
toggleCopyStreamId={toggleCopyStreamId} streams={mainStreams}
setCopyStreamIdsForPath={setCopyStreamIdsForPath} isCopyingStreamId={isCopyingStreamId}
onExtractAllStreamsPress={onExtractAllStreamsPress} toggleCopyStreamId={toggleCopyStreamId}
/> setCopyStreamIdsForPath={setCopyStreamIdsForPath}
</SideSheet> onExtractAllStreamsPress={onExtractAllStreamsPress}
<Button height={20} iconBefore="list" onClick={withBlur(() => setStreamsSelectorShown(true))}> />
Tracks ({numStreamsToCopy}/{numStreamsTotal}) </SideSheet>
</Button> <Button height={20} iconBefore="list" onClick={withBlur(() => setStreamsSelectorShown(true))}>
Tracks ({numStreamsToCopy}/{numStreamsTotal})
</Button>
</Fragment>
)}
<div style={{ flexGrow: 1 }} /> <div style={{ flexGrow: 1 }} />
<button {filePath && (
type="button" <Fragment>
onClick={withBlur(setOutputDir)}
title={customOutDir}
>
{`Out path ${customOutDir ? 'set' : 'unset'}`}
</button>
{renderOutFmt({ width: 60 })} <button
type="button"
onClick={withBlur(setOutputDir)}
title={customOutDir}
>
{`Out path ${customOutDir ? 'set' : 'unset'}`}
</button>
<button {renderOutFmt({ width: 60 })}
style={{ opacity: cutSegments.length < 2 ? 0.4 : undefined }}
type="button"
title={autoMerge ? 'Auto merge segments to one file after export' : 'Export to separate files'}
onClick={withBlur(toggleAutoMerge)}
>
<AutoMergeIcon /> {autoMerge ? 'Merge cuts' : 'Separate files'}
</button>
<button <button
type="button" style={{ opacity: cutSegments.length < 2 ? 0.4 : undefined }}
title={`Cut mode ${keyframeCut ? 'nearest keyframe cut' : 'normal cut'}`} type="button"
onClick={withBlur(toggleKeyframeCut)} title={autoMerge ? 'Auto merge segments to one file after export' : 'Export to separate files'}
> onClick={withBlur(toggleAutoMerge)}
{keyframeCut ? 'Keyframe cut' : 'Normal cut'} >
</button> <AutoMergeIcon /> {autoMerge ? 'Merge cuts' : 'Separate files'}
</button>
<button <button
type="button" type="button"
title={`Discard audio? Current: ${copyAnyAudioTrack ? 'keep audio tracks' : 'Discard audio tracks'}`} title={`Cut mode ${keyframeCut ? 'nearest keyframe cut' : 'normal cut'}`}
onClick={withBlur(toggleStripAudio)} onClick={withBlur(toggleKeyframeCut)}
> >
{copyAnyAudioTrack ? 'Keep audio' : 'Discard audio'} {keyframeCut ? 'Keyframe cut' : 'Normal cut'}
</button> </button>
<button
type="button"
title={`Discard audio? Current: ${copyAnyAudioTrack ? 'keep audio tracks' : 'Discard audio tracks'}`}
onClick={withBlur(toggleStripAudio)}
>
{copyAnyAudioTrack ? 'Keep audio' : 'Discard audio'}
</button>
</Fragment>
)}
<IoIosHelpCircle size={24} role="button" onClick={toggleHelp} style={{ verticalAlign: 'middle', marginLeft: 5 }} /> <IoIosHelpCircle size={24} role="button" onClick={toggleHelp} style={{ verticalAlign: 'middle', marginLeft: 5 }} />
</div> </div>