refactor cutEncodeSmartPart

pull/2263/head
Mikael Finstad 11 months ago
parent 714ba033a7
commit b4121056a0
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -1264,7 +1264,7 @@ function App() {
if (await tryOpenProjectPath(getEdlFilePathOld(fp, cod), 'csv')) return;
// OK, we didn't find a project file, instead maybe try to create project (segments) from chapters
const edl = await tryMapChaptersToEdl(chapters);
const edl = tryMapChaptersToEdl(chapters);
if (edl.length > 0 && enableAskForImportChapters && (await askForImportChapters())) {
console.log('Convert chapters to segments', edl);
loadCutSegments(edl);

@ -5,12 +5,11 @@ import Timecode from 'smpte-timecode';
import minBy from 'lodash/minBy';
import invariant from 'tiny-invariant';
import { pcmAudioCodecs, getMapStreamsArgs, isMov } from './util/streams';
import { pcmAudioCodecs, isMov } from './util/streams';
import { isExecaError } from './util';
import { isDurationValid } from './segments';
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe';
import { parseSrt, parseSrtToSegments } from './edlFormats';
import { CopyfileStreams } from './types';
import { UnsupportedFileError } from '../errors';
const { ffmpeg, fileTypePromise } = window.require('@electron/remote').require('./index.js');
@ -18,7 +17,7 @@ const { ffmpeg, fileTypePromise } = window.require('@electron/remote').require('
const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath } = ffmpeg;
export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath };
export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfmpeg, getFfmpegPath, setCustomFfPath };
export class RefuseOverwriteError extends Error {
@ -140,11 +139,11 @@ export async function findKeyframeNearTime({ filePath, streamIndex, time, mode }
// todo this is not in use
// https://stackoverflow.com/questions/14005110/how-to-split-a-video-using-ffmpeg-so-that-each-chunk-starts-with-a-key-frame
// http://kicherer.org/joomla/index.php/de/blog/42-avcut-frame-accurate-video-cutting-with-only-small-quality-loss
export function getSafeCutTime(frames, cutTime, nextMode) {
export function getSafeCutTime(frames: (Frame & { time: number })[], cutTime: number, nextMode: boolean) {
const sigma = 0.01;
const isCloseTo = (time1, time2) => Math.abs(time1 - time2) < sigma;
const isCloseTo = (time1: number, time2: number) => Math.abs(time1 - time2) < sigma;
let index;
let index: number;
if (frames.length < 2) throw new Error(i18n.t('Less than 2 frames found'));
@ -152,7 +151,7 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
index = frames.findIndex((f) => f.keyframe && f.time >= cutTime - sigma);
if (index === -1) throw new Error(i18n.t('Failed to find next keyframe'));
if (index >= frames.length - 1) throw new Error(i18n.t('We are on the last frame'));
const { time } = frames[index];
const { time } = frames[index]!;
if (isCloseTo(time, cutTime)) {
return undefined; // Already on keyframe, no need to modify cut time
}
@ -174,7 +173,7 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
// Last frame of video, no need to modify cut time
return undefined;
}
if (frames[index + 1].keyframe) {
if (frames[index + 1]!.keyframe) {
// Already on frame before keyframe, no need to modify cut time
return undefined;
}
@ -185,7 +184,7 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
if (index === 0) throw new Error(i18n.t('We are on the first keyframe'));
// Use frame before the found keyframe
return frames[index - 1].time;
return frames[index - 1]!.time;
}
export function findNearestKeyFrameTime({ frames, time, direction, fps }: { frames: Frame[], time: number, direction: number, fps: number | undefined }) {
@ -197,7 +196,7 @@ export function findNearestKeyFrameTime({ frames, time, direction, fps }: { fram
return nearestKeyFrame.time;
}
export async function tryMapChaptersToEdl(chapters: FFprobeChapter[]) {
export function tryMapChaptersToEdl(chapters: FFprobeChapter[]) {
try {
return chapters.map((chapter) => {
const start = parseFloat(chapter.start_time);
@ -558,56 +557,3 @@ export async function runFfmpegStartupCheck() {
export const getExperimentalArgs = (ffmpegExperimental: boolean) => (ffmpegExperimental ? ['-strict', 'experimental'] : []);
export const getVideoTimescaleArgs = (videoTimebase: number | undefined) => (videoTimebase != null ? ['-video_track_timescale', String(videoTimebase)] : []);
// 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: CopyfileStreams, videoStreamIndex: number, ffmpegExperimental: boolean,
}) {
function getVideoArgs({ streamIndex, outputIndex }: { streamIndex: number, outputIndex: number }) {
if (streamIndex !== videoStreamIndex) return undefined;
const args = [
`-c:${outputIndex}`, videoCodec,
`-b:${outputIndex}`, String(videoBitrate),
];
// seems like ffmpeg handles this itself well when encoding same source file
// if (videoLevel != null) args.push(`-level:${outputIndex}`, videoLevel);
// if (videoProfile != null) args.push(`-profile:${outputIndex}`, videoProfile);
return args;
}
const mapStreamsArgs = getMapStreamsArgs({
allFilesMeta,
copyFileStreams,
outFormat,
getVideoArgs,
});
const ffmpegArgs = [
'-hide_banner',
// No progress if we set loglevel warning :(
// '-loglevel', 'warning',
'-ss', cutFrom.toFixed(5), // if we don't -ss before -i, seeking will be slow for long files, see https://github.com/mifi/lossless-cut/issues/126#issuecomment-1135451043
'-i', filePath,
'-ss', '0', // If we don't do this, the output seems to start with an empty black after merging with the encoded part
'-t', (cutTo - cutFrom).toFixed(5),
...mapStreamsArgs,
// See https://github.com/mifi/lossless-cut/issues/170
'-ignore_unknown',
...getVideoTimescaleArgs(videoTimebase),
...getExperimentalArgs(ffmpegExperimental),
'-f', outFormat, '-y', outPath,
];
await runFfmpeg(ffmpegArgs);
return ffmpegArgs;
}

@ -5,7 +5,7 @@ import pMap from 'p-map';
import invariant from 'tiny-invariant';
import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration } from '../util';
import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, cutEncodeSmartPart, getExperimentalArgs, html5ify as ffmpegHtml5ify, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat, RefuseOverwriteError, runFfmpeg } from '../ffmpeg';
import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, getExperimentalArgs, html5ify as ffmpegHtml5ify, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat, RefuseOverwriteError, runFfmpeg } from '../ffmpeg';
import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams';
import { getSmartCutParams } from '../smartcut';
import { isDurationValid } from '../segments';
@ -418,6 +418,60 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(videoDuration) ? videoDuration : undefined, treatOutputFileModifiedTimeAsStart });
}, [appendFfmpegCommandLog, cutFromAdjustmentFrames, filePath, getOutputPlaybackRateArgs, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
// inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e
const cutEncodeSmartPart = useCallback(async ({ cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoTimebase, allFilesMeta, copyFileStreams, videoStreamIndex, ffmpegExperimental }: {
cutFrom: number, cutTo: number, outPath: string, outFormat: string, videoCodec: string, videoBitrate: number, videoTimebase: number, allFilesMeta: AllFilesMeta, copyFileStreams: CopyfileStreams, videoStreamIndex: number, ffmpegExperimental: boolean,
}) => {
invariant(filePath != null);
function getVideoArgs({ streamIndex, outputIndex }: { streamIndex: number, outputIndex: number }) {
if (streamIndex !== videoStreamIndex) return undefined;
const args = [
`-c:${outputIndex}`, videoCodec,
`-b:${outputIndex}`, String(videoBitrate),
];
// seems like ffmpeg handles this itself well when encoding same source file
// if (videoLevel != null) args.push(`-level:${outputIndex}`, videoLevel);
// if (videoProfile != null) args.push(`-profile:${outputIndex}`, videoProfile);
return args;
}
const mapStreamsArgs = getMapStreamsArgs({
allFilesMeta,
copyFileStreams,
outFormat,
getVideoArgs,
});
const ffmpegArgs = [
'-hide_banner',
// No progress if we set loglevel warning :(
// '-loglevel', 'warning',
'-ss', cutFrom.toFixed(5), // if we don't -ss before -i, seeking will be slow for long files, see https://github.com/mifi/lossless-cut/issues/126#issuecomment-1135451043
'-i', filePath,
'-ss', '0', // If we don't do this, the output seems to start with an empty black after merging with the encoded part
'-t', (cutTo - cutFrom).toFixed(5),
...mapStreamsArgs,
// See https://github.com/mifi/lossless-cut/issues/170
'-ignore_unknown',
...getVideoTimescaleArgs(videoTimebase),
...getExperimentalArgs(ffmpegExperimental),
'-f', outFormat, '-y', outPath,
];
appendFfmpegCommandLog(ffmpegArgs);
await runFfmpeg(ffmpegArgs);
}, [appendFfmpegCommandLog, filePath]);
const cutMultiple = useCallback(async ({
outputDir, customOutDir, segments, outSegFileNames, videoDuration, rotation, detectedFps, onProgress: onTotalProgress, keyframeCut, copyFileStreams, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, chapters,
}: {
@ -509,8 +563,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
if (videoCodec == null || detectedVideoBitrate == null || videoTimebase == null) throw new Error();
invariant(filePath != null);
invariant(outFormat != null);
const args = await cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate: smartCutCustomBitrate != null ? smartCutCustomBitrate * 1000 : detectedVideoBitrate, videoStreamIndex, videoTimebase, allFilesMeta, copyFileStreams: copyFileStreamsFiltered, ffmpegExperimental });
appendFfmpegCommandLog(args);
await cutEncodeSmartPart({ cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate: smartCutCustomBitrate != null ? smartCutCustomBitrate * 1000 : detectedVideoBitrate, videoStreamIndex, videoTimebase, allFilesMeta, copyFileStreams: copyFileStreamsFiltered, ffmpegExperimental });
}
// If we are cutting within two keyframes, just encode the whole part and return that
@ -573,7 +626,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
} finally {
if (chaptersPath) await tryDeleteFiles([chaptersPath]);
}
}, [needSmartCut, filePath, losslessCutSingle, shouldSkipExistingFile, smartCutCustomBitrate, appendFfmpegCommandLog, concatFiles]);
}, [needSmartCut, filePath, losslessCutSingle, shouldSkipExistingFile, cutEncodeSmartPart, smartCutCustomBitrate, concatFiles]);
const autoConcatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, mergedOutFilePath }: {
customOutDir: string | undefined,

Loading…
Cancel
Save