diff --git a/src/main/ffmpeg.ts b/src/main/ffmpeg.ts index 1223266e..630f68ba 100644 --- a/src/main/ffmpeg.ts +++ b/src/main/ffmpeg.ts @@ -7,8 +7,8 @@ import { Readable } from 'node:stream'; // eslint-disable-next-line import/no-extraneous-dependencies import { app } from 'electron'; -import { platform, arch, isWindows, isMac, isLinux } from './util.js'; -import { CaptureFormat, Html5ifyMode, Waveform } from '../../types.js'; +import { platform, arch, isWindows, isLinux } from './util.js'; +import { CaptureFormat, Waveform } from '../../types.js'; import isDev from './isDev.js'; import logger from './logger.js'; import { parseFfmpegProgressLine } from './progress.js'; @@ -472,115 +472,6 @@ export async function getDuration(filePath: string) { return parseFloat((await readFormatData(filePath)).duration); } -export async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }: { - outPath: string, filePath: string, speed: Html5ifyMode, hasAudio: boolean, hasVideo: boolean, onProgress: (p: number) => void, -}) { - let audio; - if (hasAudio) { - if (speed === 'slowest') audio = 'hq'; - else if (['slow-audio', 'fast-audio'].includes(speed)) audio = 'lq'; - else if (['fast-audio-remux'].includes(speed)) audio = 'copy'; - } - - let video; - if (hasVideo) { - if (speed === 'slowest') video = 'hq'; - else if (['slow-audio', 'slow'].includes(speed)) video = 'lq'; - else video = 'copy'; - } - - logger.info('Making HTML5 friendly version', { filePathArg, outPath, speed, video, audio }); - - let videoArgs; - let audioArgs; - - // h264/aac_at: No licensing when using HW encoder (Video/Audio Toolbox on Mac) - // https://github.com/mifi/lossless-cut/issues/372#issuecomment-810766512 - - const targetHeight = 400; - - switch (video) { - case 'hq': { - // eslint-disable-next-line unicorn/prefer-ternary - if (isMac) { - videoArgs = ['-vf', 'format=yuv420p', '-allow_sw', '1', '-vcodec', 'h264', '-b:v', '15M']; - } else { - // AV1 is very slow - // videoArgs = ['-vf', 'format=yuv420p', '-sws_flags', 'neighbor', '-vcodec', 'libaom-av1', '-crf', '30', '-cpu-used', '8']; - // Theora is a bit faster but not that much - // videoArgs = ['-vf', '-c:v', 'libtheora', '-qscale:v', '1']; - // videoArgs = ['-vf', 'format=yuv420p', '-c:v', 'libvpx-vp9', '-crf', '30', '-b:v', '0', '-row-mt', '1']; - // x264 can only be used in GPL projects - videoArgs = ['-vf', 'format=yuv420p', '-c:v', 'libx264', '-profile:v', 'high', '-preset:v', 'slow', '-crf', '17']; - } - break; - } - case 'lq': { - // eslint-disable-next-line unicorn/prefer-ternary - if (isMac) { - videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-allow_sw', '1', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k']; - } else { - // videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-sws_flags', 'neighbor', '-c:v', 'libtheora', '-qscale:v', '1']; - // x264 can only be used in GPL projects - videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-sws_flags', 'neighbor', '-c:v', 'libx264', '-profile:v', 'baseline', '-x264opts', 'level=3.0', '-preset:v', 'ultrafast', '-crf', '28']; - } - break; - } - case 'copy': { - videoArgs = ['-vcodec', 'copy']; - break; - } - default: { - videoArgs = ['-vn']; - } - } - - switch (audio) { - case 'hq': { - // eslint-disable-next-line unicorn/prefer-ternary - if (isMac) { - audioArgs = ['-acodec', 'aac_at', '-b:a', '192k']; - } else { - audioArgs = ['-acodec', 'flac']; - } - break; - } - case 'lq': { - // eslint-disable-next-line unicorn/prefer-ternary - if (isMac) { - audioArgs = ['-acodec', 'aac_at', '-ar', '44100', '-ac', '2', '-b:a', '96k']; - } else { - audioArgs = ['-acodec', 'flac', '-ar', '11025', '-ac', '2']; - } - break; - } - case 'copy': { - audioArgs = ['-acodec', 'copy']; - break; - } - default: { - audioArgs = ['-an']; - } - } - - const ffmpegArgs = [ - '-hide_banner', - - '-i', filePathArg, - ...videoArgs, - ...audioArgs, - '-sn', - '-y', outPath, - ]; - - const duration = await getDuration(filePathArg); - const process = runFfmpegProcess(ffmpegArgs); - if (duration) handleProgress(process, duration, onProgress); - - const { stdout } = await process; - logger.info(stdout.toString('utf8')); -} - export function readOneJpegFrame({ path, seekTo, videoStreamIndex }: { path: string, seekTo: number, videoStreamIndex: number }) { const args = [ '-hide_banner', '-loglevel', 'error', diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 951c39f4..0464619c 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -674,9 +674,9 @@ function App() { } }, [batchFiles, customOutDir, ensureWritableOutDir, html5ify, setWorking, workingRef]); - const getConvertToSupportedFormat = useCallback((fallback) => rememberConvertToSupportedFormat || fallback, [rememberConvertToSupportedFormat]); + const getConvertToSupportedFormat = useCallback((fallback: Html5ifyMode) => rememberConvertToSupportedFormat || fallback, [rememberConvertToSupportedFormat]); - const html5ifyAndLoadWithPreferences = useCallback(async (cod, fp, speed, hv, ha) => { + const html5ifyAndLoadWithPreferences = useCallback(async (cod: string | undefined, fp: string, speed: Html5ifyMode, hv: boolean, ha: boolean) => { if (!enableAutoHtml5ify) return; setWorking({ text: i18n.t('Converting to supported format') }); await html5ifyAndLoad(cod, fp, getConvertToSupportedFormat(speed), hv, ha); diff --git a/src/renderer/src/ffmpeg.ts b/src/renderer/src/ffmpeg.ts index bb1e42c0..672df8a7 100644 --- a/src/renderer/src/ffmpeg.ts +++ b/src/renderer/src/ffmpeg.ts @@ -14,10 +14,10 @@ import { UnsupportedFileError } from '../errors'; const { ffmpeg, fileTypePromise } = window.require('@electron/remote').require('./index.js'); -const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath } = ffmpeg; +const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath } = ffmpeg; -export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfmpeg, getFfmpegPath, setCustomFfPath }; +export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, getFfmpegPath, setCustomFfPath }; export class RefuseOverwriteError extends Error { diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts index 405019ab..ef73252e 100644 --- a/src/renderer/src/hooks/useFfmpegOperations.ts +++ b/src/renderer/src/hooks/useFfmpegOperations.ts @@ -4,8 +4,8 @@ import sum from 'lodash/sum'; import pMap from 'p-map'; import invariant from 'tiny-invariant'; -import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration } from '../util'; -import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, getExperimentalArgs, html5ify as ffmpegHtml5ify, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat, RefuseOverwriteError, runFfmpeg } from '../ffmpeg'; +import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration, isMac } from '../util'; +import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, getExperimentalArgs, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat, RefuseOverwriteError, runFfmpeg } from '../ffmpeg'; import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams'; import { getSmartCutParams } from '../smartcut'; import { isDurationValid } from '../segments'; @@ -660,11 +660,115 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea }) => { const outPath = getHtml5ifiedPath(customOutDir, filePathArg, speed); invariant(outPath != null); - await ffmpegHtml5ify({ filePath: filePathArg, outPath, speed, hasAudio, hasVideo, onProgress }); + + let audio: string | undefined; + if (hasAudio) { + if (speed === 'slowest') audio = 'hq'; + else if (['slow-audio', 'fast-audio'].includes(speed)) audio = 'lq'; + else if (['fast-audio-remux'].includes(speed)) audio = 'copy'; + } + + let video: string | undefined; + if (hasVideo) { + if (speed === 'slowest') video = 'hq'; + else if (['slow-audio', 'slow'].includes(speed)) video = 'lq'; + else video = 'copy'; + } + + console.log('Making HTML5 friendly version', { filePathArg, outPath, speed, video, audio }); + + let videoArgs: string[]; + let audioArgs: string[]; + + // h264/aac_at: No licensing when using HW encoder (Video/Audio Toolbox on Mac) + // https://github.com/mifi/lossless-cut/issues/372#issuecomment-810766512 + + const targetHeight = 400; + + switch (video) { + case 'hq': { + // eslint-disable-next-line unicorn/prefer-ternary + if (isMac) { + videoArgs = ['-vf', 'format=yuv420p', '-allow_sw', '1', '-vcodec', 'h264', '-b:v', '15M']; + } else { + // AV1 is very slow + // videoArgs = ['-vf', 'format=yuv420p', '-sws_flags', 'neighbor', '-vcodec', 'libaom-av1', '-crf', '30', '-cpu-used', '8']; + // Theora is a bit faster but not that much + // videoArgs = ['-vf', '-c:v', 'libtheora', '-qscale:v', '1']; + // videoArgs = ['-vf', 'format=yuv420p', '-c:v', 'libvpx-vp9', '-crf', '30', '-b:v', '0', '-row-mt', '1']; + // x264 can only be used in GPL projects + videoArgs = ['-vf', 'format=yuv420p', '-c:v', 'libx264', '-profile:v', 'high', '-preset:v', 'slow', '-crf', '17']; + } + break; + } + case 'lq': { + // eslint-disable-next-line unicorn/prefer-ternary + if (isMac) { + videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-allow_sw', '1', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k']; + } else { + // videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-sws_flags', 'neighbor', '-c:v', 'libtheora', '-qscale:v', '1']; + // x264 can only be used in GPL projects + videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-sws_flags', 'neighbor', '-c:v', 'libx264', '-profile:v', 'baseline', '-x264opts', 'level=3.0', '-preset:v', 'ultrafast', '-crf', '28']; + } + break; + } + case 'copy': { + videoArgs = ['-vcodec', 'copy']; + break; + } + default: { + videoArgs = ['-vn']; + } + } + + switch (audio) { + case 'hq': { + // eslint-disable-next-line unicorn/prefer-ternary + if (isMac) { + audioArgs = ['-acodec', 'aac_at', '-b:a', '192k']; + } else { + audioArgs = ['-acodec', 'flac']; + } + break; + } + case 'lq': { + // eslint-disable-next-line unicorn/prefer-ternary + if (isMac) { + audioArgs = ['-acodec', 'aac_at', '-ar', '44100', '-ac', '2', '-b:a', '96k']; + } else { + audioArgs = ['-acodec', 'flac', '-ar', '11025', '-ac', '2']; + } + break; + } + case 'copy': { + audioArgs = ['-acodec', 'copy']; + break; + } + default: { + audioArgs = ['-an']; + } + } + + const ffmpegArgs = [ + '-hide_banner', + + '-i', filePathArg, + ...videoArgs, + ...audioArgs, + '-sn', + '-y', outPath, + ]; + + const duration = await getDuration(filePathArg); + appendFfmpegCommandLog(ffmpegArgs); + const { stdout } = await runFfmpegWithProgress({ ffmpegArgs, duration, onProgress }); + + console.log(stdout.toString('utf8')); + invariant(outPath != null); await transferTimestamps({ inPath: filePathArg, outPath, treatOutputFileModifiedTimeAsStart }); return outPath; - }, [treatOutputFileModifiedTimeAsStart]); + }, [appendFfmpegCommandLog, treatOutputFileModifiedTimeAsStart]); // This is just used to load something into the player with correct duration, // so that the user can seek and then we render frames using ffmpeg & MediaSource