add FFmpeg `-hwaccel auto` setting

and show loading elapsed time
closes #2746
pull/2806/head
Mikael Finstad 4 months ago
parent aad01cc1ce
commit 260314db55
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -223,9 +223,11 @@
"Edit track metadata": "Edit track metadata",
"Edit tracks / metadata tags": "Edit tracks / metadata tags",
"EDL": "EDL",
"Elapsed: {{seconds}} seconds": "Elapsed: {{seconds}} seconds",
"empty": "empty",
"Enable \"{{filterName}}\" bitstream filter.": "Enable \"{{filterName}}\" bitstream filter.",
"Enable experimental ffmpeg features flag?": "Enable experimental ffmpeg features flag",
"Enable FFmpeg `-hwaccel auto` flag. This can improve performance segment auto detection and FFmpeg-assisted playback speed.": "Enable FFmpeg `-hwaccel auto` flag. This can improve performance segment auto detection and FFmpeg-assisted playback speed.",
"Enable HEVC / H265 hardware decoding (you may need to turn this off if you have problems with HEVC files)": "Enable HEVC / H265 hardware decoding (you may need to turn this off if you have problems with HEVC files)",
"Enable MOV Faststart?": "Enable MOV Faststart",
"Enables shifting when required by the target format.": "Enables shifting when required by the target format.",
@ -525,6 +527,7 @@
"OpenTimelineIO": "OpenTimelineIO",
"Options": "Options",
"Options affecting exported files": "Options affecting exported files",
"Other": "Other",
"Other operations": "Other operations",
"Output actions": "Output actions",
"Output container format:": "Output container format:",

@ -7,6 +7,8 @@ export interface KeyBinding {
action: KeyboardAction,
}
export type FfmpegHwAccel = 'none' | 'auto' | 'vdpau' | 'dxva2' | 'd3d11va' | 'vaapi' | 'qsv' | 'videotoolbox';
export type CaptureFormat = 'jpeg' | 'png' | 'webp';
export type TimecodeFormat = 'timecodeWithDecimalFraction' | 'frameCount' | 'seconds' | 'timecodeWithFramesFraction';
@ -100,6 +102,7 @@ export interface Config {
thumbnailsEnabled: boolean,
keyframesEnabled: boolean,
reducedMotion: 'always' | 'never' | 'user',
ffmpegHwaccel: FfmpegHwAccel,
}
export interface Waveform {

@ -1,8 +1,8 @@
// ⚠️ These types are a the contract with the user, through the documentation.
// ⚠️ JSDoc comments get converted to user documentation.
// ‼️ DO NOT change these types without considering the user impact!
// See https://github.com/mifi/lossless-cut/blob/master/expressions.md
// https://github.com/mifi/lossless-cut/blob/master/docs.md#custom-exported-file-names
// JSDoc comments get converted to user documentation.
/**
* Properties of a source file made available to the user in the export file name template.

@ -1,4 +1,7 @@
// eslint-disable-next-line import/prefer-default-export
import type { FfmpegHwAccel } from './types.ts';
export const parseFfprobeDuration = (durationStr: string | undefined) => (
durationStr != null ? parseFloat(durationStr) : undefined
);
export const getHwaccelArgs = (hwaccel: FfmpegHwAccel) => (hwaccel !== 'none' ? ['-hwaccel', hwaccel] : []);

@ -171,6 +171,7 @@ const defaults: Config = {
thumbnailsEnabled: false,
keyframesEnabled: true,
reducedMotion: 'user',
ffmpegHwaccel: 'none',
};
const configFileName = 'config.json'; // note: this is also hard-coded inside electron-store

@ -9,12 +9,12 @@ import type { Readable } from 'node:stream';
import { app, clipboard, nativeImage } from 'electron';
import { platform, arch, isWindows, isLinux } from './util.js';
import type { CaptureFormat, Waveform } from '../common/types.js';
import type { CaptureFormat, FfmpegHwAccel, Waveform } from '../common/types.js';
import type { FFprobeFormat } from '../common/ffprobe.js';
import isDev from './isDev.js';
import logger from './logger.js';
import { parseFfmpegProgressLine } from './progress.js';
import { parseFfprobeDuration } from '../common/util.js';
import { getHwaccelArgs, parseFfprobeDuration } from '../common/util.js';
import { getFfmpegJpegQuality } from './ffmpegUtil.js';
@ -283,7 +283,7 @@ interface DetectedSegment {
}
// https://stackoverflow.com/questions/35675529/using-ffmpeg-how-to-do-a-scene-change-detection-with-timecode
export async function detectSceneChanges({ filePath, streamId, minChange, onProgress, onSegmentDetected, from, to }: {
export async function detectSceneChanges({ filePath, streamId, minChange, onProgress, onSegmentDetected, from, to, ffmpegHwaccel }: {
filePath: string,
streamId: number | undefined
minChange: number | string,
@ -291,9 +291,11 @@ export async function detectSceneChanges({ filePath, streamId, minChange, onProg
onSegmentDetected: (p: DetectedSegment) => void,
from: number,
to: number,
ffmpegHwaccel: FfmpegHwAccel,
}) {
const args = [
'-hide_banner',
...getHwaccelArgs(ffmpegHwaccel),
...getInputSeekArgs({ filePath, from, to }),
'-map', streamId != null ? `0:${streamId}` : 'v:0',
'-filter:v', `select='gt(scene,${minChange})',metadata=print:file=-:direct=1`, // direct=1 to flush stdout immediately
@ -326,7 +328,7 @@ export async function detectSceneChanges({ filePath, streamId, minChange, onProg
return { ffmpegArgs: args };
}
async function detectIntervals({ filePath, customArgs, onProgress, onSegmentDetected, from, to, matchLineTokens, boundingMode }: {
async function detectIntervals({ filePath, customArgs, onProgress, onSegmentDetected, from, to, matchLineTokens, boundingMode, ffmpegHwaccel }: {
filePath: string,
customArgs: string[],
onProgress: (p: number) => void,
@ -335,9 +337,11 @@ async function detectIntervals({ filePath, customArgs, onProgress, onSegmentDete
to: number,
matchLineTokens: (line: string) => DetectedSegment | undefined,
boundingMode: boolean,
ffmpegHwaccel: FfmpegHwAccel,
}) {
const args = [
'-hide_banner',
...getHwaccelArgs(ffmpegHwaccel),
...getInputSeekArgs({ filePath, from, to }),
...customArgs,
'-f', 'null', '-',
@ -377,7 +381,7 @@ async function detectIntervals({ filePath, customArgs, onProgress, onSegmentDete
const mapFilterOptions = (options: Record<string, string>) => Object.entries(options).map(([key, value]) => `${key}=${value}`).join(':');
export async function blackDetect({ filePath, streamId, filterOptions, boundingMode, onProgress, onSegmentDetected, from, to }: {
export async function blackDetect({ streamId, filterOptions, ...rest }: {
filePath: string,
streamId: number | undefined,
filterOptions: Record<string, string>,
@ -386,14 +390,10 @@ export async function blackDetect({ filePath, streamId, filterOptions, boundingM
onSegmentDetected: (p: DetectedSegment) => void,
from: number,
to: number,
ffmpegHwaccel: FfmpegHwAccel,
}) {
return detectIntervals({
filePath,
onProgress,
onSegmentDetected,
from,
to,
boundingMode,
...rest,
matchLineTokens: (line) => {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/);
@ -417,7 +417,7 @@ export async function blackDetect({ filePath, streamId, filterOptions, boundingM
});
}
export async function silenceDetect({ filePath, streamId, filterOptions, boundingMode, onProgress, onSegmentDetected, from, to }: {
export async function silenceDetect({ streamId, filterOptions, ...rest }: {
filePath: string,
streamId: number | undefined,
filterOptions: Record<string, string>,
@ -425,14 +425,10 @@ export async function silenceDetect({ filePath, streamId, filterOptions, boundin
onProgress: (p: number) => void,
onSegmentDetected: (p: DetectedSegment) => void,
from: number, to: number,
ffmpegHwaccel: FfmpegHwAccel,
}) {
return detectIntervals({
filePath,
onProgress,
onSegmentDetected,
from,
to,
boundingMode,
...rest,
matchLineTokens: (line) => {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/);
@ -576,7 +572,7 @@ export async function getDuration(filePath: string) {
const enableLog = false;
const encode = true;
export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndexes, seekTo, size, fps, rotate, forceColorspace }: {
export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndexes, seekTo, size, fps, rotate, forceColorspace, ffmpegHwaccel }: {
path: string,
videoStreamIndex?: number | undefined,
audioStreamIndexes: number[],
@ -585,6 +581,7 @@ export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIn
fps?: number | undefined,
rotate: number | undefined,
forceColorspace?: boolean | undefined,
ffmpegHwaccel: FfmpegHwAccel,
}) {
function getFilters() {
const graph: string[] = [];
@ -677,6 +674,7 @@ export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIn
const args = [
'-hide_banner',
...(enableLog ? [] : ['-loglevel', 'error']),
...getHwaccelArgs(ffmpegHwaccel),
// https://stackoverflow.com/questions/16658873/how-to-minimize-the-delay-in-a-live-streaming-with-ffmpeg
// https://unix.stackexchange.com/questions/25372/turn-off-buffering-in-pipe

@ -7,6 +7,10 @@ describe('parseFfmpegProgressLine', () => {
const str = 'frame= 2285 fps=135 q=4.0 Lsize=N/A time=00:01:31.36 bitrate=N/A speed=5.38x ';
expect(parseFfmpegProgressLine({ line: str, duration: 60 + 31.36 })).toBe(1);
});
test('parse video with elapsed', () => {
const str = 'frame= 4 fps=0.3 q=-0.0 size=N/A time=00:01:02.12 bitrate=N/A speed=4.93x elapsed=0:00:12.59';
expect(parseFfmpegProgressLine({ line: str, duration: 100 })).toBe(0.6212);
});
test('parse audio 0', () => {
const str = 'size= 0kB time=00:00:00.00 bitrate=N/A speed=N/A ';
expect(parseFfmpegProgressLine({ line: str, duration: 1 })).toBe(0);

@ -187,7 +187,7 @@ function App() {
const [selectedBatchFiles, setSelectedBatchFiles] = useState<string[]>([]);
const allUserSettings = useUserSettingsRoot();
const { captureFormat, keyframeCut, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, autoExportExtraStreams, askBeforeClose, enableImportChapters, enableAskForFileOpenAction, playbackVolume, autoSaveProjectFile, wheelSensitivity, waveformHeight, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, segmentsToChapters, simpleMode, cutFileTemplate, cutMergedFileTemplate, mergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, safeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, darkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, waveformMode: waveformModePreference, thumbnailsEnabled, keyframesEnabled, reducedMotion } = allUserSettings.settings;
const { captureFormat, keyframeCut, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, autoExportExtraStreams, askBeforeClose, enableImportChapters, enableAskForFileOpenAction, playbackVolume, autoSaveProjectFile, wheelSensitivity, waveformHeight, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, segmentsToChapters, simpleMode, cutFileTemplate, cutMergedFileTemplate, mergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, safeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, darkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, waveformMode: waveformModePreference, thumbnailsEnabled, keyframesEnabled, reducedMotion, ffmpegHwaccel } = allUserSettings.settings;
const { setCaptureFormat, setCustomOutDir, setKeyframeCut, setPlaybackVolume, setExportConfirmEnabled, setSimpleMode, setOutFormatLocked, setSafeOutputFileName, setKeyBindings, resetKeyBindings, setStoreProjectInWorkingDir, setCleanupChoices, toggleDarkMode, setWaveformMode, setThumbnailsEnabled, setKeyframesEnabled, prefersReducedMotion, customOutDir } = allUserSettings;
const [showAdvancedSettings, setShowAdvancedSettings] = useState(!simpleMode);
@ -346,7 +346,7 @@ function App() {
const {
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, getSegEstimatedSize, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, segmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, currentCutSegOrWholeTimeline, segColorCounter,
} = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta: mainFileMeta?.ffprobeMeta, seekAbs, activeVideoStreamIndex, activeAudioStreamIndexes, handleError, showGenericDialog, simpleMode });
} = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta: mainFileMeta?.ffprobeMeta, seekAbs, activeVideoStreamIndex, activeAudioStreamIndexes, handleError, showGenericDialog, simpleMode, ffmpegHwaccel });
const { getEdlFilePath, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments });
@ -586,7 +586,7 @@ function App() {
const {
concatFiles, html5ifyDummy, cutMultiple, concatCutSegments, html5ify, fixInvalidDuration, extractStreams, tryDeleteFiles,
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate: encBitrate, appendFfmpegCommandLog });
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate: encBitrate, appendFfmpegCommandLog, ffmpegHwaccel });
const { previewFilePath, setPreviewFilePath, usingDummyVideo, setUsingDummyVideo, userHtml5ifyCurrentFile, convertFormatBatch, html5ifyAndLoadWithPreferences } = useHtml5ify({
filePath, hasVideo, hasAudio, workingRef, setWorking, ensureWritableOutDir, customOutDir, batchFiles, enableAutoHtml5ify, setProgress, html5ify, html5ifyDummy, withErrorHandling, showGenericDialog,
@ -2510,7 +2510,7 @@ function App() {
{renderSubtitles()}
</video>
{filePath != null && compatPlayerEnabled && <MediaSourcePlayer rotate={effectiveRotation} filePath={filePath} videoStream={activeVideoStream} audioStreams={activeAudioStreams} masterVideoRef={videoRef} mediaSourceQuality={mediaSourceQuality} />}
{filePath != null && compatPlayerEnabled && <MediaSourcePlayer rotate={effectiveRotation} filePath={filePath} videoStream={activeVideoStream} audioStreams={activeAudioStreams} masterVideoRef={videoRef} mediaSourceQuality={mediaSourceQuality} ffmpegHwaccel={ffmpegHwaccel} />}
</div>
{bigWaveformEnabled && <BigWaveform waveforms={waveforms} relevantTime={relevantTime} playing={playing} fileDurationNonZero={fileDurationNonZero} zoom={zoomUnrounded} seekRel={seekRel} darkMode={darkMode} />}

@ -8,11 +8,12 @@ import isDev from './isDev';
import type { ChromiumHTMLVideoElement } from './types';
import type { FFprobeStream } from '../../common/ffprobe';
import { getFrameDuration } from './util';
import type { FfmpegHwAccel } from '../../common/types';
const { compatPlayer: { createMediaSourceStream } } = window.require('@electron/remote').require('./index.js');
async function startPlayback({ path, slaveVideo, masterVideo, videoStreamIndex, audioStreamIndexes, seekTo, signal, size, fps, rotate, onCanPlay, onResetNeeded, onWaiting }: {
async function startPlayback({ path, slaveVideo, masterVideo, videoStreamIndex, audioStreamIndexes, seekTo, signal, size, fps, rotate, onCanPlay, onResetNeeded, onWaiting, ffmpegHwaccel }: {
path: string,
slaveVideo: ChromiumHTMLVideoElement,
masterVideo: ChromiumHTMLVideoElement,
@ -26,6 +27,7 @@ async function startPlayback({ path, slaveVideo, masterVideo, videoStreamIndex,
onCanPlay: () => void,
onResetNeeded: () => void,
onWaiting: () => void,
ffmpegHwaccel: FfmpegHwAccel,
}) {
let canPlay = false;
let bufferEndTime: number | undefined;
@ -88,7 +90,7 @@ async function startPlayback({ path, slaveVideo, masterVideo, videoStreamIndex,
throw new Error(`Unsupported MIME type or codec: ${mimeCodec}`);
}
mediaSourceProcess = createMediaSourceStream({ path, videoStreamIndex, audioStreamIndexes, seekTo, size, fps, rotate });
mediaSourceProcess = createMediaSourceStream({ path, videoStreamIndex, audioStreamIndexes, seekTo, size, fps, rotate, ffmpegHwaccel });
console.log('Waiting for media source process to emit first data...');
const readChunk = await mediaSourceProcess.promise;
if (readChunk == null) {
@ -305,13 +307,14 @@ async function startPlayback({ path, slaveVideo, masterVideo, videoStreamIndex,
processChunk();
}
function MediaSourcePlayer({ rotate, filePath, videoStream, audioStreams, masterVideoRef, mediaSourceQuality }: {
function MediaSourcePlayer({ rotate, filePath, videoStream, audioStreams, masterVideoRef, mediaSourceQuality, ffmpegHwaccel }: {
rotate: number | undefined,
filePath: string,
videoStream: FFprobeStream | undefined,
audioStreams: FFprobeStream[],
masterVideoRef: RefObject<HTMLVideoElement>,
mediaSourceQuality: number,
ffmpegHwaccel: FfmpegHwAccel,
}) {
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@ -382,6 +385,7 @@ function MediaSourcePlayer({ rotate, filePath, videoStream, audioStreams, master
onWaiting: () => {
setLoading(true);
},
ffmpegHwaccel,
});
} catch (err) {
console.error('Preview failed', err);
@ -394,7 +398,7 @@ function MediaSourcePlayer({ rotate, filePath, videoStream, audioStreams, master
return () => abortController.abort();
// Important that we also have eventId in the deps, so that we can restart the preview when the eventId changes
}, [audioStreamIndexes, filePath, masterVideoRef, mediaSourceQuality, rotate, videoStream]);
}, [audioStreamIndexes, ffmpegHwaccel, filePath, masterVideoRef, mediaSourceQuality, rotate, videoStream]);
const onFocus = useCallback<FocusEventHandler<HTMLVideoElement>>((e) => {
// prevent video element from stealing focus in fullscreen mode https://github.com/mifi/lossless-cut/issues/543#issuecomment-1868167775

@ -75,7 +75,7 @@ function Settings({
}) {
const { t } = useTranslation();
const { customOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableImportChapters, setEnableImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, hideNotifications, setHideNotifications, hideOsNotifications, setHideOsNotifications, autoLoadTimecode, setAutoLoadTimecode, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, setMouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, setMouseWheelKeyframeSeekModifierKey, segmentMouseModifierKey, setSegmentMouseModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors, treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart, exportConfirmEnabled, toggleExportConfirmEnabled, storeWindowBounds, setStoreWindowBounds, reducedMotion, setReducedMotion } = useUserSettings();
const { customOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableImportChapters, setEnableImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, hideNotifications, setHideNotifications, hideOsNotifications, setHideOsNotifications, autoLoadTimecode, setAutoLoadTimecode, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, setMouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, setMouseWheelKeyframeSeekModifierKey, segmentMouseModifierKey, setSegmentMouseModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors, treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart, exportConfirmEnabled, toggleExportConfirmEnabled, storeWindowBounds, setStoreWindowBounds, reducedMotion, setReducedMotion, ffmpegHwaccel, setFfmpegHwaccel } = useUserSettings();
const onLangChange = useCallback<ChangeEventHandler<HTMLSelectElement>>((e) => {
const { value } = e.target;
@ -460,15 +460,6 @@ function Settings({
</Row>
)}
{showAdvancedSettings && (
<Row>
<KeyCell>{t('Enable HEVC / H265 hardware decoding (you may need to turn this off if you have problems with HEVC files)')}</KeyCell>
<td>
<Switch checked={enableNativeHevc} onCheckedChange={setEnableNativeHevc} />
</td>
</Row>
)}
{showAdvancedSettings && (
<Row>
<KeyCell>{t('Try to automatically convert to supported format when opening unsupported file?')}</KeyCell>
@ -550,6 +541,25 @@ function Settings({
</Select>
</Row>
)}
{showAdvancedSettings && (
<>
<Header title={t('Other')} />
<Row>
<KeyCell>{t('Enable HEVC / H265 hardware decoding (you may need to turn this off if you have problems with HEVC files)')}</KeyCell>
<td>
<Switch checked={enableNativeHevc} onCheckedChange={setEnableNativeHevc} />
</td>
</Row>
<Row>
<KeyCell>{t('Enable FFmpeg `-hwaccel auto` flag. This can improve performance segment auto detection and FFmpeg-assisted playback speed.')}</KeyCell>
<td>
<Switch checked={ffmpegHwaccel === 'auto'} onCheckedChange={(v) => setFfmpegHwaccel(v ? 'auto' : 'none')} />
</td>
</Row>
</>
)}
</tbody>
</table>
);

@ -1,7 +1,8 @@
import { memo } from 'react';
import { memo, useState } from 'react';
import { motion } from 'motion/react';
import Lottie from 'react-lottie-player/dist/LottiePlayerLight';
import { Trans } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import useInterval from 'react-use/lib/useInterval';
import loadingLottie from '../7077-magic-flow.json';
import Button from './Button';
@ -13,6 +14,19 @@ function Working({ text, progress, onAbortClick }: {
progress?: number | undefined,
onAbortClick: () => void
}) {
const { t } = useTranslation();
const [startedAt] = useState(() => new Date());
const [elapsedMs, setElapsedMs] = useState(0);
// Reassure the user that the app is not frozen
// This is because some ffmpeg operations can take a long time without giving any progress updates, which might make the user think that the app is frozen
// https://github.com/mifi/lossless-cut/issues/2746
useInterval(() => {
setElapsedMs(Date.now() - startedAt.getTime());
}, 100);
return (
<div className={styles['wrapper']} style={{ position: 'absolute', bottom: 0, top: 0, left: 0, right: 0, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<motion.div
@ -21,7 +35,7 @@ function Working({ text, progress, onAbortClick }: {
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
>
<div style={{ width: 150, height: 80 }}>
<div style={{ width: '10em', height: '5em', marginBottom: '.5em' }}>
<Lottie
loop
animationData={loadingLottie}
@ -30,17 +44,21 @@ function Working({ text, progress, onAbortClick }: {
/>
</div>
<div style={{ marginTop: '.7em', textAlign: 'center' }}>
<div style={{ marginBottom: '.2em', textAlign: 'center' }}>
{text}...
</div>
<div style={{ marginBottom: '.5em', fontSize: '.9em', color: 'var(--gray-11)', textAlign: 'center' }}>
{t('Elapsed: {{seconds}} seconds', { seconds: (elapsedMs / 1000).toFixed(1) })}
</div>
{(progress != null) && (
<div style={{ marginTop: '.5em' }}>
<div style={{ marginBottom: '.5em', fontSize: '1.3em' }}>
{`${(progress * 100).toFixed(1)} %`}
</div>
)}
<div style={{ marginTop: '1.5em' }}>
<div>
<Button onClick={onAbortClick} style={{ fontSize: '1.1em', padding: '.2em 1em' }}><Trans>Abort</Trans></Button>
</div>
</motion.div>

@ -11,11 +11,12 @@ import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams';
import { needsSmartCut, getCodecParams } from '../smartcut';
import { getGuaranteedSegments, isDurationValid } from '../segments';
import type { FFprobeStream } from '../../../common/ffprobe';
import type { AvoidNegativeTs, Html5ifyMode, PreserveMetadata } from '../../../common/types';
import type { AvoidNegativeTs, FfmpegHwAccel, Html5ifyMode, PreserveMetadata } from '../../../common/types';
import type { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types';
import type { LossyMode } from '../../../main';
import { UserFacingError } from '../../errors';
import mainApi from '../mainApi';
import { getHwaccelArgs } from '../../../common/util';
const { join, resolve, dirname } = window.require('path');
const { writeFile, mkdir, access, constants: { W_OK } } = window.require('fs/promises');
@ -79,7 +80,7 @@ export async function maybeMkDeepOutDir({ outputDir, fileOutPath }: { outputDir:
}
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate, appendFfmpegCommandLog }: {
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate, appendFfmpegCommandLog, ffmpegHwaccel }: {
filePath: string | undefined,
treatInputFileModifiedTimeAsStart: boolean,
treatOutputFileModifiedTimeAsStart: boolean | null | undefined,
@ -92,6 +93,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
appendLastCommandsLog: (a: string) => void,
encCustomBitrate: number | undefined,
appendFfmpegCommandLog: (args: string[]) => void,
ffmpegHwaccel: FfmpegHwAccel,
}) {
const shouldSkipExistingFile = useCallback(async (path: string) => {
const fileExists = await mainApi.pathExists(path);
@ -734,14 +736,14 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const outPath = getHtml5ifiedPath(customOutDir, filePathArg, speed);
invariant(outPath != null);
let audio: string | undefined;
let audio: 'hq' | 'lq' | 'copy' | 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;
let video: 'hq' | 'lq' | 'copy' | undefined;
if (hasVideo) {
if (speed === 'slowest') video = 'hq';
else if (['slow-audio', 'slow'].includes(speed)) video = 'lq';
@ -824,6 +826,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const ffmpegArgs = [
'-hide_banner',
...((video === 'lq' || video === 'hq') ? getHwaccelArgs(ffmpegHwaccel) : []),
'-i', filePathArg,
...videoArgs,
@ -841,7 +844,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
invariant(outPath != null);
await transferTimestamps({ inPath: filePathArg, outPath, duration, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart });
return outPath;
}, [appendFfmpegCommandLog, html5ifyDummy, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
}, [appendFfmpegCommandLog, ffmpegHwaccel, html5ifyDummy, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
// https://stackoverflow.com/questions/34118013/how-to-determine-webm-duration-using-ffprobe
const fixInvalidDuration = useCallback(async ({ fileFormat, customOutDir, onProgress }: {

@ -31,6 +31,7 @@ import * as Dialog from '../components/Dialog';
import { UserFacingError } from '../../errors';
import { editSegmentByExpressionHelpUrl, selectSegmentByExpressionHelpUrl } from '../../../common/constants';
import type { Segment as ScopeSegment } from '../../../common/userTypes';
import type { FfmpegHwAccel } from '../../../common/types';
const remote = window.require('@electron/remote');
const { shell } = remote;
@ -41,7 +42,7 @@ type ParameterDialogParameters = Record<string, string>;
const offsetSegments = (segments: DefiniteSegmentBase[], offset: number) => segments.map((s) => ({ start: s.start + offset, end: s.end + offset }));
function useSegments({ filePath, workingRef, setWorking, setProgress, videoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta, seekAbs, activeVideoStreamIndex, activeAudioStreamIndexes, handleError, showGenericDialog, simpleMode }: {
function useSegments({ filePath, workingRef, setWorking, setProgress, videoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta, seekAbs, activeVideoStreamIndex, activeAudioStreamIndexes, handleError, showGenericDialog, simpleMode, ffmpegHwaccel }: {
filePath?: string | undefined,
workingRef: MutableRefObject<boolean>,
setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void,
@ -64,6 +65,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
handleError: HandleError,
showGenericDialog: ShowGenericDialog,
simpleMode: boolean,
ffmpegHwaccel: FfmpegHwAccel,
}) {
const { t } = useTranslation();
@ -318,8 +320,8 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
setFfmpegParametersForDialog(dialogType, parameters);
invariant(mode === '1' || mode === '2');
invariant(filePath != null);
await detectSegments({ name: 'blackScenes', workingText: i18n.t('Detecting black scenes'), errorText: i18n.t('Failed to detect black scenes'), fn: async (onSegmentDetected) => blackDetect({ filePath, streamId: activeVideoStreamIndex, filterOptions, boundingMode: mode === '1', onProgress: setProgress, onSegmentDetected, from: start, to: end }) });
}, [currentCutSegOrWholeTimeline, deleteCurrentCutSeg, showParametersDialog, getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, activeVideoStreamIndex, setProgress]);
await detectSegments({ name: 'blackScenes', workingText: i18n.t('Detecting black scenes'), errorText: i18n.t('Failed to detect black scenes'), fn: async (onSegmentDetected) => blackDetect({ filePath, streamId: activeVideoStreamIndex, filterOptions, boundingMode: mode === '1', onProgress: setProgress, onSegmentDetected, from: start, to: end, ffmpegHwaccel }) });
}, [currentCutSegOrWholeTimeline, deleteCurrentCutSeg, showParametersDialog, getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, activeVideoStreamIndex, setProgress, ffmpegHwaccel]);
const detectSilentScenes = useCallback(async () => {
const { start, end } = currentCutSegOrWholeTimeline;
@ -331,8 +333,8 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
const { mode, ...filterOptions } = parameters;
invariant(mode === '1' || mode === '2');
invariant(filePath != null);
await detectSegments({ name: 'silentScenes', workingText: i18n.t('Detecting silent scenes'), errorText: i18n.t('Failed to detect silent scenes'), fn: async (onSegmentDetected) => silenceDetect({ filePath, streamId: [...activeAudioStreamIndexes][0], filterOptions, boundingMode: mode === '1', onProgress: setProgress, onSegmentDetected, from: start, to: end }) });
}, [activeAudioStreamIndexes, currentCutSegOrWholeTimeline, deleteCurrentCutSeg, detectSegments, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress, showParametersDialog]);
await detectSegments({ name: 'silentScenes', workingText: i18n.t('Detecting silent scenes'), errorText: i18n.t('Failed to detect silent scenes'), fn: async (onSegmentDetected) => silenceDetect({ filePath, streamId: [...activeAudioStreamIndexes][0], filterOptions, boundingMode: mode === '1', onProgress: setProgress, onSegmentDetected, from: start, to: end, ffmpegHwaccel }) });
}, [activeAudioStreamIndexes, currentCutSegOrWholeTimeline, deleteCurrentCutSeg, detectSegments, ffmpegHwaccel, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress, showParametersDialog]);
const detectSceneChanges = useCallback(async () => {
const { start, end } = currentCutSegOrWholeTimeline;
@ -345,8 +347,8 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
// eslint-disable-next-line prefer-destructuring
const minChange = parameters['minChange'];
invariant(minChange != null);
await detectSegments({ name: 'sceneChanges', workingText: i18n.t('Detecting scene changes'), errorText: i18n.t('Failed to detect scene changes'), fn: async (onSegmentDetected) => ffmpegDetectSceneChanges({ filePath, streamId: activeVideoStreamIndex, minChange, onProgress: setProgress, onSegmentDetected, from: start, to: end }) });
}, [activeVideoStreamIndex, currentCutSegOrWholeTimeline, deleteCurrentCutSeg, detectSegments, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress, showParametersDialog]);
await detectSegments({ name: 'sceneChanges', workingText: i18n.t('Detecting scene changes'), errorText: i18n.t('Failed to detect scene changes'), fn: async (onSegmentDetected) => ffmpegDetectSceneChanges({ filePath, streamId: activeVideoStreamIndex, minChange, onProgress: setProgress, onSegmentDetected, from: start, to: end, ffmpegHwaccel }) });
}, [activeVideoStreamIndex, currentCutSegOrWholeTimeline, deleteCurrentCutSeg, detectSegments, ffmpegHwaccel, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress, showParametersDialog]);
const createSegmentsFromKeyframes = useCallback(async () => {
const { start, end } = currentCutSegOrWholeTimeline;

@ -194,6 +194,8 @@ export default function useUserSettingsRoot() {
useEffect(() => safeSetConfig({ keyframesEnabled }), [keyframesEnabled]);
const [reducedMotion, setReducedMotion] = useState(safeGetConfigInitial('reducedMotion'));
useEffect(() => safeSetConfig({ reducedMotion }), [reducedMotion]);
const [ffmpegHwaccel, setFfmpegHwaccel] = useState(safeGetConfigInitial('ffmpegHwaccel'));
useEffect(() => safeSetConfig({ ffmpegHwaccel }), [ffmpegHwaccel]);
const resetKeyBindings = useCallback(() => {
@ -312,6 +314,7 @@ export default function useUserSettingsRoot() {
thumbnailsEnabled,
keyframesEnabled,
reducedMotion,
ffmpegHwaccel,
};
return {
@ -394,6 +397,7 @@ export default function useUserSettingsRoot() {
setKeyframesEnabled,
prefersReducedMotion,
setReducedMotion,
setFfmpegHwaccel,
};
}

Loading…
Cancel
Save