log extract streams

pull/2263/head
Mikael Finstad 2 years ago
parent 8b7375b44f
commit d7da742c8a
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -54,7 +54,7 @@ import { getSegColor } from './util/colors';
import {
getStreamFps, isCuttingStart, isCuttingEnd,
readFileMeta, getDefaultOutFormat,
extractStreams, setCustomFfPath as ffmpegSetCustomFfPath,
setCustomFfPath as ffmpegSetCustomFfPath,
isIphoneHevc, isProblematicAvc1, tryMapChaptersToEdl,
getDuration, getTimecodeFromStreams, createChaptersFromSegments,
RefuseOverwriteError, extractSubtitleTrackToSegments,
@ -594,7 +594,7 @@ function App() {
const needSmartCut = !!(areWeCutting && enableSmartCut);
const {
appendFfmpegCommandLog, concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration,
appendFfmpegCommandLog, concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration, extractStreams,
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate: smartCutBitrate });
const { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } = useFrameCapture({ appendFfmpegCommandLog, formatTimecode, treatOutputFileModifiedTimeAsStart });
@ -1083,7 +1083,7 @@ function App() {
try {
setProgress(undefined); // If extracting extra streams takes a long time, prevent loader from being stuck at 100%
setWorking({ text: i18n.t('Extracting {{count}} unprocessable tracks', { count: nonCopiedExtraStreams.length }) });
await extractStreams({ filePath, customOutDir, streams: nonCopiedExtraStreams, enableOverwriteOutput });
await extractStreams({ customOutDir, streams: nonCopiedExtraStreams });
notices.push(i18n.t('Unprocessable streams were exported as separate files.'));
} catch (err) {
console.error('Extra stream export failed', err);
@ -1130,7 +1130,7 @@ function App() {
setWorking(undefined);
setProgress(undefined);
}
}, [filePath, numStreamsToCopy, segmentsToExport, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, selectedSegmentsOrInverse, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, showOsNotification, handleExportFailed]);
}, [filePath, numStreamsToCopy, segmentsToExport, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, selectedSegmentsOrInverse, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, extractStreams, showOsNotification, handleExportFailed]);
const onExportPress = useCallback(async () => {
if (!filePath) return;
@ -1531,7 +1531,7 @@ function App() {
try {
setWorking({ text: i18n.t('Extracting all streams') });
setStreamsSelectorShown(false);
const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput });
const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainCopiedStreams });
if (!hideAllNotifications && firstExtractedPath != null) {
showOsNotification(i18n.t('All tracks have been extracted'));
openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('All streams have been extracted as separate files') });
@ -1548,7 +1548,7 @@ function App() {
} finally {
setWorking(undefined);
}
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainCopiedStreams, setWorking, showOsNotification, workingRef]);
}, [customOutDir, extractStreams, filePath, hideAllNotifications, mainCopiedStreams, setWorking, showOsNotification, workingRef]);
const userHtml5ifyCurrentFile = useCallback(async ({ ignoreRememberedValue }: { ignoreRememberedValue?: boolean } = {}) => {
if (!filePath) return;
@ -2101,7 +2101,7 @@ function App() {
try {
setWorking({ text: i18n.t('Extracting track') });
// setStreamsSelectorShown(false);
const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput });
const [firstExtractedPath] = await extractStreams({ customOutDir, streams: mainStreams.filter((s) => s.index === index) });
if (!hideAllNotifications && firstExtractedPath != null) {
showOsNotification(i18n.t('Track has been extracted'));
openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('Track has been extracted') });
@ -2118,7 +2118,7 @@ function App() {
} finally {
setWorking(undefined);
}
}, [customOutDir, enableOverwriteOutput, filePath, hideAllNotifications, mainStreams, setWorking, showOsNotification, workingRef]);
}, [customOutDir, extractStreams, filePath, hideAllNotifications, mainStreams, setWorking, showOsNotification, workingRef]);
const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]);

@ -6,21 +6,19 @@ import minBy from 'lodash/minBy';
import invariant from 'tiny-invariant';
import { pcmAudioCodecs, getMapStreamsArgs, isMov } from './util/streams';
import { getSuffixedOutPath, isExecaError } from './util';
import { isExecaError } from './util';
import { isDurationValid } from './segments';
import { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream } from '../../../ffprobe';
import { parseSrt, parseSrtToSegments } from './edlFormats';
import { CopyfileStreams, LiteFFprobeStream } from './types';
import { CopyfileStreams } from './types';
import { UnsupportedFileError } from '../errors';
const { pathExists } = window.require('fs-extra');
const { ffmpeg, fileTypePromise } = window.require('@electron/remote').require('./index.js');
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, runFfprobe, getFfmpegPath, setCustomFfPath };
export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath };
export class RefuseOverwriteError extends Error {
@ -375,137 +373,6 @@ export async function readFileMeta(filePath: string) {
}
}
function getPreferredCodecFormat(stream: LiteFFprobeStream) {
const map = {
mp3: { format: 'mp3', ext: 'mp3' },
opus: { format: 'opus', ext: 'opus' },
vorbis: { format: 'ogg', ext: 'ogg' },
h264: { format: 'mp4', ext: 'mp4' },
hevc: { format: 'mp4', ext: 'mp4' },
eac3: { format: 'eac3', ext: 'eac3' },
subrip: { format: 'srt', ext: 'srt' },
mov_text: { format: 'mp4', ext: 'mp4' },
m4a: { format: 'ipod', ext: 'm4a' },
aac: { format: 'adts', ext: 'aac' },
jpeg: { format: 'image2', ext: 'jpeg' },
png: { format: 'image2', ext: 'png' },
// TODO add more
// TODO allow user to change?
};
const match = map[stream.codec_name];
if (match) return match;
// default fallbacks:
if (stream.codec_type === 'video') return { ext: 'mkv', format: 'matroska' };
if (stream.codec_type === 'audio') return { ext: 'mka', format: 'matroska' };
if (stream.codec_type === 'subtitle') return { ext: 'mks', format: 'matroska' };
if (stream.codec_type === 'data') return { ext: 'bin', format: 'data' }; // https://superuser.com/questions/1243257/save-data-stream
return undefined;
}
async function extractNonAttachmentStreams({ customOutDir, filePath, streams, enableOverwriteOutput }: {
customOutDir?: string | undefined, filePath: string, streams: FFprobeStream[], enableOverwriteOutput: boolean | undefined,
}) {
if (streams.length === 0) return [];
const outStreams = streams.map((s) => ({
index: s.index,
codec: s.codec_name || s.codec_tag_string || s.codec_type,
type: s.codec_type,
format: getPreferredCodecFormat(s),
}))
.filter(({ format, index }) => format != null && index != null);
// console.log(outStreams);
let streamArgs: string[] = [];
const outPaths = await pMap(outStreams, async ({ index, codec, type, format: { format, ext } }) => {
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [
...streamArgs,
'-map', `0:${index}`, '-c', 'copy', '-f', format, '-y', outPath,
];
return outPath;
}, { concurrency: 1 });
const ffmpegArgs = [
'-hide_banner',
'-i', filePath,
...streamArgs,
];
const { stdout } = await runFfmpeg(ffmpegArgs);
console.log(stdout.toString('utf8'));
return outPaths;
}
async function extractAttachmentStreams({ customOutDir, filePath, streams, enableOverwriteOutput }: {
customOutDir?: string | undefined, filePath: string, streams: FFprobeStream[], enableOverwriteOutput: boolean | undefined,
}) {
if (streams.length === 0) return [];
console.log('Extracting', streams.length, 'attachment streams');
let streamArgs: string[] = [];
const outPaths = await pMap(streams, async ({ index, codec_name: codec, codec_type: type }) => {
const ext = codec || 'bin';
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (outPath == null) throw new Error();
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [
...streamArgs,
`-dump_attachment:${index}`, outPath,
];
return outPath;
}, { concurrency: 1 });
const ffmpegArgs = [
'-y',
'-hide_banner',
'-loglevel', 'error',
...streamArgs,
'-i', filePath,
];
try {
const { stdout } = await runFfmpeg(ffmpegArgs);
console.log(stdout.toString('utf8'));
} catch (err) {
// Unfortunately ffmpeg will exit with code 1 even though it's a success
// Note: This is kind of hacky:
if (err instanceof Error && 'exitCode' in err && 'stderr' in err && err.exitCode === 1 && typeof err.stderr === 'string' && err.stderr.includes('At least one output file must be specified')) return outPaths;
throw err;
}
return outPaths;
}
// https://stackoverflow.com/questions/32922226/extract-every-audio-and-subtitles-from-a-video-with-ffmpeg
export async function extractStreams({ filePath, customOutDir, streams, enableOverwriteOutput }: {
filePath: string, customOutDir: string | undefined, streams: FFprobeStream[], enableOverwriteOutput?: boolean | undefined,
}) {
const attachmentStreams = streams.filter((s) => s.codec_type === 'attachment');
const nonAttachmentStreams = streams.filter((s) => s.codec_type !== 'attachment');
// TODO progress
// Attachment streams are handled differently from normal streams
return [
...(await extractNonAttachmentStreams({ customOutDir, filePath, streams: nonAttachmentStreams, enableOverwriteOutput })),
...(await extractAttachmentStreams({ customOutDir, filePath, streams: attachmentStreams, enableOverwriteOutput })),
];
}
async function renderThumbnail(filePath: string, timestamp: number, signal: AbortSignal) {
const args = [
'-ss', String(timestamp),

@ -5,13 +5,13 @@ 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 } from '../ffmpeg';
import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, cutEncodeSmartPart, 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';
import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs, Html5ifyMode, PreserveMetadata } from '../../../../types';
import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, ParamsByStreamId, SegmentToExport } from '../types';
import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types';
const { join, resolve, dirname } = window.require('path');
const { writeFile, mkdir, access, constants: { F_OK, W_OK } } = window.require('fs/promises');
@ -674,8 +674,146 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
return outPath;
}, [appendFfmpegCommandLog, filePath, treatOutputFileModifiedTimeAsStart]);
function getPreferredCodecFormat(stream: LiteFFprobeStream) {
const map = {
mp3: { format: 'mp3', ext: 'mp3' },
opus: { format: 'opus', ext: 'opus' },
vorbis: { format: 'ogg', ext: 'ogg' },
h264: { format: 'mp4', ext: 'mp4' },
hevc: { format: 'mp4', ext: 'mp4' },
eac3: { format: 'eac3', ext: 'eac3' },
subrip: { format: 'srt', ext: 'srt' },
mov_text: { format: 'mp4', ext: 'mp4' },
m4a: { format: 'ipod', ext: 'm4a' },
aac: { format: 'adts', ext: 'aac' },
jpeg: { format: 'image2', ext: 'jpeg' },
png: { format: 'image2', ext: 'png' },
// TODO add more
// TODO allow user to change?
};
const match = map[stream.codec_name];
if (match) return match;
// default fallbacks:
if (stream.codec_type === 'video') return { ext: 'mkv', format: 'matroska' };
if (stream.codec_type === 'audio') return { ext: 'mka', format: 'matroska' };
if (stream.codec_type === 'subtitle') return { ext: 'mks', format: 'matroska' };
if (stream.codec_type === 'data') return { ext: 'bin', format: 'data' }; // https://superuser.com/questions/1243257/save-data-stream
return undefined;
}
async function extractNonAttachmentStreams({ customOutDir, streams }: {
customOutDir?: string | undefined, streams: FFprobeStream[],
}) {
invariant(filePath != null);
if (streams.length === 0) return [];
const outStreams = streams.map((s) => ({
index: s.index,
codec: s.codec_name || s.codec_tag_string || s.codec_type,
type: s.codec_type,
format: getPreferredCodecFormat(s),
}))
.filter(({ format, index }) => format != null && index != null);
// console.log(outStreams);
let streamArgs: string[] = [];
const outPaths = await pMap(outStreams, async ({ index, codec, type, format: { format, ext } }) => {
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [
...streamArgs,
'-map', `0:${index}`, '-c', 'copy', '-f', format, '-y', outPath,
];
return outPath;
}, { concurrency: 1 });
const ffmpegArgs = [
'-hide_banner',
'-i', filePath,
...streamArgs,
];
appendFfmpegCommandLog(ffmpegArgs);
const { stdout } = await runFfmpeg(ffmpegArgs);
console.log(stdout.toString('utf8'));
return outPaths;
}
async function extractAttachmentStreams({ customOutDir, streams }: {
customOutDir?: string | undefined, streams: FFprobeStream[],
}) {
invariant(filePath != null);
if (streams.length === 0) return [];
console.log('Extracting', streams.length, 'attachment streams');
let streamArgs: string[] = [];
const outPaths = await pMap(streams, async ({ index, codec_name: codec, codec_type: type }) => {
const ext = codec || 'bin';
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (outPath == null) throw new Error();
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [
...streamArgs,
`-dump_attachment:${index}`, outPath,
];
return outPath;
}, { concurrency: 1 });
const ffmpegArgs = [
'-y',
'-hide_banner',
'-loglevel', 'error',
...streamArgs,
'-i', filePath,
];
try {
appendFfmpegCommandLog(ffmpegArgs);
const { stdout } = await runFfmpeg(ffmpegArgs);
console.log(stdout.toString('utf8'));
} catch (err) {
// Unfortunately ffmpeg will exit with code 1 even though it's a success
// Note: This is kind of hacky:
if (err instanceof Error && 'exitCode' in err && 'stderr' in err && err.exitCode === 1 && typeof err.stderr === 'string' && err.stderr.includes('At least one output file must be specified')) return outPaths;
throw err;
}
return outPaths;
}
// https://stackoverflow.com/questions/32922226/extract-every-audio-and-subtitles-from-a-video-with-ffmpeg
async function extractStreams({ customOutDir, streams }: {
customOutDir: string | undefined, streams: FFprobeStream[],
}) {
invariant(filePath != null);
const attachmentStreams = streams.filter((s) => s.codec_type === 'attachment');
const nonAttachmentStreams = streams.filter((s) => s.codec_type !== 'attachment');
// TODO progress
// Attachment streams are handled differently from normal streams
return [
...(await extractNonAttachmentStreams({ customOutDir, streams: nonAttachmentStreams })),
...(await extractAttachmentStreams({ customOutDir, streams: attachmentStreams })),
];
}
return {
appendFfmpegCommandLog, cutMultiple, concatFiles, html5ify, html5ifyDummy, fixInvalidDuration, autoConcatCutSegments,
appendFfmpegCommandLog, cutMultiple, concatFiles, html5ify, html5ifyDummy, fixInvalidDuration, autoConcatCutSegments, extractStreams,
};
}

Loading…
Cancel
Save