fix any types

this also fixes a bug where concat check used the incorrect stream meta "fps" (instead of avg_frame_rate)
pull/2350/head
Mikael Finstad 1 year ago
parent 8485b14019
commit 250217344f
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -287,7 +287,7 @@ export type FFprobeStreamTags = {
// https://github.com/mifi/lossless-cut/issues/1530
title?: string,
}
} & Record<string, string>
/**
* An FFprobe response stream object
@ -557,7 +557,7 @@ export interface FFprobeChapter {
/**
* The "tags" field on an FFprobe response format object
*/
export interface FFprobeFormatTags {
export type FFprobeFormatTags = {
/**
* Not clear, probably the media type brand, but not sure
*/
@ -682,7 +682,7 @@ export interface FFprobeFormatTags {
* The song's publisher (only present in audio files)
*/
PUBLISHER?: string
}
} & Record<string, string>
/**
* An FFprobe response format object

@ -62,6 +62,8 @@
"@types/node": "20",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/smpte-timecode": "^1.2.5",
"@types/sortablejs": "^1.15.0",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",

@ -23,7 +23,7 @@ export function createMediaSourceStream({ path, videoStreamIndex, audioStreamInd
stdout.pause();
const readChunk = async () => new Promise((resolve, reject) => {
const readChunk = async () => new Promise<Buffer | null>((resolve, reject) => {
let cleanup: () => void;
const onClose = () => {

@ -88,7 +88,7 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
import BigWaveform from './components/BigWaveform';
import isDev from './isDev';
import { Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, TunerType } from './types';
import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, TunerType } from './types';
import { CaptureFormat, KeyboardAction, Html5ifyMode, WaveformMode, ApiActionRequest } from '../../../types';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
import useLoading from './hooks/useLoading';
@ -168,7 +168,7 @@ function App() {
const incrementMediaSourceQuality = useCallback(() => setMediaSourceQuality((v) => (v + 1) % mediaSourceQualities.length), []);
// Batch state / concat files
const [batchFiles, setBatchFiles] = useState<{ path: string }[]>([]);
const [batchFiles, setBatchFiles] = useState<BatchFile[]>([]);
const [selectedBatchFiles, setSelectedBatchFiles] = useState<string[]>([]);
const allUserSettings = useUserSettingsRoot();

@ -13,7 +13,7 @@ class ErrorBoundary extends Component<{ children: ReactNode }> {
this.state = { error: undefined };
}
static getDerivedStateFromError(error) {
static getDerivedStateFromError(error: unknown) {
return { error };
}

@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback, useMemo, memo, CSSProperties, RefObject } from 'react';
import { useEffect, useRef, useState, useCallback, useMemo, memo, CSSProperties, RefObject, ReactEventHandler, FocusEventHandler } from 'react';
import { Spinner } from 'evergreen-ui';
import { useDebounce } from 'use-debounce';
@ -25,7 +25,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex,
let canPlay = false;
let bufferEndTime: number | undefined;
let bufferStartTime = 0;
let stream;
let stream: ReturnType<typeof createMediaSourceStream> | undefined;
let done = false;
let interval: NodeJS.Timeout | undefined;
let objectUrl: string | undefined;
@ -48,7 +48,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex,
const mediaSource = new MediaSource();
let streamTimestamp;
let streamTimestamp: number | undefined;
let lastRemoveTimestamp = 0;
function setStandardPlaybackRate() {
@ -111,7 +111,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex,
const processChunk = async () => {
try {
const chunk = await stream.readChunk();
const chunk = await stream!.readChunk();
if (chunk == null) {
console.log('End of stream');
return;
@ -264,7 +264,7 @@ function MediaSourcePlayer({ rotate, filePath, playerTime, videoStream, audioStr
const canvasRef = useRef<HTMLCanvasElement>(null);
const [loading, setLoading] = useState(true);
const onVideoError = useCallback((error) => {
const onVideoError = useCallback<ReactEventHandler<HTMLVideoElement>>((error) => {
console.error('video error', error);
}, []);
@ -348,7 +348,7 @@ function MediaSourcePlayer({ rotate, filePath, playerTime, videoStream, audioStr
if (videoRef.current) videoRef.current.volume = playbackVolume;
}, [playbackVolume]);
const onFocus = useCallback((e) => {
const onFocus = useCallback<FocusEventHandler<HTMLVideoElement | HTMLCanvasElement>>((e) => {
// prevent video element from stealing focus in fullscreen mode https://github.com/mifi/lossless-cut/issues/543#issuecomment-1868167775
e.target.blur();
}, []);

@ -1,4 +1,4 @@
import { memo, useMemo, useRef, useCallback, useState, SetStateAction, Dispatch, ReactNode } from 'react';
import { memo, useMemo, useRef, useCallback, useState, SetStateAction, Dispatch, ReactNode, MouseEventHandler } from 'react';
import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
import { AiOutlineSplitCells } from 'react-icons/ai';
import { MotionStyle, motion } from 'framer-motion';
@ -168,7 +168,7 @@ const Segment = memo(({
const CheckIcon = selected ? FaRegCheckCircle : FaRegCircle;
const onToggleSegmentSelectedClick = useCallback((e) => {
const onToggleSegmentSelectedClick = useCallback<MouseEventHandler>((e) => {
e.stopPropagation();
onToggleSegmentSelected(seg);
}, [onToggleSegmentSelected, seg]);
@ -310,7 +310,7 @@ function SegmentList({
const sortableList = useMemo(() => segments.map((seg) => ({ id: seg.segId, seg })), [segments]);
const setSortableList = useCallback((newList) => {
const setSortableList = useCallback((newList: typeof sortableList) => {
if (isEqual(segments.map((s) => s.segId), newList.map((l) => l.id))) return; // No change
updateSegOrders(newList.map((list) => list.id));
}, [segments, updateSegOrders]);

@ -9,6 +9,7 @@ import { SortAlphabeticalIcon, SortAlphabeticalDescIcon } from 'evergreen-ui';
import BatchFile from './BatchFile';
import { controlsBackground, darkModeTransition } from '../colors';
import { mySpring } from '../animations';
import { BatchFile as BatchFileType } from '../types';
const iconStyle = {
@ -20,14 +21,25 @@ const iconStyle = {
padding: '3px 5px',
};
function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchListRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }) {
function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchListRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }: {
selectedBatchFiles: string[],
filePath: string | undefined,
width: number,
batchFiles: BatchFileType[],
setBatchFiles: (f: BatchFileType[]) => void,
onBatchFileSelect: (f: string) => void,
batchListRemoveFile: (path: string | undefined) => void,
closeBatch: () => void,
onMergeFilesClick: () => void,
onBatchConvertToSupportedFormatClick: () => void,
}) {
const { t } = useTranslation();
const [sortDesc, setSortDesc] = useState<boolean>();
const sortableList = batchFiles.map((batchFile) => ({ id: batchFile.path, batchFile }));
const setSortableList = useCallback((newList) => {
const setSortableList = useCallback((newList: { batchFile: BatchFileType }[]) => {
setBatchFiles(newList.map(({ batchFile }) => batchFile));
}, [setBatchFiles]);

@ -25,13 +25,13 @@ function BigWaveform({ waveforms, relevantTime, playing, durationSafe, zoom, see
const smoothTime = smoothTimeRaw ?? relevantTime;
const mouseDownRef = useRef<{ relevantTime: number, x }>();
const mouseDownRef = useRef<{ relevantTime: number, x: number }>();
const containerRef = useRef<HTMLDivElement>(null);
const getRect = useCallback(() => containerRef.current!.getBoundingClientRect(), []);
const handleMouseDown = useCallback((e) => {
const rect = e.target.getBoundingClientRect();
const handleMouseDown = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
const rect = (e.target as HTMLDivElement).getBoundingClientRect();
const x = e.clientX - rect.left;
mouseDownRef.current = { relevantTime, x };

@ -142,7 +142,7 @@ function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFi
return;
}
// check all these parameters
['codec_name', 'width', 'height', 'fps', 'pix_fmt', 'level', 'profile', 'sample_fmt', 'r_frame_rate', 'time_base'].forEach((key) => {
(['codec_name', 'width', 'height', 'pix_fmt', 'level', 'profile', 'sample_fmt', 'avg_frame_rate', 'r_frame_rate', 'time_base'] as const).forEach((key) => {
const val = stream[key];
const referenceVal = referenceStream[key];
if (val !== referenceVal) {
@ -187,7 +187,7 @@ function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFi
};
}, [allFilesMetaCache, enableReadFileMeta, isShown, paths]);
const onOutputFormatUserChange = useCallback((newFormat) => setFileFormat(newFormat), [setFileFormat]);
const onOutputFormatUserChange = useCallback((newFormat: string) => setFileFormat(newFormat), [setFileFormat]);
const onConcatClick = useCallback(() => {
if (outFileName == null) throw new Error();

@ -698,7 +698,7 @@ const KeyboardShortcuts = memo(({
const categoriesWithActions = useMemo(() => Object.entries(groupBy(actionEntries, ([, { category }]) => category)), [actionEntries]);
const onDeleteBindingClick = useCallback(({ action, keys }) => {
const onDeleteBindingClick = useCallback(({ action, keys }: { action: KeyboardAction, keys: string }) => {
// eslint-disable-next-line no-alert
if (!window.confirm(t('Are you sure?'))) return;

@ -11,7 +11,7 @@ const commonSubtitleFormats = ['ass', 'srt', 'sup', 'webvtt'];
function renderFormatOptions(formats: string[]) {
return formats.map((format) => (
<option key={format} value={format}>{format} - {allOutFormats[format]}</option>
<option key={format} value={format}>{format} - {(allOutFormats as Record<string, string>)[format]}</option>
));
}
@ -32,7 +32,7 @@ function OutputFormatSelect({ style, detectedFileFormat, fileFormat, onOutputFor
{detectedFileFormat && (
<option key={detectedFileFormat} value={detectedFileFormat}>
{detectedFileFormat} - {allOutFormats[detectedFileFormat]} {i18n.t('(detected)')}
{detectedFileFormat} - {(allOutFormats as Record<string, string>)[detectedFileFormat]} {i18n.t('(detected)')}
</option>
)}

@ -1,11 +1,17 @@
import { CSSProperties, useMemo } from 'react';
import { FaStepForward } from 'react-icons/fa';
import { useSegColors } from '../contexts';
import useUserSettings from '../hooks/useUserSettings';
import { SegmentBase, SegmentColorIndex } from '../types';
const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }: {
currentCutSeg: SegmentBase & SegmentColorIndex, side: 'start' | 'end', Icon, onClick?: (() => void) | undefined, title?: string | undefined, style?: CSSProperties | undefined
currentCutSeg: SegmentBase & SegmentColorIndex,
side: 'start' | 'end',
Icon: typeof FaStepForward,
onClick?: (() => void) | undefined,
title?: string | undefined,
style?: CSSProperties | undefined,
}) => {
const { darkMode } = useUserSettings();
const { getSegColor } = useSegColors();
@ -18,9 +24,9 @@ const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, styl
return (
<Icon
size={13}
title={title}
title={title as string}
role="button"
style={{ flexShrink: 0, color: 'white', padding: start ? '4px 4px 4px 2px' : '4px 2px 4px 4px', borderLeft: start && border, borderRight: !start && border, background: backgroundColor, borderRadius: 6, ...style }}
style={{ flexShrink: 0, color: 'white', padding: start ? '4px 4px 4px 2px' : '4px 2px 4px 4px', borderLeft: start ? border : undefined, borderRight: !start ? border : undefined, background: backgroundColor, borderRadius: 6, ...style }}
onClick={onClick}
/>
);

@ -7,7 +7,11 @@ import { SegmentBase, SegmentColorIndex } from '../types';
// constant side because we are mirroring
const SetCutpointButton = ({ currentCutSeg, side, title, onClick, style }: {
currentCutSeg: SegmentBase & SegmentColorIndex, side: 'start' | 'end', title?: string, onClick?: () => void, style?: CSSProperties
currentCutSeg: SegmentBase & SegmentColorIndex,
side: 'start' | 'end',
title?: string,
onClick?: () => void,
style?: CSSProperties,
}) => (
<SegmentCutpointButton currentCutSeg={currentCutSeg} side="end" Icon={FaHandPointUp} onClick={onClick} title={title} style={{ transform: side === 'start' ? mirrorTransform : undefined, ...style }} />
);

@ -122,7 +122,7 @@ function Settings({
<td>
<Select value={language || ''} onChange={onLangChange} style={{ fontSize: '1.2em' }}>
<option key="" value="">{t('System language')}</option>
{Object.keys(langNames).map((lang) => <option key={lang} value={lang}>{langNames[lang]}</option>)}
{Object.keys(langNames).map((lang) => <option key={lang} value={lang}>{langNames[lang as keyof typeof langNames]}</option>)}
</Select>
</td>
</Row>

@ -1,4 +1,4 @@
import { memo, useRef, useState, useMemo, useCallback, useEffect } from 'react';
import { memo, useRef, useState, useMemo, useCallback, useEffect, FormEventHandler, MouseEventHandler } from 'react';
import { useTranslation } from 'react-i18next';
import { TextInput, TrashIcon, ResetIcon, TickIcon, EditIcon, PlusIcon, Button, IconButton } from 'evergreen-ui';
import invariant from 'tiny-invariant';
@ -68,14 +68,14 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
}
}, [editingTag, editingTagVal, existingTags, mergedTags, newTag, onResetClick, saveTag, setEditingTag]);
function onSubmit(e) {
const onSubmit = useCallback<FormEventHandler<HTMLFormElement>>((e) => {
e.preventDefault();
onEditClick();
}
}, [onEditClick]);
const onAddPress = useCallback(async (e) => {
const onAddPress = useCallback<MouseEventHandler<HTMLButtonElement>>(async (e) => {
e.preventDefault();
e.target.blur();
(e.target as HTMLButtonElement).blur();
if (newTag || editingTag != null) {
// save any unsaved edit

@ -1,4 +1,4 @@
import { memo, useState, useCallback, CSSProperties } from 'react';
import { memo, useState, useCallback, CSSProperties, ChangeEventHandler } from 'react';
import { Button } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
@ -6,17 +6,25 @@ import Switch from './Switch';
function ValueTuner({ style, title, value, setValue, onFinished, resolution = 1000, min: minIn = 0, max: maxIn = 1, resetToDefault }: {
style?: CSSProperties, title: string, value: number, setValue: (string) => void, onFinished: () => void, resolution?: number, min?: number, max?: number, resetToDefault: () => void
style?: CSSProperties,
title: string,
value: number,
setValue: (v: number) => void,
onFinished: () => void,
resolution?: number,
min?: number,
max?: number,
resetToDefault: () => void,
}) {
const { t } = useTranslation();
const [min, setMin] = useState(minIn);
const [max, setMax] = useState(maxIn);
function onChange(e) {
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>((e) => {
e.target.blur();
setValue(Math.min(Math.max(min, ((e.target.value / resolution) * (max - min)) + min), max));
}
setValue(Math.min(Math.max(min, ((Number(e.target.value) / resolution) * (max - min)) + min), max));
}, [max, min, resolution, setValue]);
const isZoomed = !(min === minIn && max === maxIn);

@ -1,9 +1,13 @@
import { memo, useState, useCallback, useRef, useEffect } from 'react';
import { memo, useState, useCallback, useRef, useEffect, ChangeEventHandler } from 'react';
import { FaVolumeMute, FaVolumeUp } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
function VolumeControl({ playbackVolume, setPlaybackVolume, onToggleMutedClick }: { playbackVolume: number, setPlaybackVolume: (a: number) => void, onToggleMutedClick: () => void }) {
function VolumeControl({ playbackVolume, setPlaybackVolume, onToggleMutedClick }: {
playbackVolume: number,
setPlaybackVolume: (a: number) => void,
onToggleMutedClick: () => void,
}) {
const [volumeControlVisible, setVolumeControlVisible] = useState(false);
const timeoutRef = useRef<number>();
const { t } = useTranslation();
@ -15,9 +19,9 @@ function VolumeControl({ playbackVolume, setPlaybackVolume, onToggleMutedClick }
return () => clear();
}, [playbackVolume, volumeControlVisible]);
const onVolumeChange = useCallback((e) => {
const onVolumeChange = useCallback<ChangeEventHandler<HTMLInputElement>>((e) => {
e.target.blur();
setPlaybackVolume(e.target.value / 100);
setPlaybackVolume(Number(e.target.value) / 100);
}, [setPlaybackVolume]);
const onVolumeIconClick = useCallback(() => {

@ -1,4 +1,4 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, ChangeEventHandler } from 'react';
import i18n from 'i18next';
import { ReactSwal } from '../swal';
@ -8,7 +8,9 @@ import Checkbox from '../components/Checkbox';
// eslint-disable-next-line import/prefer-default-export
export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initialOption }: {
allowedOptions: Html5ifyMode[], showRemember?: boolean | undefined, initialOption?: Html5ifyMode | undefined
allowedOptions: Html5ifyMode[],
showRemember?: boolean | undefined,
initialOption?: Html5ifyMode | undefined,
}) {
const availOptions: Record<Html5ifyMode, string> = {
fastest: i18n.t('Fastest: FFmpeg-assisted playback'),
@ -31,12 +33,12 @@ export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initia
const [option, setOption] = useState(selectedOption);
const [remember, setRemember] = useState(rememberChoice);
const onOptionChange = useCallback((e) => {
selectedOption = e.currentTarget.value;
const onOptionChange = useCallback<ChangeEventHandler<HTMLInputElement>>((e) => {
selectedOption = e.currentTarget.value as Html5ifyMode;
setOption(selectedOption);
}, []);
const onRememberChange = useCallback((checked) => {
const onRememberChange = useCallback((checked: boolean) => {
rememberChoice = checked;
setRemember(rememberChoice);
}, []);

@ -6,6 +6,7 @@ import SyntaxHighlighter from 'react-syntax-highlighter';
import { tomorrow as lightSyntaxStyle, tomorrowNight as darkSyntaxStyle } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import JSON5 from 'json5';
import type { SweetAlertOptions } from 'sweetalert2';
import invariant from 'tiny-invariant';
import { formatDuration } from '../util/duration';
import Swal, { ReactSwal, swalToastOptions, toast } from '../swal';
@ -221,7 +222,7 @@ async function askForNumSegments() {
return parseInt(value, 10);
}
export async function createNumSegments(fileDuration) {
export async function createNumSegments(fileDuration: number) {
const numSegments = await askForNumSegments();
if (numSegments == null) return undefined;
const edl: SegmentBase[] = [];
@ -259,17 +260,17 @@ async function askForSegmentDuration({ fileDuration, inputPlaceholder, parseTime
// https://github.com/mifi/lossless-cut/issues/1153
async function askForSegmentsRandomDurationRange() {
function parse(str) {
function parse(str: string) {
// eslint-disable-next-line unicorn/better-regex
const match = str.replaceAll(/\s/g, '').match(/^duration([\d.]+)to([\d.]+),gap([-\d.]+)to([-\d.]+)$/i);
if (!match) return undefined;
const values = match.slice(1);
const parsed = values.map((val) => parseFloat(val));
const durationMin = parsed[0];
const durationMax = parsed[1];
const gapMin = parsed[2];
const gapMax = parsed[3];
const durationMin = parsed[0]!;
const durationMax = parsed[1]!;
const gapMin = parsed[2]!;
const gapMax = parsed[3]!;
if (!(parsed.every((val) => !Number.isNaN(val)) && durationMin <= durationMax && gapMin <= gapMax && durationMin > 0)) return undefined;
return { durationMin, durationMax, gapMin, gapMax };
@ -306,7 +307,7 @@ async function askForSegmentsStartOrEnd(text: string) {
});
if (!value) return undefined;
return value === 'both' ? ['start', 'end'] : [value];
return value === 'both' ? ['start', 'end'] as const : [value as 'start' | 'end'] as const;
}
export async function askForShiftSegments({ inputPlaceholder, parseTimecode }: { inputPlaceholder: string, parseTimecode: ParseTimecode }) {
@ -338,6 +339,7 @@ export async function askForShiftSegments({ inputPlaceholder, parseTimecode }: {
if (value == null) return undefined;
const parsed = parseValue(value);
invariant(parsed != null);
const startOrEnd = await askForSegmentsStartOrEnd(i18n.t('Do you want to shift the start or end timestamp by {{time}}?', { time: formatDuration({ seconds: parsed, shorten: true }) }));
if (startOrEnd == null) return undefined;
@ -567,7 +569,7 @@ export async function labelSegmentDialog({ currentName, maxLength }: { currentNa
title: i18n.t('Label current segment'),
inputValue: currentName,
input: currentName.includes('\n') ? 'textarea' : 'text',
inputValidator: (v) => (v.length > maxLength ? `${i18n.t('Max length')} ${maxLength}` : null),
inputValidator: (v: string) => (v.length > maxLength ? `${i18n.t('Max length')} ${maxLength}` : null),
});
return value;
}
@ -685,7 +687,7 @@ export async function openDirToast({ filePath, text, html, ...props }: SweetAler
if (value) showItemInFolder(filePath);
}
const UnorderedList = ({ children }) => (
const UnorderedList = ({ children }: { children: ReactNode }) => (
<ul style={{ paddingLeft: '1em' }}>{children}</ul>
);
const ListItem = ({ icon: Icon, iconColor, children, style }: { icon: IconComponent, iconColor?: string, children: ReactNode, style?: CSSProperties }) => (
@ -736,7 +738,7 @@ export async function openConcatFinishedToast({ filePath, warnings, notices }: {
await openDirToast({ filePath, html, width: 800, position: 'center', timer: 30000 });
}
export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }) {
export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }: { detectedFps: number | undefined, outputPlaybackRate: number }) {
const fps = detectedFps || 1;
const currentFps = fps * outputPlaybackRate;

@ -141,7 +141,11 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?
else if (type === 'srt') filters = [{ name: i18n.t('Subtitles (SRT)'), extensions: ['srt'] }];
else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }];
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], filters, title: i18n.t('Import project') });
const { canceled, filePaths } = await showOpenDialog({
properties: ['openFile'],
title: i18n.t('Import project'),
...(filters && { filters }),
});
const [firstFilePath] = filePaths;
if (canceled || firstFilePath == null) return [];
return readEdlFile({ type, path: firstFilePath, fps });

@ -1,7 +1,7 @@
import pMap from 'p-map';
import sortBy from 'lodash/sortBy';
import i18n from 'i18next';
import Timecode from 'smpte-timecode';
import Timecode, { FRAMERATE } from 'smpte-timecode';
import minBy from 'lodash/minBy';
import invariant from 'tiny-invariant';
@ -88,13 +88,13 @@ export async function readFrames({ filePath, from, to, streamIndex }: {
return sortBy(packetsFiltered, 'time');
}
export async function readFramesAroundTime({ filePath, streamIndex, aroundTime, window }) {
export async function readFramesAroundTime({ filePath, streamIndex, aroundTime, window }: { filePath: string, streamIndex: number, aroundTime: number, window: number }) {
if (aroundTime == null) throw new Error('aroundTime was nullish');
const { from, to } = getIntervalAroundTime(aroundTime, window);
return readFrames({ filePath, from, to, streamIndex });
}
export async function readKeyframesAroundTime({ filePath, streamIndex, aroundTime, window }) {
export async function readKeyframesAroundTime({ filePath, streamIndex, aroundTime, window }: { filePath: string, streamIndex: number, aroundTime: number, window: number }) {
const frames = await readFramesAroundTime({ filePath, aroundTime, streamIndex, window });
return frames.filter((frame) => frame.keyframe);
}
@ -158,7 +158,7 @@ export function getSafeCutTime(frames: (Frame & { time: number })[], cutTime: nu
return time;
}
const findReverseIndex = (arr, cb) => {
const findReverseIndex = <T>(arr: T[], cb: (value: T, i: number, obj: T[]) => unknown) => {
// eslint-disable-next-line unicorn/no-array-callback-reference
const ret = [...arr].reverse().findIndex(cb);
if (ret === -1) return -1;
@ -477,7 +477,7 @@ export function isIphoneHevc(format: FFprobeFormat, streams: FFprobeStream[]) {
if (!streams.some((s) => s.codec_name === 'hevc')) return false;
const makeTag = format.tags && format.tags['com.apple.quicktime.make'];
const modelTag = format.tags && format.tags['com.apple.quicktime.model'];
return (makeTag === 'Apple' && modelTag.startsWith('iPhone'));
return (makeTag === 'Apple' && modelTag?.startsWith('iPhone'));
}
export function isProblematicAvc1(outFormat: string | undefined, streams: FFprobeStream[]) {
@ -524,7 +524,7 @@ export function getStreamFps(stream: FFprobeStream) {
function parseTimecode(str: string, frameRate?: number | undefined) {
// console.log(str, frameRate);
const t = Timecode(str, frameRate ? parseFloat(frameRate.toFixed(3)) : undefined);
const t = Timecode(str, frameRate ? parseFloat(frameRate.toFixed(3)) as FRAMERATE : undefined);
if (!t) return undefined;
const seconds = ((t.hours * 60) + t.minutes) * 60 + t.seconds + (t.frames / t.frameRate);
return Number.isFinite(seconds) ? seconds : undefined;

@ -31,10 +31,10 @@ export function parseLevel(videoStream: FFprobeStream) {
return undefined;
}
export function parseProfile(videoStream) {
export function parseProfile(videoStream: FFprobeStream) {
const { profile: ffprobeProfile, codec_name: videoCodec } = videoStream;
let map;
let map: Map<string, { profile: string, warn?: boolean }>;
if (videoCodec === 'h264') {
// List of profiles that x264 supports https://trac.ffmpeg.org/wiki/Encode/H.264
// baseline, main, high, high10 (first 10 bit compatible profile), high422 (supports yuv420p, yuv422p, yuv420p10le and yuv422p10le), high444 (supports as above as well as yuv444p and yuv444p10le)
@ -71,12 +71,13 @@ export function parseProfile(videoStream) {
return undefined;
}
if (!map.has(ffprobeProfile)) {
const match = map.get(ffprobeProfile);
if (match == null) {
console.warn('Unknown ffprobe profile', ffprobeProfile);
return undefined;
}
const match = map.get(ffprobeProfile);
if (match.warn) console.warn('Possibly unknown ffprobe profile', ffprobeProfile);
return match.profile;
}

@ -559,7 +559,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
}];
// eslint-disable-next-line no-shadow
async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }) {
async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }: { cutFrom: number, cutTo: number, outPath: string }) {
if (await shouldSkipExistingFile(outPath)) return;
if (videoCodec == null || detectedVideoBitrate == null || videoTimebase == null) throw new Error();
invariant(filePath != null);
@ -851,16 +851,16 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// TODO add more
// TODO allow user to change?
};
} as const;
const match = map[stream.codec_name];
const match = map[stream.codec_name as keyof typeof map];
if (match) return match;
// default fallbacks:
if (stream.codec_type === 'video') return { ext: 'mkv', format: 'matroska' };
if (stream.codec_type === 'audio') return { ext: 'mka', format: 'matroska' };
if (stream.codec_type === 'subtitle') return { ext: 'mks', format: 'matroska' };
if (stream.codec_type === 'data') return { ext: 'bin', format: 'data' }; // https://superuser.com/questions/1243257/save-data-stream
if (stream.codec_type === 'video') return { ext: 'mkv', format: 'matroska' } as const;
if (stream.codec_type === 'audio') return { ext: 'mka', format: 'matroska' } as const;
if (stream.codec_type === 'subtitle') return { ext: 'mks', format: 'matroska' } as const;
if (stream.codec_type === 'data') return { ext: 'bin', format: 'data' } as const; // https://superuser.com/questions/1243257/save-data-stream
return undefined;
}
@ -871,13 +871,19 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
invariant(filePath != null);
if (streams.length === 0) return [];
const outStreams = streams.map((s) => ({
index: s.index,
codec: s.codec_name || s.codec_tag_string || s.codec_type,
type: s.codec_type,
format: getPreferredCodecFormat(s),
}))
.filter(({ format, index }) => format != null && index != null);
const outStreams = streams.flatMap((s) => {
const format = getPreferredCodecFormat(s);
const { index } = s;
if (format == null || index == null) return [];
return [{
index,
codec: s.codec_name || s.codec_tag_string || s.codec_type,
type: s.codec_type,
format,
}];
});
// console.log(outStreams);

@ -20,7 +20,7 @@ function useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream,
const [neighbouringKeyFramesMap, setNeighbouringKeyFrames] = useState<Record<string, Frame>>({});
const neighbouringKeyFrames = useMemo(() => Object.values(neighbouringKeyFramesMap), [neighbouringKeyFramesMap]);
const findNearestKeyFrameTime = useCallback(({ time, direction }) => ffmpegFindNearestKeyFrameTime({ frames: neighbouringKeyFrames, time, direction, fps: detectedFps }), [neighbouringKeyFrames, detectedFps]);
const findNearestKeyFrameTime = useCallback(({ time, direction }: { time: number, direction: number }) => ffmpegFindNearestKeyFrameTime({ frames: neighbouringKeyFrames, time, direction, fps: detectedFps }), [neighbouringKeyFrames, detectedFps]);
useEffect(() => setNeighbouringKeyFrames({}), [filePath, videoStream]);

@ -45,7 +45,7 @@ export default ({ mainStreams, filePath, autoExportExtraStreams }: {
const checkCopyingAnyTrackOfType = useCallback((filter: (s: FFprobeStream) => boolean) => mainStreams.some((stream) => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]);
const toggleStripStream = useCallback((filter) => {
const toggleStripStream = useCallback((filter: (s: FFprobeStream) => boolean) => {
const copyingAnyTrackOfType = checkCopyingAnyTrackOfType(filter);
invariant(filePath != null);
setCopyStreamIdsForPath(filePath, (old) => {

@ -1,7 +1,7 @@
import React from 'react';
// https://stackoverflow.com/questions/64997362/how-do-i-see-what-props-have-changed-in-react
export default function useWhatChanged(props) {
export default function useWhatChanged(props: Record<string, unknown>) {
// cache the last set of props
const prev = React.useRef(props);

@ -106,27 +106,30 @@ export function partitionIntoOverlappingRanges<T extends SegmentBase | ApparentS
return ret.filter((group) => group.length > 1).map((group) => sortBy(group, (seg) => getSegmentStart(seg)));
}
export function combineOverlappingSegments(existingSegments, getSegApparentEnd2) {
export function combineOverlappingSegments<T extends SegmentBase>(existingSegments: T[], getSegApparentEnd2: (seg: SegmentBase) => number) {
const partitionedSegments = partitionIntoOverlappingRanges(existingSegments, getSegApparentStart, getSegApparentEnd2);
return existingSegments.map((existingSegment) => {
return existingSegments.flatMap((existingSegment) => {
const partOfPartition = partitionedSegments.find((partition) => partition.includes(existingSegment));
if (partOfPartition == null) return existingSegment; // this is not an overlapping segment, pass it through
if (partOfPartition == null) {
return [existingSegment]; // this is not an overlapping segment, pass it through
}
const index = partOfPartition.indexOf(existingSegment);
// The first segment is the one with the lowest "start" value, so we use its start value
if (index === 0) {
return {
return [{
...existingSegment,
// but use the segment with the highest "end" value as the end value.
end: sortBy(partOfPartition, (segment) => segment.end)[partOfPartition.length - 1]!.end,
};
}];
}
return undefined; // then remove all other segments in this partition group
}).filter(Boolean);
return []; // then remove all other segments in this partition group
});
}
export function combineSelectedSegments<T extends SegmentBase>(existingSegments: T[], getSegApparentEnd2, isSegmentSelected) {
export function combineSelectedSegments(existingSegments: StateSegment[], getSegApparentEnd2: (seg: StateSegment) => number, isSegmentSelected: (seg: StateSegment) => boolean) {
const selectedSegments = existingSegments.filter((segment) => isSegmentSelected(segment));
const firstSegment = minBy(selectedSegments, (seg) => getSegApparentStart(seg));
const lastSegment = maxBy(selectedSegments, (seg) => getSegApparentEnd2(seg));

@ -37,9 +37,9 @@ const customTheme: ProviderProps<DefaultTheme>['value'] = {
// https://github.com/segmentio/evergreen/blob/master/src/themes/default/components/button.js
// eslint-disable-next-line @typescript-eslint/no-explicit-any
border: ((_theme, props) => `1px solid ${borderColorForIntent(props.intent)}`) as any as string, // todo types
border: ((_theme: unknown, props: { intent: IntentTypes }) => `1px solid ${borderColorForIntent(props.intent)}`) as any as string, // todo types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
color: ((_theme, props) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types
color: ((_theme: unknown, props: { color: string, intent: IntentTypes }) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types
_hover: {
backgroundColor: 'var(--gray4)',
@ -61,7 +61,7 @@ const customTheme: ProviderProps<DefaultTheme>['value'] = {
// https://github.com/segmentio/evergreen/blob/master/src/themes/default/components/button.js
// eslint-disable-next-line @typescript-eslint/no-explicit-any
color: ((_theme, props) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types
color: ((_theme: unknown, props: { color: string, intent: IntentTypes }) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types
_hover: {
backgroundColor: 'var(--gray4)',

@ -136,3 +136,8 @@ export interface StreamParams {
bsfHevcMp4toannexb?: boolean,
}
export type ParamsByStreamId = Map<string, Map<number, StreamParams>>;
export interface BatchFile {
path: string,
name: string,
}

@ -506,8 +506,8 @@ export async function readDirRecursively(dirPath: string) {
export function getImportProjectType(filePath: string) {
if (filePath.endsWith('Summary.txt')) return 'dv-analyzer-summary-txt';
const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'edl', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' };
const matchingExt = Object.keys(edlFormatForExtension).find((ext) => filePath.toLowerCase().endsWith(`.${ext}`));
const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'edl', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' } as const;
const matchingExt = Object.keys(edlFormatForExtension).find((ext) => filePath.toLowerCase().endsWith(`.${ext}`)) as keyof typeof edlFormatForExtension | undefined;
if (!matchingExt) return undefined;
return edlFormatForExtension[matchingExt];
}

@ -67,10 +67,12 @@ Object.getOwnPropertyNames(myGlobal).forEach(function (prop) {
}
});
// @ts-expect-error dunno
// eslint-disable-next-line no-proto, prefer-arrow-callback, func-names
Object.getOwnPropertyNames(myGlobal.__proto__).forEach(function (prop) {
// eslint-disable-next-line no-prototype-builtins
if (!wl.hasOwnProperty(prop)) {
// @ts-expect-error dunno
// eslint-disable-next-line no-proto
Object.defineProperty(myGlobal.__proto__, prop, {
// eslint-disable-next-line func-names, object-shorthand
@ -92,7 +94,7 @@ Object.defineProperty(Array.prototype, 'join', {
// eslint-disable-next-line wrap-iife, func-names
value: function (old) {
// eslint-disable-next-line func-names
return function (arg) {
return function (arg: unknown[]) {
// @ts-expect-error dunno how to fix
if (this.length > 500 || (arg && arg.length > 500)) {
// eslint-disable-next-line no-throw-literal

@ -4,8 +4,6 @@
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"noEmit": true,
"noImplicitAny": false, // todo
"types": ["vite/client"],
},
"references": [

@ -2233,6 +2233,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-syntax-highlighter@npm:^15.5.13":
version: 15.5.13
resolution: "@types/react-syntax-highlighter@npm:15.5.13"
dependencies:
"@types/react": "npm:*"
checksum: 10/0cc47785326aea5effac9ee68c263abc6448c63b6a084eec3084fd13c08da93addfdc3102ff919cbe420aa71fbded9bf041cc2c51fe625c6ee0751e8a131bd38
languageName: node
linkType: hard
"@types/react-transition-group@npm:^4.4.0":
version: 4.4.4
resolution: "@types/react-transition-group@npm:4.4.4"
@ -2315,6 +2324,13 @@ __metadata:
languageName: node
linkType: hard
"@types/smpte-timecode@npm:^1.2.5":
version: 1.2.5
resolution: "@types/smpte-timecode@npm:1.2.5"
checksum: 10/e91c0535df278bd3cbe9b5517c5edf084531322237ec876ec40154d79968faa343404822f5b85df42455a2749e1e7a13a1254b9fc893cf7de09823e0d74c4f03
languageName: node
linkType: hard
"@types/sortablejs@npm:^1.15.0":
version: 1.15.0
resolution: "@types/sortablejs@npm:1.15.0"
@ -7599,6 +7615,8 @@ __metadata:
"@types/node": "npm:20"
"@types/react": "npm:^18.2.66"
"@types/react-dom": "npm:^18.2.22"
"@types/react-syntax-highlighter": "npm:^15.5.13"
"@types/smpte-timecode": "npm:^1.2.5"
"@types/sortablejs": "npm:^1.15.0"
"@types/yargs-parser": "npm:^21.0.3"
"@typescript-eslint/eslint-plugin": "npm:^6.12.0"

Loading…
Cancel
Save