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:
parent
dac0ac9f44
commit
1a674fa735
@ -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]);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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');
|
||||||
|
@ -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[];
|
||||||
}>
|
}>
|
||||||
|
@ -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([
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user