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

View File

@ -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;

View File

@ -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,

View File

@ -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<FFprobeStream, 'time_base' | 'codec_type' | 'disposition' | 'index' | 'bit_rate' | 'codec_name'>[],
}) {
const videoStreams = getRealVideoStreams(streams);
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 type LiteFFprobeStream = Pick<FFprobeStream, 'index' | 'codec_type' | 'codec_tag' | 'codec_name' | 'disposition' | 'tags' | 'sample_rate' | 'time_base'>;
export type AllFilesMeta = Record<string, {
streams: FFprobeStream[];
streams: LiteFFprobeStream[];
formatData: FFprobeFormat;
chapters: FFprobeChapter[];
}>

View File

@ -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>): FFprobeStreamDisposition => ({
@ -26,14 +27,14 @@ const makeDisposition = (override?: Partial<FFprobeStreamDisposition>): 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([

View File

@ -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<string, Pick<AllFilesMeta[string], 'streams'>>,
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<FFprobeStream, 'index' | 'codec_type' | 'codec_tag' | 'codec_name' | 'disposition' | 'tags'>;
export function isStreamThumbnail(stream: LiteFFprobeStream) {
export function isStreamThumbnail(stream: Pick<FFprobeStream, 'codec_type' | 'disposition'>) {
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 getRealVideoStreams = <T extends LiteFFprobeStream>(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 isGpsStream = <T extends LiteFFprobeStream>(stream: T) => stream.codec_type === 'subtitle' && stream.tags?.['handler_name'] === '\u0010DJI.Subtitle';
export const getAudioStreams = <T extends Pick<FFprobeStream, 'codec_type'>>(streams: T[]) => streams.filter((stream) => stream.codec_type === 'audio');
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 Pick<FFprobeStream, 'codec_type'>>(streams: T[]) => streams.filter((stream) => stream.codec_type === '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
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<FFprobeStream, 'time_base'>) {
const timebaseMatch = videoStream.time_base && videoStream.time_base.split('/');
if (timebaseMatch) {
const timebaseParsed = parseInt(timebaseMatch[1]!, 10);