Work around broken track interleaving in Matroska files

Some Matroska files have their tracks stored non-interleaved, e.g.
with all audio data appended after all video data. ffmpeg reads such
files incorrectly when demuxing multiple streams interleaved: the
output audio can end up with wrong timestamps or be scrambled, while
reading each stream type individually works fine. This caused exported
segments of such files to play back starting from the wrong position.

To fix the problem without relying on ffmpeg behavior:

- Add checkMatroskaTrackInterleaving() in main/ffmpeg.ts: scans only
  EBML cluster headers (no media data is read, so it stays fast even
  for large files) and flags the file when cluster timestamps jump
  backwards by more than 2 seconds.
- Run the check when opening a Matroska file (in loadMedia; skipped
  for files that get html5ified or are not Matroska) and inform the
  user with a toast explaining that the workaround will be applied
  automatically on export.
- Add losslessCutSingleAudioSafe() in useFfmpegOperations: cuts the
  segment in two passes (all streams except audio, then audio only)
  and losslessly merges them, cutting each pass in a way ffmpeg
  handles correctly for such files. Temp files are deleted afterwards;
  stream order, metadata and chapters are preserved. The safe path is
  only used for files flagged at load time; all other files go through
  the existing losslessCutSingle path unchanged.
- Add translations for the new notice (en, zh_Hans).
pull/3014/head
埃博拉酱-机器人 1 month ago
parent 08bcf877d9
commit df8ef7bace

@ -787,6 +787,7 @@
"This experimental feature will re-encode the part of the video from the cutpoint until the next keyframe in order to attempt to make a 100% accurate cut. Only works on some files. I've had success with some h264 files, and only a few h265 files. See more here: {{url}}": "This experimental feature will re-encode the part of the video from the cutpoint until the next keyframe in order to attempt to make a 100% accurate cut. Only works on some files. I've had success with some h264 files, and only a few h265 files. See more here: {{url}}",
"This file contains an audio track that FFmpeg is unable to mux into the MP4 format, so MOV has been auto-selected as the default output format.": "This file contains an audio track that FFmpeg is unable to mux into the MP4 format, so MOV has been auto-selected as the default output format.",
"This file does not have a valid duration. This may cause issues. You can try to fix the file's duration from the File menu": "This file does not have a valid duration. This may cause issues. You can try to fix the file's duration from the File menu",
"This file has broken track interleaving": "This file has broken track interleaving (some tracks are not interleaved properly). LosslessCut will automatically work around this when exporting, so that exported segments get correct timestamps.",
"This file has embedded chapters. Do you want to import the chapters as cut-segments?": "This file has embedded chapters. Do you want to import the chapters as cut-segments?",
"This gives you an overview of the export and allows you to customise more parameters before exporting, like changing the output file name.": "This gives you an overview of the export and allows you to customise more parameters before exporting, like changing the output file name.",
"This is hardcoded by FFmpeg and cannot be changed.": "This is hardcoded by FFmpeg and cannot be changed.",

@ -310,6 +310,7 @@
"Failed to fix file duration": "修复文件时长失败",
"Fixing file duration": "正在修复文件时长",
"This file does not have a valid duration. This may cause issues. You can try to fix the file's duration from the File menu": "此文件的时长无效。这可能有问题。你可以尝试使用文件菜单中的修复错误时长",
"This file has broken track interleaving": "此文件的轨道交织损坏(部分轨道未正确交织存储)。导出时 LosslessCut 会自动采用安全切割方式规避该问题,以保证导出片段的时间戳正确。",
"Change value": "改变值",
"Other operations": "其它操作",
"Enable MOV Faststart?": "启用 MOV 快速启动",

@ -1,5 +1,5 @@
import { join } from 'node:path';
import { access } from 'node:fs/promises';
import { access, open as openFile, type FileHandle } from 'node:fs/promises';
import readline from 'node:readline';
import stringToStream from 'string-to-stream';
import type { Options as ExecaOptions, ResultPromise } from 'execa';
@ -588,6 +588,122 @@ export async function getDuration(filePath: string) {
return parseFfprobeDuration((await readFormatData(filePath)).duration);
}
// Detection of Matroska files with broken track interleaving, e.g. where all audio data is stored at the very end of the file instead of being interleaved with the video data.
// ffmpeg's multi-stream interleaved reading of such files produces outputs with wrong timestamps (e.g. audio starting at 17s instead of 0s), while reading each track type individually works fine.
// Only EBML cluster headers are scanned (no media data is read), so this is fast even for large files.
export async function checkMatroskaTrackInterleaving(filePath: string): Promise<{ hasProblems: boolean }> {
let handle: FileHandle;
try {
handle = await openFile(filePath, 'r');
} catch {
return { hasProblems: false };
}
try {
const { size: fileSize } = await handle.stat();
const cacheSize = 128 * 1024;
let cacheStart = -1;
let cacheBuf = Buffer.allocUnsafe(0);
const readAt = async (position: number, length: number): Promise<Buffer> => {
if (cacheStart >= 0 && position >= cacheStart && position + length <= cacheStart + cacheBuf.length) {
return cacheBuf.subarray(position - cacheStart, position - cacheStart + length);
}
const readLength = Math.min(Math.max(length, cacheSize), fileSize - position);
const buf = Buffer.allocUnsafe(readLength);
await handle.read(buf, 0, readLength, position);
cacheStart = position;
cacheBuf = buf;
return buf;
};
// Parse an EBML variable size integer, see Matroska spec
const readVint = async (position: number): Promise<{ value: bigint, length: number, allOnes: boolean }> => {
const [first] = await readAt(position, 1);
let length = 0;
for (let i = 7; i >= 0; i -= 1) {
if ((first! & (1 << i)) !== 0) { length = 8 - i; break; }
}
if (length === 0) throw new Error('Invalid EBML vint');
const bytes = await readAt(position, length);
let value = BigInt(bytes[0]! & ((1 << (8 - length)) - 1));
let allOnes = (bytes[0]! & ((1 << (8 - length)) - 1)) === ((1 << (8 - length)) - 1);
for (let i = 1; i < length; i += 1) {
value = (value << 8n) | BigInt(bytes[i]!);
if (bytes[i] !== 0xff) allOnes = false;
}
return { value, length, allOnes };
};
const readEbmlElementHeader = async (position: number): Promise<{ id: bigint, size: bigint, unknownSize: boolean, headerLength: number }> => {
const idInfo = await readVint(position);
const id = idInfo.value | (1n << BigInt(7 * idInfo.length)); // keep the marker bit so that IDs match the spec values
const sizeInfo = await readVint(position + idInfo.length);
return { id, size: sizeInfo.value, unknownSize: sizeInfo.allOnes, headerLength: idInfo.length + sizeInfo.length };
};
const ID_EBML = 0x1A45DFA3n;
const ID_SEGMENT = 0x18538067n;
const ID_CLUSTER = 0x1F43B675n;
const ID_TIMESTAMP = 0xE7n;
const top = await readEbmlElementHeader(0);
if (top.id !== ID_EBML) return { hasProblems: false }; // not an EBML/Matroska file
let offset = top.headerLength + Number(top.size);
const segment = await readEbmlElementHeader(offset);
if (segment.id !== ID_SEGMENT) return { hasProblems: false };
const segmentLimit = segment.unknownSize ? fileSize : Math.min(fileSize, offset + segment.headerLength + Number(segment.size));
offset += segment.headerLength;
let lastClusterTimestamp: number | undefined;
let hasProblems = false;
// Iterate over the Segment's top-level elements, only parsing cluster headers (no media data is read).
// Cluster timestamps increase monotonically in a properly interleaved file; a large jump backwards indicates that clusters of some track(s) were written after clusters of other tracks.
while (offset < segmentLimit) {
const element = await readEbmlElementHeader(offset);
if (element.unknownSize) break; // e.g. live stream, cannot reliably scan
const dataStart = offset + element.headerLength;
if (element.id === ID_CLUSTER) {
// Look for the Timestamp child element (only scan a bounded part of the cluster's children)
let inner = dataStart;
const innerLimit = Math.min(dataStart + Number(element.size), dataStart + 4096);
let clusterTimestamp: number | undefined;
while (inner < innerLimit) {
const child = await readEbmlElementHeader(inner);
if (child.unknownSize) break;
if (child.id === ID_TIMESTAMP) {
const tsBytes = await readAt(inner + child.headerLength, Number(child.size));
let v = 0;
// eslint-disable-next-line no-restricted-syntax
for (const b of tsBytes) v = v * 256 + b;
clusterTimestamp = v;
break;
}
inner += child.headerLength + Number(child.size);
}
if (clusterTimestamp != null) {
// Timestamps are in TimestampScale units (normally 1ms). Healthy files only ever increase.
// A backward jump of more than 2 seconds is always a sign of broken interleaving.
if (lastClusterTimestamp != null && clusterTimestamp < lastClusterTimestamp - 2000) hasProblems = true;
lastClusterTimestamp = Math.max(lastClusterTimestamp ?? 0, clusterTimestamp);
}
}
offset = dataStart + Number(element.size);
}
if (hasProblems) logger.info('Detected broken track interleaving in Matroska file', filePath);
return { hasProblems };
} catch (err) {
logger.warn('Failed to check Matroska track interleaving', err);
return { hasProblems: false };
} finally {
await handle.close();
}
}
const enableLog = false;
const encode = true;

@ -63,6 +63,7 @@ import {
RefuseOverwriteError, extractSubtitleTrackToSegments,
mapRecommendedDefaultFormat,
getFfCommandLine,
checkMatroskaTrackInterleaving,
} from './ffmpeg';
import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, isAudioDefinitelyNotSupported, willPlayerProperlyHandleVideo, doesPlayerSupportHevcPlayback, getSubtitleStreams, enableVideoTrack, enableAudioTrack, canHtml5PlayerPlayStreams, isMatroska } from './util/streams';
import { exportEdlFile, readEdlFile, loadLlcProject, askForEdlImport } from './edlStore';
@ -146,6 +147,7 @@ function App() {
const [progress, setProgress] = useState<number>();
const [startTimeOffset, setStartTimeOffset] = useState(0);
const [filePath, setFilePath] = useState<string>();
const [fileHasTrackInterleavingProblem, setFileHasTrackInterleavingProblem] = useState(false);
const [fileDuration, setFileDuration] = useState<number>();
const [externalFilesMeta, setExternalFilesMeta] = useState<FilesMeta>({});
const [paramsByFile, setParamsByFile] = useState<ParamsByFile>(new Map());
@ -593,7 +595,7 @@ function App() {
const {
concatFiles, html5ifyDummy, cutMultiple, concatCutSegments, html5ify, fixInvalidDuration, decimate, extractStreams, tryDeleteFiles,
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate: encBitrate, appendFfmpegCommandLog, ffmpegHwaccel });
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate: encBitrate, appendFfmpegCommandLog, ffmpegHwaccel, fileHasTrackInterleavingProblem });
const { previewFilePath, setPreviewFilePath, usingDummyVideo, setUsingDummyVideo, userHtml5ifyCurrentFile, convertFormatBatch, html5ifyAndLoadWithPreferences } = useHtml5ify({
filePath, hasVideo, hasAudio, workingRef, setWorking, ensureWritableOutDir, customOutDir, batchFiles, enableAutoHtml5ify, setProgress, html5ify, html5ifyDummy, withErrorHandling, showGenericDialog,
@ -695,6 +697,7 @@ function App() {
setExportConfirmOpen(false);
setOutputPlaybackRateState(1);
setCurrentFileExportCount(0);
setFileHasTrackInterleavingProblem(false);
}, [videoRef, setCommandedTime, setPlaybackRate, setPreviewFilePath, setUsingDummyVideo, setPlaying, playingRef, setPlaybackMode, cutSegmentsHistory, setDetectedFileFormat, setCopyStreamIdsByFile, setThumbnails, setSubtitlesByStreamId, setOutputPlaybackRateState]);
@ -1492,6 +1495,9 @@ function App() {
const needsAutoHtml5ify = !existingHtml5FriendlyFile && !willPlayerProperlyHandleVideo({ streams: ffprobeMeta.streams, hevcPlaybackSupported, isMasBuild }) && validDuration;
// Detect Matroska files with broken track interleaving: exporting them needs a special workaround, so detect it now (when the file is opened) instead of having to deal with it after exporting.
const trackInterleavingProblemDetected = needsAutoHtml5ify ? false : (isMatroska(fileFormatNew) ? await checkMatroskaTrackInterleaving(fp) : { hasProblems: false }).hasProblems;
console.log('loadMedia', { filePath: fp, customOutDir: cod, projectPath });
// BEGIN STATE UPDATES:
@ -1532,6 +1538,7 @@ function App() {
setMainFileMeta({ ffprobeMeta, stats: { size: fileStats.size, atime: fileStats.atimeMs, mtime: fileStats.mtimeMs, ctime: fileStats.ctimeMs, birthtime: fileStats.birthtimeMs } });
setCopyStreamIdsForPath(fp, () => copyStreamIdsForPathNew);
setDetectedFileFormat(fileFormatNew);
setFileHasTrackInterleavingProblem(trackInterleavingProblemDetected);
if (outFormatLocked) {
setFileFormat(outFormatLocked);
} else {
@ -1549,6 +1556,8 @@ function App() {
showNotification({ icon: 'info', text: i18n.t('The audio track is not supported while previewing. You can convert to a supported format from the menu') });
} else if (!validDuration) {
getSwal().toast.fire({ icon: 'warning', timer: 10000, text: i18n.t('This file does not have a valid duration. This may cause issues. You can try to fix the file\'s duration from the File menu') });
} else if (trackInterleavingProblemDetected) {
getSwal().toast.fire({ icon: 'info', timer: 15000, showConfirmButton: true, text: i18n.t('This file has broken track interleaving') });
}
// This needs to be last, because it triggers <video> to load the video

@ -18,10 +18,10 @@ import { parseFfprobeDuration } from '../../common/util';
const { ffmpeg } = window.require('@electron/remote').require('./index.js');
const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath, checkFfExists } = ffmpeg;
const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath, checkFfExists, checkMatroskaTrackInterleaving } = ffmpeg;
export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, getFfmpegPath, setCustomFfPath };
export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, getFfmpegPath, setCustomFfPath, checkMatroskaTrackInterleaving };
export class RefuseOverwriteError extends Error {

@ -79,7 +79,7 @@ export async function maybeMkDeepOutDir({ outputDir, fileOutPath }: { outputDir:
}
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate, appendFfmpegCommandLog, ffmpegHwaccel }: {
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, isEncoding, lossyMode, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, encCustomBitrate, appendFfmpegCommandLog, ffmpegHwaccel, fileHasTrackInterleavingProblem }: {
filePath: string | undefined,
treatInputFileModifiedTimeAsStart: boolean,
treatOutputFileModifiedTimeAsStart: boolean | null | undefined,
@ -93,6 +93,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
encCustomBitrate: number | undefined,
appendFfmpegCommandLog: (args: string[]) => void,
ffmpegHwaccel: FfmpegHwAccel,
fileHasTrackInterleavingProblem: boolean,
}) {
const shouldSkipExistingFile = useCallback(async (path: string) => {
const fileExists = await mainApi.pathExists(path);
@ -261,7 +262,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
movFastStart: boolean,
paramsByFile: ParamsByFile,
videoTimebase?: number | undefined,
detectedFps?: number,
detectedFps?: number | undefined,
}) => {
const frameDuration = getFrameDuration(detectedFps);
@ -478,6 +479,85 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(fileDuration) ? fileDuration : undefined, treatOutputFileModifiedTimeAsStart });
}, [appendFfmpegCommandLog, cutFromAdjustmentFrames, cutToAdjustmentFrames, filePath, getOutputPlaybackRateArgs, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
// Same as losslessCutSingle, but safe for Matroska files with broken track interleaving (e.g. audio data stored after all video data instead of being interleaved).
// ffmpeg reads such files incorrectly when reading multiple streams interleaved, and some streams end up with wrong timestamps.
// Workaround: cut the file twice - once with all streams except audio, once with audio only (both are read correctly because ffmpeg doesn't need interleaved reading in those cases), then losslessly merge them.
const losslessCutSingleAudioSafe = useCallback(async ({
keyframeCut, avoidNegativeTs, copyFileStreams, cutFrom, cutTo, chaptersPath, onProgress, outPath, customOutDir, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, paramsByFile, detectedFps,
}: {
keyframeCut: boolean,
avoidNegativeTs: AvoidNegativeTs | undefined,
copyFileStreams: CopyfileStreams,
cutFrom: number,
cutTo: number,
chaptersPath: string | undefined,
onProgress: (p: number) => void,
outPath: string,
customOutDir: string | undefined,
fileDuration: number | undefined,
rotation: number | undefined,
allFilesMeta: AllFilesMeta,
outFormat: string,
shortestFlag: boolean,
ffmpegExperimental: boolean,
preserveMetadata: PreserveMetadata,
preserveMovData: boolean,
preserveChapters: boolean,
movFastStart: boolean,
paramsByFile: ParamsByFile,
detectedFps?: number | undefined,
}) => {
invariant(filePath != null);
const firstFile = copyFileStreams[0];
invariant(firstFile != null);
const { path: mainPath, streamIds: allStreamIds } = firstFile;
const mainFileStreams = allFilesMeta[mainPath]?.streams ?? [];
const audioStreamIds = allStreamIds.filter((streamId) => mainFileStreams.find((s) => s.index === streamId)?.codec_type === 'audio');
const nonAudioStreamIds = allStreamIds.filter((streamId: number) => !audioStreamIds.includes(streamId));
// No audio streams selected, so nothing to work around
if (audioStreamIds.length === 0 || nonAudioStreamIds.length === 0) {
return losslessCutSingle({ keyframeCut, avoidNegativeTs, copyFileStreams, cutFrom, cutTo, chaptersPath, onProgress, outPath, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, paramsByFile, detectedFps });
}
console.log('Cutting file with broken track interleaving using audio-safe two-pass method');
const ext = getOutFileExtension({ isCustomFormatSelected: true, outFormat, filePath });
const uniqueSuffix = Date.now();
const nonAudioOutPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `noaudio-segment-${uniqueSuffix}${ext}` });
const audioOnlyOutPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `audio-only-segment-${uniqueSuffix}${ext}` });
try {
// Pass 1: cut everything except audio. Progress 0 to 0.5
await losslessCutSingle({ keyframeCut, avoidNegativeTs, copyFileStreams: [{ path: mainPath, streamIds: nonAudioStreamIds }], cutFrom, cutTo, chaptersPath, onProgress: (p) => onProgress(p / 2), outPath: nonAudioOutPath, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, paramsByFile, detectedFps });
// Pass 2: cut audio only. Progress 0.5 to 0.75
await losslessCutSingle({ keyframeCut, avoidNegativeTs, copyFileStreams: [{ path: mainPath, streamIds: audioStreamIds }], cutFrom, cutTo, chaptersPath, onProgress: (p) => onProgress(0.5 + p / 4), outPath: audioOnlyOutPath, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, paramsByFile, detectedFps });
// Losslessly merge the two parts back together. Progress 0.75 to 1
const mergeArgs = [
'-hide_banner',
'-i', nonAudioOutPath,
'-i', audioOnlyOutPath,
'-map', '0',
'-map', '1:a',
'-c', 'copy',
...getMatroskaFlags(),
'-ignore_unknown',
'-f', outFormat, '-y', outPath,
];
appendFfmpegCommandLog(mergeArgs);
const result = await runFfmpegWithProgress({ ffmpegArgs: mergeArgs, duration: cutTo - cutFrom, onProgress: (p) => onProgress(0.75 + p / 4) });
logStdoutStderr(result);
await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(fileDuration) ? fileDuration : undefined, treatOutputFileModifiedTimeAsStart });
} finally {
await tryDeleteFiles([nonAudioOutPath, audioOnlyOutPath]);
}
}, [appendFfmpegCommandLog, filePath, losslessCutSingle, 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, hasBFrames }: {
cutFrom: number,
@ -603,9 +683,19 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
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, paramsByFile, onProgress: (progress) => onSingleProgress(i, progress),
});
const onSingleCutProgress = (progress: number) => onSingleProgress(i, progress);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (fileHasTrackInterleavingProblem) {
// The source file has broken track interleaving (detected when the file was opened).
// Cut it with the audio-safe two-pass method so that stream timestamps stay correct.
await losslessCutSingleAudioSafe({
cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath: finalOutPath, customOutDir, copyFileStreams, keyframeCut, avoidNegativeTs, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, paramsByFile, onProgress: onSingleCutProgress,
});
} else {
await losslessCutSingle({
cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath: finalOutPath, copyFileStreams, keyframeCut, avoidNegativeTs, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, paramsByFile, onProgress: onSingleCutProgress,
});
}
return { path: finalOutPath, created: true };
}
@ -710,7 +800,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
} finally {
if (chaptersPath) await tryDeleteFiles([chaptersPath]);
}
}, [shouldSkipExistingFile, isEncoding, filePath, lossyMode, losslessCutSingle, cutEncodeSmartPart, encCustomBitrate, concatFiles]);
}, [shouldSkipExistingFile, isEncoding, filePath, lossyMode, losslessCutSingle, losslessCutSingleAudioSafe, fileHasTrackInterleavingProblem, cutEncodeSmartPart, encCustomBitrate, concatFiles]);
const concatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, chapterNames, preserveMetadataOnMerge, mergedOutFilePath }: {
customOutDir: string | undefined,

Loading…
Cancel
Save