From e707695a39cbe371ac76b406ef36a1ee9679ee4f Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 7 Apr 2020 22:03:33 +0800 Subject: [PATCH] Allow batch convert to friendly format #295 #172 Also improve conver to friendly format flow --- public/menu.js | 28 ++++---------- src/App.jsx | 100 ++++++++++++++++++++++++++++++++++++++++++++----- src/ffmpeg.js | 4 +- 3 files changed, 101 insertions(+), 31 deletions(-) diff --git a/public/menu.js b/public/menu.js index 14310ec8..745f7e56 100644 --- a/public/menu.js +++ b/public/menu.js @@ -43,27 +43,9 @@ module.exports = (app, mainWindow, newVersion) => { }, { type: 'separator' }, { - label: 'Convert to friendly format (fastest)', + label: 'Convert to friendly format', click() { - mainWindow.webContents.send('html5ify', 'fastest'); - }, - }, - { - label: 'Convert to friendly format (fast)', - click() { - mainWindow.webContents.send('html5ify', 'fast'); - }, - }, - { - label: 'Convert to friendly codec (slow)', - click() { - mainWindow.webContents.send('html5ify', 'slow'); - }, - }, - { - label: 'Convert to friendly codec (slowest, audio)', - click() { - mainWindow.webContents.send('html5ify', 'slow-audio'); + mainWindow.webContents.send('html5ify'); }, }, { type: 'separator' }, @@ -140,6 +122,12 @@ module.exports = (app, mainWindow, newVersion) => { mainWindow.webContents.send('show-merge-dialog', true); }, }, + { + label: 'Batch convert to friendly format', + click() { + mainWindow.webContents.send('batchConvertFriendlyFormat'); + }, + }, { label: 'Set custom start offset/timecode', click() { diff --git a/src/App.jsx b/src/App.jsx index 7f2a3a58..c5b9d6a5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1261,19 +1261,53 @@ const App = memo(() => { resetState(); } - async function html5ify(event, speed) { + async function askForHtml5ifySpeed(allowedOptions) { + const availOptions = { + fastest: i18n.t('Fastest: Low playback speed (no audio)'), + fast: i18n.t('Fast: Full quality remux, unlikely to work'), + slow: i18n.t('Slow: Low video quality encode (no audio)'), + 'slow-audio': i18n.t('Slowest: Low video/audio quality encode'), + }; + const inputOptions = {}; + allowedOptions.forEach((allowedOption) => { + inputOptions[allowedOption] = availOptions[allowedOption]; + }); + + const { value } = await Swal.fire({ + title: i18n.t('Convert to supported format'), + input: 'radio', + inputValue: 'fastest', + text: i18n.t('These options will let you convert files to a format that is supported by the player. You can try different options and see which works with your file. Note that this is for preview only. When you run cut operations, they will still be lossless with full quality'), + showCancelButton: true, + inputOptions, + inputValidator: (v) => !v && i18n.t('You need to choose something!'), + }); + + return value; + } + + async function html5ifyInternal({ customOutDir: cod, filePath: fp, speed, hasAudio: ha, hasVideo: hv }) { + const path = getHtml5ifiedPath(cod, fp, speed); + const encodeVideo = ['slow', 'slow-audio'].includes(speed) && hv; + const encodeAudio = speed === 'slow-audio' && ha; + await ffmpegHtml5ify(fp, path, encodeVideo, encodeAudio); + return path; + } + + async function html5ify() { if (!filePath) return; try { setWorking(true); - if (['fast', 'slow', 'slow-audio'].includes(speed)) { - const html5FriendlyPathRequested = getHtml5ifiedPath(customOutDir, filePath, speed); - const encodeVideo = ['slow', 'slow-audio'].includes(speed); - const encodeAudio = speed === 'slow-audio'; - await ffmpegHtml5ify(filePath, html5FriendlyPathRequested, encodeVideo, encodeAudio); - load({ filePath, html5FriendlyPathRequested, customOutDir }); - } else { + + const speed = await askForHtml5ifySpeed(['fastest', 'fast', 'slow', 'slow-audio']); + if (!speed) return; + + if (speed === 'fastest') { await createDummyVideo(customOutDir, filePath); + } else if (['fast', 'slow', 'slow-audio'].includes(speed)) { + const path = await html5ifyInternal({ customOutDir, filePath, speed, hasAudio, hasVideo }); + load({ filePath, html5FriendlyPathRequested: path, customOutDir }); } } catch (err) { errorToast(i18n.t('Failed to html5ify file')); @@ -1347,6 +1381,52 @@ const App = memo(() => { toggleSettings(); } + async function batchConvertFriendlyFormat() { + const title = i18n.t('Select files to batch convert to friendly format'); + const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'], title, message: title }); + if (canceled || filePaths.length < 1) return; + + const failedFiles = []; + let i = 0; + + const speed = await askForHtml5ifySpeed(['fast', 'slow', 'slow-audio']); + if (!speed) return; + + try { + setWorking(true); + setCutProgress(0); + + // eslint-disable-next-line no-restricted-syntax + for (const path of filePaths) { + try { + // eslint-disable-next-line no-await-in-loop + const { newCustomOutDir, cancel } = await assureOutDirAccess(path); + if (cancel) { + toast.fire({ title: i18n.t('Aborted') }); + return; + } + + // eslint-disable-next-line no-await-in-loop + await html5ifyInternal({ customOutDir: newCustomOutDir, filePath: path, speed, hasAudio: true, hasVideo: true }); + } catch (err2) { + console.error('Failed to html5ify', path, err2); + failedFiles.push(path); + } + + i += 1; + setCutProgress(i / filePaths.length); + } + + if (failedFiles.length > 0) toast.fire({ title: `${i18n.t('Failed to convert files:')} ${failedFiles.join(' ')}` }); + } catch (err) { + errorToast(i18n.t('Failed to html5ify')); + console.error('Failed to html5ify', err); + } finally { + setWorking(false); + setCutProgress(); + } + } + electron.ipcRenderer.on('file-opened', fileOpened); electron.ipcRenderer.on('close-file', closeFile); electron.ipcRenderer.on('html5ify', html5ify); @@ -1360,6 +1440,7 @@ const App = memo(() => { electron.ipcRenderer.on('openHelp', openHelp); electron.ipcRenderer.on('openSettings', openSettings); electron.ipcRenderer.on('openAbout', openAbout); + electron.ipcRenderer.on('batchConvertFriendlyFormat', batchConvertFriendlyFormat); return () => { electron.ipcRenderer.removeListener('file-opened', fileOpened); @@ -1375,11 +1456,12 @@ const App = memo(() => { electron.ipcRenderer.removeListener('openHelp', openHelp); electron.ipcRenderer.removeListener('openSettings', openSettings); electron.ipcRenderer.removeListener('openAbout', openAbout); + electron.ipcRenderer.removeListener('batchConvertFriendlyFormat', batchConvertFriendlyFormat); }; }, [ load, mergeFiles, outputDir, filePath, isFileOpened, customOutDir, startTimeOffset, getHtml5ifiedPath, createDummyVideo, resetState, extractAllStreams, userOpenFiles, cutSegmentsHistory, - loadEdlFile, cutSegments, edlFilePath, askBeforeClose, toggleHelp, toggleSettings, + loadEdlFile, cutSegments, edlFilePath, askBeforeClose, toggleHelp, toggleSettings, assureOutDirAccess, hasAudio, hasVideo, ]); async function showAddStreamSourceDialog() { diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 6c72f16d..1d266f5b 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -312,12 +312,12 @@ export async function html5ify(filePath, outPath, encodeVideo, encodeAudio) { let videoArgs; if (!encodeVideo) videoArgs = ['-vcodec', 'copy']; else if (os.platform() === 'darwin') { - videoArgs = ['-vf', 'scale=-2:400,format=yuv420p', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k']; + videoArgs = ['-vf', 'scale=-2:400,format=yuv420p', '-allow_sw', '1', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k']; } else { videoArgs = ['-vf', 'scale=-2:400,format=yuv420p', '-sws_flags', 'neighbor', '-vcodec', 'libx264', '-profile:v', 'baseline', '-x264opts', 'level=3.0', '-preset:v', 'ultrafast', '-crf', '28']; } - const audioArgs = encodeAudio ? ['-acodec', 'aac', '-b:a', '96k'] : ['-an']; + const audioArgs = encodeAudio ? ['-acodec', 'aac', '-ar', '44100', '-ac', '2', '-b:a', '96k'] : ['-an']; const ffmpegArgs = [ '-hide_banner',