1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 02:12:30 +01:00

improve types

This commit is contained in:
Mikael Finstad 2024-08-27 20:16:27 +02:00
parent dac0ac9f44
commit 1a674fa735
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
7 changed files with 49 additions and 39 deletions

View File

@ -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({ 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]); }, [showNotification]);
const showPreviewFileLoadedMessage = useCallback((fileName) => { const showPreviewFileLoadedMessage = useCallback((fileName: string) => {
showNotification({ icon: 'info', text: i18n.t('Loaded existing preview file: {{ fileName }}', { fileName }) }); showNotification({ icon: 'info', text: i18n.t('Loaded existing preview file: {{ fileName }}', { fileName }) });
}, [showNotification]); }, [showNotification]);

View File

@ -5,11 +5,12 @@ import Timecode from 'smpte-timecode';
import minBy from 'lodash/minBy'; import minBy from 'lodash/minBy';
import invariant from 'tiny-invariant'; 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 { getSuffixedOutPath, isExecaError } from './util';
import { isDurationValid } from './segments'; import { isDurationValid } from './segments';
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe'; import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe';
import { parseSrt, parseSrtToSegments } from './edlFormats'; import { parseSrt, parseSrtToSegments } from './edlFormats';
import { CopyfileStreams, LiteFFprobeStream } from './types';
const FileType = window.require('file-type'); const FileType = window.require('file-type');
const { pathExists } = window.require('fs-extra'); const { pathExists } = window.require('fs-extra');
@ -58,7 +59,7 @@ export function isCuttingEnd(cutTo: number, duration: number | undefined) {
return cutTo < duration; return cutTo < duration;
} }
function getIntervalAroundTime(time, window) { function getIntervalAroundTime(time: number, window: number) {
return { return {
from: Math.max(time - window / 2, 0), from: Math.max(time - window / 2, 0),
to: time + window / 2, to: time + window / 2,
@ -681,7 +682,7 @@ export const getVideoTimescaleArgs = (videoTimebase: number | undefined) => (vid
// inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e // inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e
export async function cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoTimebase, allFilesMeta, copyFileStreams, videoStreamIndex, ffmpegExperimental }: { 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 }) { function getVideoArgs({ streamIndex, outputIndex }: { streamIndex: number, outputIndex: number }) {
if (streamIndex !== videoStreamIndex) return undefined; if (streamIndex !== videoStreamIndex) return undefined;

View File

@ -432,8 +432,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
console.log('Smart cut on video stream', videoStreamIndex); console.log('Smart cut on video stream', videoStreamIndex);
const onCutProgress = (progress) => onSingleProgress(i, progress / 2); const onCutProgress = (progress: number) => onSingleProgress(i, progress / 2);
const onConcatProgress = (progress) => onSingleProgress(i, (1 + progress) / 2); const onConcatProgress = (progress: number) => onSingleProgress(i, (1 + progress) / 2);
const copyFileStreamsFiltered = [{ const copyFileStreamsFiltered = [{
path: filePath, path: filePath,

View File

@ -10,7 +10,7 @@ const mapVideoCodec = (codec: string) => ({ av1: 'libsvtav1' }[codec] ?? codec);
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export async function getSmartCutParams({ path, videoDuration, desiredCutFrom, streams }: { 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<FFprobeStream, 'time_base' | 'codec_type' | 'disposition' | 'index' | 'bit_rate' | 'codec_name'>[],
}) { }) {
const videoStreams = getRealVideoStreams(streams); const videoStreams = getRealVideoStreams(streams);
if (videoStreams.length > 1) throw new Error('Can only smart cut video with exactly one video stream'); if (videoStreams.length > 1) throw new Error('Can only smart cut video with exactly one video stream');

View File

@ -119,8 +119,10 @@ export type CopyfileStreams = {
export interface Chapter { start: number, end: number, name?: string | undefined } export interface Chapter { start: number, end: number, name?: string | undefined }
export type LiteFFprobeStream = Pick<FFprobeStream, 'index' | 'codec_type' | 'codec_tag' | 'codec_name' | 'disposition' | 'tags' | 'sample_rate' | 'time_base'>;
export type AllFilesMeta = Record<string, { export type AllFilesMeta = Record<string, {
streams: FFprobeStream[]; streams: LiteFFprobeStream[];
formatData: FFprobeFormat; formatData: FFprobeFormat;
chapters: FFprobeChapter[]; chapters: FFprobeChapter[];
}> }>

View File

@ -1,7 +1,8 @@
import { test, expect } from 'vitest'; import { test, expect } from 'vitest';
import { LiteFFprobeStream, getMapStreamsArgs, getStreamIdsToCopy } from './streams'; import { getMapStreamsArgs, getStreamIdsToCopy } from './streams';
import { FFprobeStreamDisposition } from '../../../../ffprobe'; import { FFprobeStreamDisposition } from '../../../../ffprobe';
import { LiteFFprobeStream } from '../types';
const makeDisposition = (override?: Partial<FFprobeStreamDisposition>): FFprobeStreamDisposition => ({ const makeDisposition = (override?: Partial<FFprobeStreamDisposition>): FFprobeStreamDisposition => ({
@ -26,14 +27,14 @@ const makeDisposition = (override?: Partial<FFprobeStreamDisposition>): FFprobeS
}); });
const streams1: LiteFFprobeStream[] = [ const streams1: LiteFFprobeStream[] = [
{ index: 0, codec_type: 'video', codec_tag: '0x0000', codec_name: 'mjpeg', disposition: makeDisposition({ attached_pic: 1 }) }, { 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', disposition: makeDisposition() }, { 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', 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', 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', 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', 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: '', 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', disposition: makeDisposition() }, { index: 7, codec_type: 'subtitle', codec_tag: '0x0000', codec_name: 'subrip', time_base: '', disposition: makeDisposition() },
]; ];
const path = '/path/to/file'; const path = '/path/to/file';
@ -66,8 +67,8 @@ test('getMapStreamsArgs', () => {
test('getMapStreamsArgs, subtitles to matroska', () => { test('getMapStreamsArgs, subtitles to matroska', () => {
const outFormat = 'matroska'; const outFormat = 'matroska';
const streams = [ const streams: LiteFFprobeStream[] = [
{ index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text' }, { index: 0, codec_type: 'subtitle', codec_tag: '0x67337874', codec_name: 'mov_text', time_base: '', disposition: makeDisposition() },
]; ];
expect(getMapStreamsArgs({ expect(getMapStreamsArgs({
@ -136,7 +137,7 @@ test('getStreamIdsToCopy, includeAllStreams false', () => {
test('srt output', () => { test('srt output', () => {
expect(getMapStreamsArgs({ 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] }], copyFileStreams: [{ path, streamIds: [0] }],
outFormat: 'srt', outFormat: 'srt',
})).toEqual([ })).toEqual([
@ -146,7 +147,7 @@ test('srt output', () => {
test('webvtt output', () => { test('webvtt output', () => {
expect(getMapStreamsArgs({ 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] }], copyFileStreams: [{ path, streamIds: [0] }],
outFormat: 'webvtt', outFormat: 'webvtt',
})).toEqual([ })).toEqual([
@ -156,7 +157,7 @@ test('webvtt output', () => {
test('ass output', () => { test('ass output', () => {
expect(getMapStreamsArgs({ 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] }], copyFileStreams: [{ path, streamIds: [0] }],
outFormat: 'ass', outFormat: 'ass',
})).toEqual([ })).toEqual([

View File

@ -1,5 +1,6 @@
import invariant from 'tiny-invariant';
import { FFprobeStream, FFprobeStreamDisposition } from '../../../../ffprobe'; 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 // https://www.ffmpeg.org/doxygen/3.2/libavutil_2utils_8c_source.html#l00079
const defaultProcessedCodecTypes = new Set([ 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; type GetVideoArgsFn = (a: { streamIndex: number, outputIndex: number }) => string[] | undefined;
function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition = false, getVideoArgs = () => undefined }: { function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition = false, getVideoArgs = () => undefined, areWeCutting }: {
stream: FFprobeStream, outputIndex: number, outFormat: string | undefined, manuallyCopyDisposition?: boolean | undefined, getVideoArgs?: GetVideoArgsFn | undefined stream: LiteFFprobeStream, outputIndex: number, outFormat: string | undefined, manuallyCopyDisposition?: boolean | undefined, getVideoArgs?: GetVideoArgsFn | undefined, areWeCutting: boolean | undefined
}) { }) {
let args: string[] = []; let args: string[] = [];
function addArgs(...newArgs) { function addArgs(...newArgs: string[]) {
args.push(...newArgs); args.push(...newArgs);
} }
function addCodecArgs(codec) { function addCodecArgs(codec: string) {
addArgs(`-c:${outputIndex}`, codec); addArgs(`-c:${outputIndex}`, codec);
} }
@ -203,20 +204,27 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
return args; return args;
} }
export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, copyFileStreams, manuallyCopyDisposition, getVideoArgs }: { export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, copyFileStreams, manuallyCopyDisposition, getVideoArgs, areWeCutting }: {
startIndex?: number, outFormat: string | undefined, allFilesMeta, copyFileStreams: { streamIds: number[], path: string }[], manuallyCopyDisposition?: boolean, getVideoArgs?: GetVideoArgsFn, startIndex?: number,
outFormat: string | undefined,
allFilesMeta: Record<string, Pick<AllFilesMeta[string], 'streams'>>,
copyFileStreams: CopyfileStreams,
manuallyCopyDisposition?: boolean,
getVideoArgs?: GetVideoArgsFn,
areWeCutting?: boolean,
}) { }) {
let args: string[] = []; let args: string[] = [];
let outputIndex = startIndex; let outputIndex = startIndex;
copyFileStreams.forEach(({ streamIds, path }, fileIndex) => { copyFileStreams.forEach(({ streamIds, path }, fileIndex) => {
streamIds.forEach((streamId) => { streamIds.forEach((streamId) => {
const { streams } = allFilesMeta[path]; const { streams } = allFilesMeta[path]!;
const stream = streams.find((s) => s.index === streamId); const stream = streams.find((s) => s.index === streamId);
invariant(stream != null);
args = [ args = [
...args, ...args,
'-map', `${fileIndex}:${streamId}`, '-map', `${fileIndex}:${streamId}`,
...getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition, getVideoArgs }), ...getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition, getVideoArgs, areWeCutting }),
]; ];
outputIndex += 1; outputIndex += 1;
}); });
@ -232,16 +240,14 @@ export function shouldCopyStreamByDefault(stream: FFprobeStream) {
export const attachedPicDisposition = 'attached_pic'; export const attachedPicDisposition = 'attached_pic';
export type LiteFFprobeStream = Pick<FFprobeStream, 'index' | 'codec_type' | 'codec_tag' | 'codec_name' | 'disposition' | 'tags'>; export function isStreamThumbnail(stream: Pick<FFprobeStream, 'codec_type' | 'disposition'>) {
export function isStreamThumbnail(stream: LiteFFprobeStream) {
return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1; return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1;
} }
export const getAudioStreams = <T extends LiteFFprobeStream>(streams: T[]) => streams.filter((stream) => stream.codec_type === 'audio'); export const getAudioStreams = <T extends Pick<FFprobeStream, 'codec_type'>>(streams: T[]) => streams.filter((stream) => stream.codec_type === 'audio');
export const getRealVideoStreams = <T extends LiteFFprobeStream>(streams: T[]) => streams.filter((stream) => stream.codec_type === 'video' && !isStreamThumbnail(stream)); export const getRealVideoStreams = <T extends Pick<FFprobeStream, 'codec_type' | 'disposition'>>(streams: T[]) => streams.filter((stream) => stream.codec_type === 'video' && !isStreamThumbnail(stream));
export const getSubtitleStreams = <T extends LiteFFprobeStream>(streams: T[]) => streams.filter((stream) => stream.codec_type === 'subtitle'); export const getSubtitleStreams = <T extends Pick<FFprobeStream, 'codec_type'>>(streams: T[]) => streams.filter((stream) => stream.codec_type === 'subtitle');
export const isGpsStream = <T extends LiteFFprobeStream>(stream: T) => stream.codec_type === 'subtitle' && stream.tags?.['handler_name'] === '\u0010DJI.Subtitle'; export const isGpsStream = <T extends Pick<FFprobeStream, 'codec_type' | 'tags'>>(stream: T) => stream.codec_type === 'subtitle' && stream.tags?.['handler_name'] === '\u0010DJI.Subtitle';
// videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes // videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes
const getHtml5TrackId = (ffmpegTrackIndex: number) => String(ffmpegTrackIndex + 1); 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)); return audioStreams.every((stream) => ['ac3', 'eac3'].includes(stream.codec_name));
} }
export function getVideoTimebase(videoStream: FFprobeStream) { export function getVideoTimebase(videoStream: Pick<FFprobeStream, 'time_base'>) {
const timebaseMatch = videoStream.time_base && videoStream.time_base.split('/'); const timebaseMatch = videoStream.time_base && videoStream.time_base.split('/');
if (timebaseMatch) { if (timebaseMatch) {
const timebaseParsed = parseInt(timebaseMatch[1]!, 10); const timebaseParsed = parseInt(timebaseMatch[1]!, 10);