implement experimental lossy encoding mode #372

e.g. `--lossy-mode "{ videoEncoder: 'libx264' }"`
pull/2531/head
Mikael Finstad 11 months ago
parent af24a14467
commit 25082978d6
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -12,6 +12,7 @@ import remote from '@electron/remote/main';
import { stat } from 'node:fs/promises';
import assert from 'node:assert';
import timers from 'node:timers/promises';
import { z } from 'zod';
import logger from './logger.js';
import menu from './menu.js';
@ -220,12 +221,19 @@ function parseCliArgs(rawArgv = process.argv) {
return yargsParser(argsWithoutAppName, {
boolean: ['allow-multiple-instances', 'disable-networking'],
string: ['settings-json', 'config-dir'],
string: ['settings-json', 'config-dir', 'lossy-mode'],
});
}
const argv = parseCliArgs();
const lossyModeSchema = z.object({ videoEncoder: z.union([z.literal('libx264'), z.literal('libx265'), z.literal('libsvtav1')]) });
// eslint-disable-next-line prefer-destructuring
const lossyMode = argv['lossyMode'] ? lossyModeSchema.parse(JSON5.parse(argv['lossyMode'])) : undefined;
export { lossyMode };
export type LossyMode = z.infer<typeof lossyModeSchema>;
if (argv['localesPath'] != null) i18nCommon.setCustomLocalesPath(argv['localesPath']);

@ -108,7 +108,7 @@ const { exists } = window.require('fs-extra');
const { lstat } = window.require('fs/promises');
const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path');
const { focusWindow, hasDisabledNetworking, quitApp, pathToFileURL, setProgressBar, sendOsNotification } = window.require('@electron/remote').require('./index.js');
const { focusWindow, hasDisabledNetworking, quitApp, pathToFileURL, setProgressBar, sendOsNotification, lossyMode } = window.require('@electron/remote').require('./index.js');
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
@ -164,7 +164,7 @@ function App() {
const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState<number>();
const [editingSegmentTags, setEditingSegmentTags] = useState<SegmentTags>();
const [mediaSourceQuality, setMediaSourceQuality] = useState(0);
const [smartCutBitrate, setSmartCutBitrate] = useState<number | undefined>();
const [encBitrate, setEncBitrate] = useState<number | undefined>();
const [exportCount, setExportCount] = useState(0);
const [maxKeyframes, setMaxKeyframes] = useState(1000);
@ -654,10 +654,11 @@ function App() {
const areWeCutting = useMemo(() => segmentsToExport.some(({ start, end }) => isCuttingStart(start) || isCuttingEnd(end, fileDuration)), [fileDuration, segmentsToExport]);
const needSmartCut = areWeCutting && enableSmartCut;
const isEncoding = needSmartCut || lossyMode != null;
const {
concatFiles, html5ifyDummy, cutMultiple, concatCutSegments, html5ify, fixInvalidDuration, extractStreams, tryDeleteFiles,
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate: smartCutBitrate, appendFfmpegCommandLog });
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate: encBitrate, appendFfmpegCommandLog });
const { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } = useFrameCapture({ appendFfmpegCommandLog, formatTimecode, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart });
@ -2759,7 +2760,7 @@ function App() {
{/* Dialogs */}
<ExportConfirm areWeCutting={areWeCutting} segmentsOrInverse={segmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} mergedFileTemplate={mergedFileTemplateOrDefault} setMergedFileTemplate={setMergedFileTemplate} generateOutSegFileNames={generateOutSegFileNames} generateMergedFileNames={generateMergedFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} smartCutBitrate={smartCutBitrate} setSmartCutBitrate={setSmartCutBitrate} toggleSettings={toggleSettings} outputPlaybackRate={outputPlaybackRate} />
<ExportConfirm areWeCutting={areWeCutting} segmentsOrInverse={segmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} mergedFileTemplate={mergedFileTemplateOrDefault} setMergedFileTemplate={setMergedFileTemplate} generateOutSegFileNames={generateOutSegFileNames} generateMergedFileNames={generateMergedFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} isEncoding={isEncoding} encBitrate={encBitrate} setEncBitrate={setEncBitrate} toggleSettings={toggleSettings} outputPlaybackRate={outputPlaybackRate} lossyMode={lossyMode} />
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}>
{mainStreams && filePath != null && (

@ -26,6 +26,7 @@ import TextInput from './TextInput';
import { UseSegments } from '../hooks/useSegments';
import ExportDialog from './ExportDialog';
import ToggleExportConfirm from './ToggleExportConfirm';
import { LossyMode } from '../../../main';
const outDirStyle: CSSProperties = { ...highlightedTextStyle, wordBreak: 'break-all', cursor: 'pointer' };
@ -92,10 +93,12 @@ function ExportConfirm({
segmentsOrInverse,
mainCopiedThumbnailStreams,
needSmartCut,
smartCutBitrate,
setSmartCutBitrate,
isEncoding,
encBitrate,
setEncBitrate,
toggleSettings,
outputPlaybackRate,
lossyMode,
} : {
areWeCutting: boolean,
segmentsToExport: SegmentToExport[],
@ -119,10 +122,12 @@ function ExportConfirm({
segmentsOrInverse: UseSegments['segmentsOrInverse'],
mainCopiedThumbnailStreams: FFprobeStream[],
needSmartCut: boolean,
smartCutBitrate: number | undefined,
setSmartCutBitrate: Dispatch<SetStateAction<number | undefined>>,
isEncoding: boolean,
encBitrate: number | undefined,
setEncBitrate: Dispatch<SetStateAction<number | undefined>>,
toggleSettings: () => void,
outputPlaybackRate: number,
lossyMode: LossyMode | undefined,
}) {
const { t } = useTranslation();
@ -147,8 +152,8 @@ function ExportConfirm({
movFastStart: isMov && isIpod && !movFastStart ? { warning: true, text: t('For the ipod format, it is recommended to activate this option') } : undefined,
preserveMovData: isMov && isIpod && preserveMovData ? { warning: true, text: t('For the ipod format, it is recommended to deactivate this option') } : undefined,
smartCut: areWeCutting && needSmartCut ? { warning: true, text: t('Smart cut is experimental and will not work on all files.') } : undefined,
cutMode: areWeCutting && !needSmartCut && !keyframeCut ? { text: t('Note: Keyframe cut is recommended for most common files') } : undefined,
avoidNegativeTs: !needSmartCut ? (() => {
cutMode: areWeCutting && !isEncoding && !keyframeCut ? { text: t('Note: Keyframe cut is recommended for most common files') } : undefined,
avoidNegativeTs: !isEncoding ? (() => {
if (willMerge) {
if (avoidNegativeTs !== 'make_non_negative') {
return { text: t('When merging, it\'s generally recommended to set this to "make_non_negative"') };
@ -181,7 +186,7 @@ function ExportConfirm({
specific,
totalNum: generic.filter((n) => n.warning).length + Object.values(specific).filter((n) => n != null && n.warning).length,
};
}, [areWeCutting, areWeCuttingProblematicStreams, avoidNegativeTs, effectiveExportMode, enableOverwriteOutput, isIpod, isMov, keyframeCut, mainCopiedThumbnailStreams, movFastStart, needSmartCut, outFormat, outputPlaybackRate, preserveMovData, t, willMerge]);
}, [areWeCutting, areWeCuttingProblematicStreams, avoidNegativeTs, effectiveExportMode, enableOverwriteOutput, isEncoding, isIpod, isMov, keyframeCut, mainCopiedThumbnailStreams, movFastStart, needSmartCut, outFormat, outputPlaybackRate, preserveMovData, t, willMerge]);
const exportModeDescription = useMemo(() => ({
segments_to_chapters: t('Don\'t cut the file, but instead export an unmodified original which has chapters generated from segments'),
@ -266,15 +271,15 @@ function ExportConfirm({
const canEditSegTemplate = !willMerge || !autoDeleteMergedSegments;
const handleSmartCutBitrateToggle = useCallback((checked: boolean) => {
setSmartCutBitrate(() => (checked ? undefined : 10000));
}, [setSmartCutBitrate]);
const handleEncBitrateToggle = useCallback((checked: boolean) => {
setEncBitrate(() => (checked ? undefined : 10000));
}, [setEncBitrate]);
const handleSmartCutBitrateChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const handleEncBitrateChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const v = parseInt(e.target.value, 10);
if (Number.isNaN(v) || v <= 0) return;
setSmartCutBitrate(v);
}, [setSmartCutBitrate]);
setEncBitrate(v);
}, [setEncBitrate]);
return (
<ExportDialog
@ -541,44 +546,58 @@ function ExportConfirm({
</td>
</tr>
{needSmartCut && (
{!isEncoding && (
<tr>
<td>
{t('Keyframe cut mode')}
{renderNotice(notices.specific['cutMode'])}
</td>
<td>
<Switch checked={keyframeCut} onCheckedChange={() => toggleKeyframeCut()} />
</td>
<td>
{renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? <HelpIcon onClick={onKeyframeCutHelpPress} />}
</td>
</tr>
)}
</>
)}
{isEncoding && (
<tr>
<td>
{t('Smart cut auto detect bitrate')}
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
{smartCutBitrate != null && (
{encBitrate != null && (
<>
<TextInput value={smartCutBitrate} onChange={handleSmartCutBitrateChange} style={{ width: '4em', flexGrow: 0, marginRight: '.3em' }} />
<TextInput value={encBitrate} onChange={handleEncBitrateChange} style={{ width: '4em', flexGrow: 0, marginRight: '.3em' }} />
<span style={{ marginRight: '.3em' }}>{t('kbit/s')}</span>
</>
)}
<span><Switch checked={smartCutBitrate == null} onCheckedChange={handleSmartCutBitrateToggle} /></span>
<span><Switch checked={encBitrate == null} onCheckedChange={handleEncBitrateToggle} /></span>
</div>
</td>
<td />
</tr>
)}
{!needSmartCut && (
{lossyMode != null && (
<tr>
<td>
{t('Keyframe cut mode')}
{renderNotice(notices.specific['cutMode'])}
Lossy mode
</td>
<td>
<Switch checked={keyframeCut} onCheckedChange={() => toggleKeyframeCut()} />
</td>
<td>
{renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? <HelpIcon onClick={onKeyframeCutHelpPress} />}
<Switch disabled checked={lossyMode != null} />
<div>{lossyMode.videoEncoder}</div>
</td>
<td />
</tr>
)}
</>
)}
{!needSmartCut && (
{!isEncoding && (
<tr>
<td>
&quot;ffmpeg&quot; <code className="highlighted">avoid_negative_ts</code>

@ -7,11 +7,12 @@ import invariant from 'tiny-invariant';
import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration, isMac } from '../util';
import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, getExperimentalArgs, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat, RefuseOverwriteError, runFfmpeg } from '../ffmpeg';
import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams';
import { getSmartCutParams } from '../smartcut';
import { needsSmartCut, getCodecParams } from '../smartcut';
import { getGuaranteedSegments, isDurationValid } from '../segments';
import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs, Html5ifyMode, PreserveMetadata } from '../../../../types';
import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types';
import { LossyMode } from '../../../main';
const { join, resolve, dirname } = window.require('path');
const { writeFile, mkdir, access, constants: { F_OK, W_OK } } = window.require('fs/promises');
@ -76,17 +77,18 @@ async function pathExists(path: string) {
}
}
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate, appendFfmpegCommandLog }: {
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate, appendFfmpegCommandLog }: {
filePath: string | undefined,
treatInputFileModifiedTimeAsStart: boolean,
treatOutputFileModifiedTimeAsStart: boolean | null | undefined,
enableOverwriteOutput: boolean,
needSmartCut: boolean,
isEncoding: boolean,
lossyMode: LossyMode | undefined,
outputPlaybackRate: number,
cutFromAdjustmentFrames: number,
cutToAdjustmentFrames: number,
appendLastCommandsLog: (a: string) => void,
smartCutCustomBitrate: number | undefined,
encCustomBitrate: number | undefined,
appendFfmpegCommandLog: (args: string[]) => void,
}) {
const shouldSkipExistingFile = useCallback(async (path: string) => {
@ -524,6 +526,9 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// then it will cut the part *from* the keyframe to "end", and concat them together and return the concated file
// so that for the calling code it looks as if it's just a normal segment
async function cutSegment({ start: desiredCutFrom, end: cutTo }: { start: number, end: number }, i: number) {
const onProgress = (progress: number) => onSingleProgress(i, progress / 2);
const onConcatProgress = (progress: number) => onSingleProgress(i, (1 + progress) / 2);
const finalOutPath = join(outputDir, outSegFileNames[i]!);
if (await shouldSkipExistingFile(finalOutPath)) return { path: finalOutPath, created: false };
@ -533,7 +538,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const actualOutputDir = dirname(finalOutPath);
if (actualOutputDir !== outputDir) await mkdir(actualOutputDir, { recursive: true });
if (!needSmartCut) {
if (!isEncoding) {
// simple lossless cut
invariant(outFormat != null);
await losslessCutSingle({
cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath: finalOutPath, copyFileStreams, keyframeCut, avoidNegativeTs, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, onProgress: (progress) => onSingleProgress(i, progress),
@ -541,6 +547,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
return { path: finalOutPath, created: true };
}
// we are probably encoding (smart cut or lossy mode)
invariant(filePath != null);
// smart cut only supports cutting main file (no externally added files)
@ -551,36 +558,47 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
return match ? [match] : [];
});
const { losslessCutFrom, segmentNeedsSmartCut, videoCodec, videoBitrate: detectedVideoBitrate, videoStreamIndex, videoTimebase } = await getSmartCutParams({ path: filePath, fileDuration, desiredCutFrom, streams: streamsToCopyFromMainFile });
if (segmentNeedsSmartCut && !detectedFps) throw new Error('Smart cut is not possible when FPS is unknown');
const sourceCodecParams = await getCodecParams({ path: filePath, fileDuration, streams: streamsToCopyFromMainFile });
const { videoStream, videoTimebase } = sourceCodecParams;
console.log('Smart cut on video stream', videoStreamIndex);
const onProgress = (progress: number) => onSingleProgress(i, progress / 2);
const onConcatProgress = (progress: number) => onSingleProgress(i, (1 + progress) / 2);
const videoCodec = lossyMode ? lossyMode.videoEncoder : sourceCodecParams.videoCodec;
const copyFileStreamsFiltered = [{
path: filePath,
// with smart cut, we only copy/cut *one* video stream, and *all* other non-video streams (main file only)
streamIds: streamsToCopyFromMainFile.filter((stream) => stream.index === videoStreamIndex || stream.codec_type !== 'video').map((stream) => stream.index),
streamIds: streamsToCopyFromMainFile.filter((stream) => stream.index === videoStream.index || stream.codec_type !== 'video').map((stream) => stream.index),
}];
// eslint-disable-next-line no-shadow
async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }: { cutFrom: number, cutTo: number, outPath: string }) {
if (await shouldSkipExistingFile(outPath)) return;
if (videoCodec == null || detectedVideoBitrate == null || videoTimebase == null) throw new Error();
invariant(videoCodec != null);
invariant(sourceCodecParams.videoBitrate != null);
invariant(sourceCodecParams.videoTimebase != null);
invariant(filePath != null);
invariant(outFormat != null);
await cutEncodeSmartPart({ cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate: smartCutCustomBitrate != null ? smartCutCustomBitrate * 1000 : detectedVideoBitrate, videoStreamIndex, videoTimebase, allFilesMeta, copyFileStreams: copyFileStreamsFiltered, ffmpegExperimental });
await cutEncodeSmartPart({ cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate: encCustomBitrate != null ? encCustomBitrate * 1000 : sourceCodecParams.videoBitrate, videoStreamIndex: videoStream.index, videoTimebase: sourceCodecParams.videoTimebase, allFilesMeta, copyFileStreams: copyFileStreamsFiltered, ffmpegExperimental });
}
const cutEncodeWholePart = async () => {
await cutEncodeSmartPartWrapper({ cutFrom: desiredCutFrom, cutTo, outPath: finalOutPath });
return { path: finalOutPath, created: true };
};
if (lossyMode) {
console.log('Lossy mode: cutting/encoding the whole segment', { desiredCutFrom, cutTo });
return cutEncodeWholePart();
}
const { losslessCutFrom, segmentNeedsSmartCut } = await needsSmartCut({ path: filePath, desiredCutFrom, videoStream });
if (segmentNeedsSmartCut && !detectedFps) throw new Error('Smart cut is not possible when FPS is unknown');
console.log('Smart cut on video stream', videoStream.index);
// If we are cutting within two keyframes, just encode the whole part and return that
// See https://github.com/mifi/lossless-cut/pull/1267#issuecomment-1236381740
if (segmentNeedsSmartCut && losslessCutFrom > cutTo) {
console.log('Segment is between two keyframes, cutting/encoding the whole segment', { desiredCutFrom, losslessCutFrom, cutTo });
await cutEncodeSmartPartWrapper({ cutFrom: desiredCutFrom, cutTo, outPath: finalOutPath });
return { path: finalOutPath, created: true };
return cutEncodeWholePart();
}
invariant(outFormat != null);
@ -631,7 +649,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
} finally {
if (chaptersPath) await tryDeleteFiles([chaptersPath]);
}
}, [needSmartCut, filePath, losslessCutSingle, shouldSkipExistingFile, cutEncodeSmartPart, smartCutCustomBitrate, concatFiles]);
}, [shouldSkipExistingFile, isEncoding, filePath, losslessCutSingle, cutEncodeSmartPart, encCustomBitrate, lossyMode, concatFiles]);
const concatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, chapterNames, preserveMetadataOnMerge, mergedOutFilePath }: {
customOutDir: string | undefined,

@ -8,20 +8,11 @@ const { stat } = window.require('fs-extra');
const mapVideoCodec = (codec: string) => ({ av1: 'libsvtav1' }[codec] ?? codec);
// eslint-disable-next-line import/prefer-default-export
export async function getSmartCutParams({ path, fileDuration, desiredCutFrom, streams }: {
export async function needsSmartCut({ path, desiredCutFrom, videoStream }: {
path: string,
fileDuration: number | undefined,
desiredCutFrom: number,
streams: Pick<FFprobeStream, 'time_base' | 'codec_type' | 'disposition' | 'index' | 'bit_rate' | 'codec_name'>[],
videoStream: Pick<FFprobeStream, 'index'>,
}) {
const videoStreams = getRealVideoStreams(streams);
if (videoStreams.length > 1) throw new Error('Can only smart cut video with exactly one video stream');
const [videoStream] = videoStreams;
if (videoStream == null) throw new Error('Smart cut only works on videos');
const readKeyframes = async (window: number) => readKeyframesAroundTime({ filePath: path, streamIndex: videoStream.index, aroundTime: desiredCutFrom, window });
let keyframes = await readKeyframes(10);
@ -32,7 +23,6 @@ export async function getSmartCutParams({ path, fileDuration, desiredCutFrom, st
return {
losslessCutFrom: keyframeAtExactTime.time,
videoStreamIndex: videoStream.index,
segmentNeedsSmartCut: false,
};
}
@ -48,12 +38,32 @@ export async function getSmartCutParams({ path, fileDuration, desiredCutFrom, st
console.log('Smart cut from keyframe', { keyframe: nextKeyframe.time, desiredCutFrom });
return {
losslessCutFrom: nextKeyframe.time,
segmentNeedsSmartCut: true,
};
}
// eslint-disable-next-line import/prefer-default-export
export async function getCodecParams({ path, fileDuration, streams }: {
path: string,
fileDuration: number | undefined,
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');
const [videoStream] = videoStreams;
if (videoStream == null) throw new Error('Smart cut only works on videos');
let videoBitrate = parseInt(videoStream.bit_rate!, 10);
if (Number.isNaN(videoBitrate)) {
console.warn('Unable to detect input bitrate');
console.warn('Unable to detect input bitrate.');
const stats = await stat(path);
if (fileDuration == null) throw new Error('Video duration is unknown, cannot estimate bitrate');
videoBitrate = (stats.size * 8) / fileDuration;
console.warn('Estimated bitrate.', videoBitrate / 1e6, 'Mbit/s');
}
// to account for inaccuracies and quality loss
@ -74,9 +84,7 @@ export async function getSmartCutParams({ path, fileDuration, desiredCutFrom, st
// const videoProfile = parseProfile(videoStream);
return {
losslessCutFrom: nextKeyframe.time,
videoStreamIndex: videoStream.index,
segmentNeedsSmartCut: true,
videoStream,
videoCodec,
videoBitrate: Math.floor(videoBitrate),
videoTimebase: timebase,

Loading…
Cancel
Save