1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-24 11:22:34 +01:00
This commit is contained in:
Mikael Finstad 2024-05-20 12:17:53 +02:00
parent 918277bd75
commit 4892437b83
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
13 changed files with 92 additions and 29 deletions

View File

@ -87,7 +87,7 @@ import BigWaveform from './components/BigWaveform';
import isDev from './isDev';
import { Chapter, ChromiumHTMLVideoElement, CustomTagsByFile, EdlFileType, FfmpegCommandLog, FilesMeta, FormatTimecode, ParamsByStreamId, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types';
import { CaptureFormat, KeyboardAction, Html5ifyMode, WaveformMode } from '../../../types';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
const electron = window.require('electron');
@ -151,7 +151,7 @@ function App() {
// State per application launch
const lastOpenedPathRef = useRef<string>();
const [waveformMode, setWaveformMode] = useState<'big-waveform' | 'waveform'>();
const [waveformMode, setWaveformMode] = useState<WaveformMode>();
const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false);
const [keyframesEnabled, setKeyframesEnabled] = useState(true);
const [showRightBar, setShowRightBar] = useState(true);
@ -215,7 +215,7 @@ function App() {
const videoRef = useRef<ChromiumHTMLVideoElement>(null);
const videoContainerRef = useRef<HTMLDivElement>(null);
const setOutputPlaybackRate = useCallback((v) => {
const setOutputPlaybackRate = useCallback((v: number) => {
setOutputPlaybackRateState(v);
if (videoRef.current) videoRef.current.playbackRate = v;
}, []);
@ -1634,7 +1634,7 @@ function App() {
const toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
const seekClosestKeyframe = useCallback((direction) => {
const seekClosestKeyframe = useCallback((direction: number) => {
const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction });
if (time == null) return;
userSeekAbs(time);

View File

@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { CSSProperties, Dispatch, SetStateAction, memo, useCallback, useEffect, useMemo, useState } from 'react';
import { motion } from 'framer-motion';
import { MdRotate90DegreesCcw } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
@ -23,16 +23,18 @@ import { useSegColors } from './contexts';
import { isExactDurationMatch } from './util/duration';
import useUserSettings from './hooks/useUserSettings';
import { askForPlaybackRate } from './dialogs';
import { ApparentCutSegment, FormatTimecode, ParseTimecode, SegmentToExport, StateSegment } from './types';
import { WaveformMode } from '../../../types';
const { clipboard } = window.require('electron');
const zoomOptions = Array.from({ length: 13 }).fill().map((unused, z) => 2 ** z);
const zoomOptions = Array.from({ length: 13 }).fill(undefined).map((_unused, z) => 2 ** z);
const leftRightWidth = 100;
// eslint-disable-next-line react/display-name
const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) => {
const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }: { invertCutSegments: boolean, setInvertCutSegments: Dispatch<SetStateAction<boolean>> }) => {
const { t } = useTranslation();
const onYinYangClick = useCallback(() => {
@ -65,15 +67,26 @@ const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) =
// eslint-disable-next-line react/display-name
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart, formatTimecode, parseTimecode }) => {
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart, formatTimecode, parseTimecode }: {
darkMode: boolean,
cutTime: number,
setCutTime: (type: 'start' | 'end', v: number) => void,
startTimeOffset: number,
seekAbs: (a: number) => void,
currentCutSeg: StateSegment,
currentApparentCutSeg: ApparentCutSegment,
isStart?: boolean,
formatTimecode: FormatTimecode,
parseTimecode: ParseTimecode,
}) => {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
const [cutTimeManual, setCutTimeManual] = useState();
const [cutTimeManual, setCutTimeManual] = useState<string>();
// Clear manual overrides if upstream cut time has changed
useEffect(() => {
setCutTimeManual();
setCutTimeManual(undefined);
}, [setCutTimeManual, currentApparentCutSeg.start, currentApparentCutSeg.end]);
const isCutTimeManualSet = () => cutTimeManual !== undefined;
@ -83,7 +96,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
return `.1em solid ${darkMode ? segColor.desaturate(0.4).lightness(50).string() : segColor.desaturate(0.2).lightness(60).string()}`;
}, [currentCutSeg, darkMode, getSegColor]);
const cutTimeInputStyle = {
const cutTimeInputStyle: CSSProperties = {
border, borderRadius: 5, backgroundColor: 'var(--gray5)', transition: darkModeTransition, fontSize: 13, textAlign: 'center', padding: '1px 5px', marginTop: 0, marginBottom: 0, marginLeft: isStart ? 0 : 5, marginRight: isStart ? 5 : 0, boxSizing: 'border-box', fontFamily: 'inherit', width: 90, outline: 'none',
};
@ -92,7 +105,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
try {
setCutTime(isStart ? 'start' : 'end', timeWithoutOffset);
seekAbs(timeWithoutOffset);
setCutTimeManual();
setCutTimeManual(undefined);
} catch (err) {
console.error('Cannot set cut time', err);
// If we get an error from setCutTime, remain in the editing state (cutTimeManual)
@ -104,7 +117,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
e.preventDefault();
// Don't proceed if not a valid time value
const timeWithOffset = parseTimecode(cutTimeManual);
const timeWithOffset = cutTimeManual != null ? parseTimecode(cutTimeManual) : undefined;
if (timeWithOffset === undefined) return;
trySetTime(timeWithOffset);
@ -158,7 +171,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')}
onChange={(e) => handleCutTimeInput(e.target.value)}
onPaste={handleCutTimePaste}
onBlur={() => setCutTimeManual()}
onBlur={() => setCutTimeManual(undefined)}
onContextMenu={handleContextMenu}
value={isCutTimeManualSet()
? cutTimeManual
@ -181,6 +194,54 @@ function BottomBar({
toggleShowThumbnails, toggleWaveformMode, waveformMode, showThumbnails,
outputPlaybackRate, setOutputPlaybackRate,
formatTimecode, parseTimecode,
}: {
zoom: number,
setZoom: Dispatch<SetStateAction<number>>,
timelineToggleComfortZoom: () => void,
isRotationSet: boolean,
rotation: number,
areWeCutting: boolean,
increaseRotation: () => void,
cleanupFilesDialog: () => void,
captureSnapshot: () => void,
onExportPress: () => void,
segmentsToExport: SegmentToExport[],
hasVideo: boolean,
seekAbs: (a: number) => void,
currentSegIndexSafe: number,
cutSegments: StateSegment[],
currentCutSeg: StateSegment,
setCutStart: () => void,
setCutEnd: () => void,
setCurrentSegIndex: Dispatch<SetStateAction<number>>,
jumpTimelineStart: () => void,
jumpTimelineEnd: () => void,
jumpCutEnd: () => void,
jumpCutStart: () => void,
startTimeOffset: number,
setCutTime: (type: 'start' | 'end', v: number) => void,
currentApparentCutSeg: ApparentCutSegment,
playing: boolean,
shortStep: (a: number) => void,
togglePlay: () => void,
toggleLoopSelectedSegments: () => void,
hasAudio: boolean,
keyframesEnabled: boolean,
toggleShowKeyframes: () => void,
seekClosestKeyframe: (a: number) => void,
detectedFps: number | undefined,
isFileOpened: boolean,
selectedSegments: ApparentCutSegment[],
darkMode: boolean,
setDarkMode: Dispatch<SetStateAction<boolean>>,
toggleShowThumbnails: () => void,
toggleWaveformMode: () => void,
waveformMode: WaveformMode | undefined,
showThumbnails: boolean,
outputPlaybackRate: number,
setOutputPlaybackRate: (v: number) => void,
formatTimecode: FormatTimecode,
parseTimecode: ParseTimecode,
}) {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
@ -188,7 +249,7 @@ function BottomBar({
// ok this is a bit over-engineered but what the hell!
const loopSelectedSegmentsButtonStyle = useMemo(() => {
// cannot have less than 1 gradient element:
const selectedSegmentsSafe = (selectedSegments.length > 1 ? selectedSegments : [selectedSegments[0], selectedSegments[0]]).slice(0, 10);
const selectedSegmentsSafe = (selectedSegments.length > 1 ? selectedSegments : [selectedSegments[0]!, selectedSegments[0]!]).slice(0, 10);
const gradientColors = selectedSegmentsSafe.map((seg, i) => {
const segColor = getSegColorRaw(seg);
@ -233,7 +294,7 @@ function BottomBar({
const opacity = seg ? undefined : 0.5;
const text = seg ? `${newIndex + 1}` : '-';
const wide = text.length > 1;
const segButtonStyle = {
const segButtonStyle: CSSProperties = {
backgroundColor, opacity, padding: `6px ${wide ? 4 : 6}px`, borderRadius: 10, color: seg ? 'white' : undefined, fontSize: wide ? 12 : 14, width: 20, boxSizing: 'border-box', letterSpacing: -1, lineHeight: '10px', fontWeight: 'bold', margin: '0 6px',
};
@ -262,7 +323,7 @@ function BottomBar({
{hasAudio && (
<GiSoundWaves
size={24}
style={{ padding: '0 .1em', color: ['big-waveform', 'waveform'].includes(waveformMode) ? primaryTextColor : undefined }}
style={{ padding: '0 .1em', color: waveformMode != null && ['big-waveform', 'waveform'].includes(waveformMode) ? primaryTextColor : undefined }}
role="button"
title={t('Show waveform')}
onClick={() => toggleWaveformMode()}

View File

@ -4,8 +4,8 @@ import Swal from '../swal';
// eslint-disable-next-line import/prefer-default-export
export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps }) {
const { value: captureChoice } = await Swal.fire({
export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps }: { segmentsNumFrames: number, plural: boolean, fps: number }) {
const { value: captureChoice } = await Swal.fire<string>({
text: i18n.t(plural ? 'Extract frames of the selected segments as images' : 'Extract frames of the current segment as images'),
icon: 'question',
input: 'radio',
@ -23,7 +23,7 @@ export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps
if (!captureChoice) return undefined;
let filter;
let filter: string | undefined;
let estimatedMaxNumFiles = segmentsNumFrames;
if (captureChoice === 'thumbnailFilter') {
@ -44,7 +44,7 @@ export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps
}
if (captureChoice === 'selectNthSec' || captureChoice === 'selectNthFrame') {
let nthFrame;
let nthFrame: number;
if (captureChoice === 'selectNthFrame') {
const { value } = await Swal.fire({
text: i18n.t('Capture exactly one image every nth frame'),

View File

@ -1,6 +1,8 @@
// This code is for future use (e.g. creating black video to fill in using same codec parameters)
export function parseLevel(videoStream) {
import { FFprobeStream } from '../../../ffprobe';
export function parseLevel(videoStream: FFprobeStream) {
const { level: levelNumeric, codec_name: videoCodec } = videoStream;
if (levelNumeric == null || Number.isNaN(levelNumeric)) return undefined;
@ -9,7 +11,7 @@ export function parseLevel(videoStream) {
if (levelNumeric === 9) return '1b'; // 13 is 1.3. That are all like that (20 is 2.0, etc) except 1b which is 9.
let level = (levelNumeric / 10).toFixed(1); // https://stackoverflow.com/questions/42619191/what-does-level-mean-in-ffprobe-output
if (level >= 0) {
if (parseFloat(level) >= 0) {
if (level.slice(-2) === '.0') level = level.slice(0, -2); // slice off .0
const validLevels = ['1', '1b', '1.1', '1.2', '1.3', '2', '2.1', '2.2', '3', '3.1', '3.2', '4', '4.1', '4.2', '5', '5.1', '5.2', '6', '6.1', '6.2']; // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
if (validLevels.includes(level)) return level;
@ -17,7 +19,7 @@ export function parseLevel(videoStream) {
} else if (videoCodec === 'hevc') {
// Note that on MacOS we don't use x265, but videotoolbox
let level = (levelNumeric / 30).toFixed(1); // https://stackoverflow.com/questions/69983131/whats-the-difference-between-ffprobe-level-and-h-264-level
if (level >= 0) {
if (parseFloat(level) >= 0) {
if (level.slice(-2) === '.0') level = level.slice(0, -2); // slice off .0
const validLevels = ['1', '2', '2.1', '3', '3.1', '4', '4.1', '5', '5.1', '5.2', '6', '6.1', '6.2']; // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
if (validLevels.includes(level)) return level;

View File

@ -230,7 +230,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
setCutSegments(cutSegmentsNew);
}, [setCutSegments, cutSegments]);
const setCutTime = useCallback((type, time) => {
const setCutTime = useCallback((type: 'start' | 'end', time: number) => {
if (!isDurationValid(duration)) return;
const currentSeg = currentCutSeg;

View File

@ -17,11 +17,11 @@ export async function loadMifiLink() {
}
}
export async function runStartupCheck({ ffmpeg }) {
export async function runStartupCheck({ ffmpeg }: { ffmpeg: boolean }) {
try {
if (ffmpeg) await runFfmpegStartupCheck();
} catch (err) {
if (['EPERM', 'EACCES'].includes(err.code)) {
if (err instanceof Error && 'code' in err && typeof err.code === 'string' && ['EPERM', 'EACCES'].includes(err.code)) {
toast.fire({
timer: 30000,
icon: 'error',

View File

@ -5,8 +5,6 @@
"noEmit": true,
"noImplicitAny": false, // todo
"checkJs": false, // todo
"allowJs": true, // todo
},
"references": [
{ "path": "./tsconfig.main.json" },

View File

@ -112,6 +112,8 @@ export interface ApiKeyboardActionRequest {
export type Html5ifyMode = 'fastest' | 'fast-audio-remux' | 'fast-audio' | 'fast' | 'slow' | 'slow-audio' | 'slowest';
export type WaveformMode = 'big-waveform' | 'waveform';
// This is the contract with the user, see https://github.com/mifi/lossless-cut/blob/master/expressions.md
export interface ScopeSegment {
label: string,