From 1a674fa7353b25a664d5d3f75fb35b8c29252094 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 27 Aug 2024 20:16:27 +0200 Subject: [PATCH] improve types --- src/renderer/src/App.tsx | 2 +- src/renderer/src/ffmpeg.ts | 7 ++-- src/renderer/src/hooks/useFfmpegOperations.ts | 4 +- src/renderer/src/smartcut.ts | 2 +- src/renderer/src/types.ts | 4 +- src/renderer/src/util/streams.test.ts | 29 +++++++------- src/renderer/src/util/streams.ts | 40 +++++++++++-------- 7 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 2c0ad910..ebe0dfe5 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -589,7 +589,7 @@ function App() { showNotification({ timer: 13000, text: i18n.t('File is not natively supported. Preview playback may be slow and of low quality, but the final export will be lossless. You may convert the file from the menu for a better preview.') }); }, [showNotification]); - const showPreviewFileLoadedMessage = useCallback((fileName) => { + const showPreviewFileLoadedMessage = useCallback((fileName: string) => { showNotification({ icon: 'info', text: i18n.t('Loaded existing preview file: {{ fileName }}', { fileName }) }); }, [showNotification]); diff --git a/src/renderer/src/ffmpeg.ts b/src/renderer/src/ffmpeg.ts index 64f867a2..bbe079b8 100644 --- a/src/renderer/src/ffmpeg.ts +++ b/src/renderer/src/ffmpeg.ts @@ -5,11 +5,12 @@ import Timecode from 'smpte-timecode'; import minBy from 'lodash/minBy'; import invariant from 'tiny-invariant'; -import { pcmAudioCodecs, getMapStreamsArgs, isMov, LiteFFprobeStream } from './util/streams'; +import { pcmAudioCodecs, getMapStreamsArgs, isMov } from './util/streams'; import { getSuffixedOutPath, isExecaError } from './util'; import { isDurationValid } from './segments'; import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe'; import { parseSrt, parseSrtToSegments } from './edlFormats'; +import { CopyfileStreams, LiteFFprobeStream } from './types'; const FileType = window.require('file-type'); const { pathExists } = window.require('fs-extra'); @@ -58,7 +59,7 @@ export function isCuttingEnd(cutTo: number, duration: number | undefined) { return cutTo < duration; } -function getIntervalAroundTime(time, window) { +function getIntervalAroundTime(time: number, window: number) { return { from: Math.max(time - window / 2, 0), to: time + window / 2, @@ -681,7 +682,7 @@ export const getVideoTimescaleArgs = (videoTimebase: number | undefined) => (vid // inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e export async function cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoTimebase, allFilesMeta, copyFileStreams, videoStreamIndex, ffmpegExperimental }: { - filePath: string, cutFrom: number, cutTo: number, outPath: string, outFormat: string, videoCodec: string, videoBitrate: number, videoTimebase: number, allFilesMeta, copyFileStreams, videoStreamIndex: number, ffmpegExperimental: boolean, + filePath: string, cutFrom: number, cutTo: number, outPath: string, outFormat: string, videoCodec: string, videoBitrate: number, videoTimebase: number, allFilesMeta, copyFileStreams: CopyfileStreams, videoStreamIndex: number, ffmpegExperimental: boolean, }) { function getVideoArgs({ streamIndex, outputIndex }: { streamIndex: number, outputIndex: number }) { if (streamIndex !== videoStreamIndex) return undefined; diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts index 037b6367..2caea4c0 100644 --- a/src/renderer/src/hooks/useFfmpegOperations.ts +++ b/src/renderer/src/hooks/useFfmpegOperations.ts @@ -432,8 +432,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea console.log('Smart cut on video stream', videoStreamIndex); - const onCutProgress = (progress) => onSingleProgress(i, progress / 2); - const onConcatProgress = (progress) => onSingleProgress(i, (1 + progress) / 2); + const onCutProgress = (progress: number) => onSingleProgress(i, progress / 2); + const onConcatProgress = (progress: number) => onSingleProgress(i, (1 + progress) / 2); const copyFileStreamsFiltered = [{ path: filePath, diff --git a/src/renderer/src/smartcut.ts b/src/renderer/src/smartcut.ts index 36fdbf23..b65c7c8a 100644 --- a/src/renderer/src/smartcut.ts +++ b/src/renderer/src/smartcut.ts @@ -10,7 +10,7 @@ const mapVideoCodec = (codec: string) => ({ av1: 'libsvtav1' }[codec] ?? codec); // eslint-disable-next-line import/prefer-default-export export async function getSmartCutParams({ path, videoDuration, desiredCutFrom, streams }: { - path: string, videoDuration: number | undefined, desiredCutFrom: number, streams: FFprobeStream[], + path: string, videoDuration: number | undefined, desiredCutFrom: number, streams: Pick[], }) { const videoStreams = getRealVideoStreams(streams); if (videoStreams.length > 1) throw new Error('Can only smart cut video with exactly one video stream'); diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index 91cb01bb..12158318 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -119,8 +119,10 @@ export type CopyfileStreams = { export interface Chapter { start: number, end: number, name?: string | undefined } +export type LiteFFprobeStream = Pick; + export type AllFilesMeta = Record diff --git a/src/renderer/src/util/streams.test.ts b/src/renderer/src/util/streams.test.ts index 2ba7c595..a0df24f7 100644 --- a/src/renderer/src/util/streams.test.ts +++ b/src/renderer/src/util/streams.test.ts @@ -1,7 +1,8 @@ import { test, expect } from 'vitest'; -import { LiteFFprobeStream, getMapStreamsArgs, getStreamIdsToCopy } from './streams'; +import { getMapStreamsArgs, getStreamIdsToCopy } from './streams'; import { FFprobeStreamDisposition } from '../../../../ffprobe'; +import { LiteFFprobeStream } from '../types'; const makeDisposition = (override?: Partial): FFprobeStreamDisposition => ({ @@ -26,14 +27,14 @@ const makeDisposition = (override?: Partial): FFprobeS }); const streams1: LiteFFprobeStream[] = [ - { index: 0, codec_type: 'video', codec_tag: '0x0000', codec_name: 'mjpeg', disposition: makeDisposition({ attached_pic: 1 }) }, - { index: 1, codec_type: 'audio', codec_tag: '0x6134706d', codec_name: 'aac', disposition: makeDisposition() }, - { index: 2, codec_type: 'video', codec_tag: '0x31637661', codec_name: 'h264', disposition: makeDisposition() }, - { index: 3, codec_type: 'video', codec_tag: '0x0000', codec_name: 'hevc', disposition: makeDisposition() }, - { index: 4, codec_type: 'audio', codec_tag: '0x6134706d', codec_name: 'aac', disposition: makeDisposition() }, - { index: 5, codec_type: 'attachment', codec_tag: '0x0000', codec_name: 'ttf', disposition: makeDisposition() }, - { index: 6, codec_type: 'data', codec_tag: '0x64636d74', codec_name: '', disposition: makeDisposition() }, - { index: 7, codec_type: 'subtitle', codec_tag: '0x0000', codec_name: 'subrip', disposition: makeDisposition() }, + { index: 0, codec_type: 'video', codec_tag: '0x0000', codec_name: 'mjpeg', time_base: '', disposition: makeDisposition({ attached_pic: 1 }) }, + { index: 1, codec_type: 'audio', codec_tag: '0x6134706d', codec_name: 'aac', time_base: '', disposition: makeDisposition() }, + { index: 2, codec_type: 'video', codec_tag: '0x31637661', codec_name: 'h264', time_base: '', disposition: makeDisposition() }, + { index: 3, codec_type: 'video', codec_tag: '0x0000', codec_name: 'hevc', time_base: '', disposition: makeDisposition() }, + { index: 4, codec_type: 'audio', codec_tag: '0x6134706d', codec_name: 'aac', time_base: '', disposition: makeDisposition() }, + { index: 5, codec_type: 'attachment', codec_tag: '0x0000', codec_name: 'ttf', time_base: '', disposition: makeDisposition() }, + { index: 6, codec_type: 'data', codec_tag: '0x64636d74', codec_name: '', time_base: '', disposition: makeDisposition() }, + { index: 7, codec_type: 'subtitle', codec_tag: '0x0000', codec_name: 'subrip', time_base: '', disposition: makeDisposition() }, ]; const path = '/path/to/file'; @@ -66,8 +67,8 @@ test('getMapStreamsArgs', () => { test('getMapStreamsArgs, subtitles to matroska', () => { const outFormat = 'matroska'; - const streams = [ - { index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text' }, + const streams: LiteFFprobeStream[] = [ + { index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text', time_base: '', disposition: makeDisposition() }, ]; expect(getMapStreamsArgs({ @@ -136,7 +137,7 @@ test('getStreamIdsToCopy, includeAllStreams false', () => { test('srt output', () => { expect(getMapStreamsArgs({ - allFilesMeta: { [path]: { streams: [{ index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text' }] } }, + allFilesMeta: { [path]: { streams: [{ index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text', time_base: '', disposition: makeDisposition() }] } }, copyFileStreams: [{ path, streamIds: [0] }], outFormat: 'srt', })).toEqual([ @@ -146,7 +147,7 @@ test('srt output', () => { test('webvtt output', () => { expect(getMapStreamsArgs({ - allFilesMeta: { [path]: { streams: [{ index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text' }] } }, + allFilesMeta: { [path]: { streams: [{ index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text', time_base: '', disposition: makeDisposition() }] } }, copyFileStreams: [{ path, streamIds: [0] }], outFormat: 'webvtt', })).toEqual([ @@ -156,7 +157,7 @@ test('webvtt output', () => { test('ass output', () => { expect(getMapStreamsArgs({ - allFilesMeta: { [path]: { streams: [{ index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text' }] } }, + allFilesMeta: { [path]: { streams: [{ index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text', time_base: '', disposition: makeDisposition() }] } }, copyFileStreams: [{ path, streamIds: [0] }], outFormat: 'ass', })).toEqual([ diff --git a/src/renderer/src/util/streams.ts b/src/renderer/src/util/streams.ts index 5d9018e6..6c0b491d 100644 --- a/src/renderer/src/util/streams.ts +++ b/src/renderer/src/util/streams.ts @@ -1,5 +1,6 @@ +import invariant from 'tiny-invariant'; import { FFprobeStream, FFprobeStreamDisposition } from '../../../../ffprobe'; -import { ChromiumHTMLAudioElement, ChromiumHTMLVideoElement } from '../types'; +import { AllFilesMeta, ChromiumHTMLAudioElement, ChromiumHTMLVideoElement, CopyfileStreams, LiteFFprobeStream } from '../types'; // https://www.ffmpeg.org/doxygen/3.2/libavutil_2utils_8c_source.html#l00079 const defaultProcessedCodecTypes = new Set([ @@ -113,15 +114,15 @@ export const isMov = (format: string | undefined) => format != null && ['ismv', type GetVideoArgsFn = (a: { streamIndex: number, outputIndex: number }) => string[] | undefined; -function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition = false, getVideoArgs = () => undefined }: { - stream: FFprobeStream, outputIndex: number, outFormat: string | undefined, manuallyCopyDisposition?: boolean | undefined, getVideoArgs?: GetVideoArgsFn | undefined +function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition = false, getVideoArgs = () => undefined, areWeCutting }: { + stream: LiteFFprobeStream, outputIndex: number, outFormat: string | undefined, manuallyCopyDisposition?: boolean | undefined, getVideoArgs?: GetVideoArgsFn | undefined, areWeCutting: boolean | undefined }) { let args: string[] = []; - function addArgs(...newArgs) { + function addArgs(...newArgs: string[]) { args.push(...newArgs); } - function addCodecArgs(codec) { + function addCodecArgs(codec: string) { addArgs(`-c:${outputIndex}`, codec); } @@ -203,20 +204,27 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi return args; } -export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, copyFileStreams, manuallyCopyDisposition, getVideoArgs }: { - startIndex?: number, outFormat: string | undefined, allFilesMeta, copyFileStreams: { streamIds: number[], path: string }[], manuallyCopyDisposition?: boolean, getVideoArgs?: GetVideoArgsFn, +export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, copyFileStreams, manuallyCopyDisposition, getVideoArgs, areWeCutting }: { + startIndex?: number, + outFormat: string | undefined, + allFilesMeta: Record>, + copyFileStreams: CopyfileStreams, + manuallyCopyDisposition?: boolean, + getVideoArgs?: GetVideoArgsFn, + areWeCutting?: boolean, }) { let args: string[] = []; let outputIndex = startIndex; copyFileStreams.forEach(({ streamIds, path }, fileIndex) => { streamIds.forEach((streamId) => { - const { streams } = allFilesMeta[path]; + const { streams } = allFilesMeta[path]!; const stream = streams.find((s) => s.index === streamId); + invariant(stream != null); args = [ ...args, '-map', `${fileIndex}:${streamId}`, - ...getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition, getVideoArgs }), + ...getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition, getVideoArgs, areWeCutting }), ]; outputIndex += 1; }); @@ -232,16 +240,14 @@ export function shouldCopyStreamByDefault(stream: FFprobeStream) { export const attachedPicDisposition = 'attached_pic'; -export type LiteFFprobeStream = Pick; - -export function isStreamThumbnail(stream: LiteFFprobeStream) { +export function isStreamThumbnail(stream: Pick) { return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1; } -export const getAudioStreams = (streams: T[]) => streams.filter((stream) => stream.codec_type === 'audio'); -export const getRealVideoStreams = (streams: T[]) => streams.filter((stream) => stream.codec_type === 'video' && !isStreamThumbnail(stream)); -export const getSubtitleStreams = (streams: T[]) => streams.filter((stream) => stream.codec_type === 'subtitle'); -export const isGpsStream = (stream: T) => stream.codec_type === 'subtitle' && stream.tags?.['handler_name'] === '\u0010DJI.Subtitle'; +export const getAudioStreams = >(streams: T[]) => streams.filter((stream) => stream.codec_type === 'audio'); +export const getRealVideoStreams = >(streams: T[]) => streams.filter((stream) => stream.codec_type === 'video' && !isStreamThumbnail(stream)); +export const getSubtitleStreams = >(streams: T[]) => streams.filter((stream) => stream.codec_type === 'subtitle'); +export const isGpsStream = >(stream: T) => stream.codec_type === 'subtitle' && stream.tags?.['handler_name'] === '\u0010DJI.Subtitle'; // videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes const getHtml5TrackId = (ffmpegTrackIndex: number) => String(ffmpegTrackIndex + 1); @@ -365,7 +371,7 @@ export function isAudioDefinitelyNotSupported(streams: FFprobeStream[]) { return audioStreams.every((stream) => ['ac3', 'eac3'].includes(stream.codec_name)); } -export function getVideoTimebase(videoStream: FFprobeStream) { +export function getVideoTimebase(videoStream: Pick) { const timebaseMatch = videoStream.time_base && videoStream.time_base.split('/'); if (timebaseMatch) { const timebaseParsed = parseInt(timebaseMatch[1]!, 10);