Skip to content

Commit

Permalink
allow extracting markers as images too
Browse files Browse the repository at this point in the history
  • Loading branch information
mifi committed Feb 20, 2025
1 parent 2502183 commit 2502204
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 25 deletions.
27 changes: 20 additions & 7 deletions src/main/ffmpeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ function getCodecOpts(captureFormat: CaptureFormat) {

export async function captureFrames({ from, to, videoPath, outPathTemplate, quality, filter, framePts, onProgress, captureFormat }: {
from: number,
to: number,
to?: number | undefined,
videoPath: string,
outPathTemplate: string,
quality: number,
Expand All @@ -456,22 +456,35 @@ export async function captureFrames({ from, to, videoPath, outPathTemplate, qual
const args = [
'-ss', String(from),
'-i', videoPath,
'-t', String(Math.max(0, to - from)),
...(to != null ? ['-t', String(Math.max(0, to - from))] : []),
...getQualityOpts({ captureFormat, quality }),
...(filter != null ? ['-vf', filter] : []),
// https://superuser.com/questions/1336285/use-ffmpeg-for-thumbnail-selections
...(framePts ? ['-frame_pts', '1'] : []),
'-vsync', '0', // else we get a ton of duplicates (thumbnail filter)
// only apply filter for non-markers
...(filter != null && to != null
? [
'-vf', filter,
// https://superuser.com/questions/1336285/use-ffmpeg-for-thumbnail-selections
...(framePts ? ['-frame_pts', '1'] : []),
'-vsync', '0', // else we get a ton of duplicates (thumbnail filter)
]
: [
'-frames:v', '1', // for markers, just capture 1 frame
]
),
...getCodecOpts(captureFormat),
'-f', 'image2',
'-y', outPathTemplate,
];

const process = runFfmpegProcess(args, { buffer: false });

handleProgress(process, to - from, onProgress);
if (to != null) {
handleProgress(process, to - from, onProgress);
}

await process;

onProgress(1);

return args;
}

Expand Down
25 changes: 13 additions & 12 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ import { askForHtml5ifySpeed } from './dialogs/html5ify';
import { askForOutDir, askForImportChapters, promptTimecode, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable } from './dialogs';
import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
import { sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, getPlaybackMode, getSegmentTags, filterNonMarkers } from './segments';
import { sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, getPlaybackMode, getSegmentTags } from './segments';
import { generateOutSegFileNames as generateOutSegFileNamesRaw, generateMergedFileNames as generateMergedFileNamesRaw, defaultOutSegTemplate, defaultCutMergedFileTemplate } from './util/outputNameTemplate';
import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './util/constants';
import BigWaveform from './components/BigWaveform';

import isDev from './isDev';
import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, TunerType } from './types';
import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentBase, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, TunerType } from './types';
import { CaptureFormat, KeyboardAction, Html5ifyMode, WaveformMode, ApiActionRequest } from '../../../types';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
import useLoading from './hooks/useLoading';
Expand Down Expand Up @@ -1192,10 +1192,14 @@ function App() {
}, i18n.t('Failed to capture frame'));
}, [filePath, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]);

const extractSegmentFramesAsImages = useCallback(async (segments: { start: number, end: number }[]) => {
const extractSegmentsFramesAsImages = useCallback(async (segments: SegmentBase[]) => {
if (!filePath || detectedFps == null || workingRef.current) return;
const segmentsNumFrames = segments.reduce((acc, { start, end }) => acc + (getFrameCount(end - start) ?? 0), 0);
const captureFramesResponse = await askExtractFramesAsImages({ segmentsNumFrames, plural: segments.length > 1, fps: detectedFps });
const segmentsNumFrames = segments.reduce((acc, { start, end }) => acc + (end == null ? 1 : (getFrameCount(end - start) ?? 0)), 0);
const areAllSegmentsMarkers = segments.every((seg) => seg.end == null);
const captureFramesResponse = areAllSegmentsMarkers
? { filter: undefined, estimatedMaxNumFiles: segmentsNumFrames }
: await askExtractFramesAsImages({ segmentsNumFrames, plural: segments.length > 1, fps: detectedFps });

if (captureFramesResponse == null) return;

try {
Expand Down Expand Up @@ -1233,13 +1237,9 @@ function App() {
}
}, [filePath, detectedFps, workingRef, getFrameCount, setWorking, hideAllNotifications, captureFramesRange, customOutDir, captureFormat, captureFrameQuality, captureFrameFileNameFormat, showOsNotification, outputDir]);

const extractCurrentSegmentFramesAsImages = useCallback(() => {
const { end } = currentCutSeg;
invariant(currentCutSeg != null && end != null);
extractSegmentFramesAsImages([{ ...currentCutSeg, end }]);
}, [currentCutSeg, extractSegmentFramesAsImages]);
const extractCurrentSegmentFramesAsImages = useCallback(() => extractSegmentsFramesAsImages([currentCutSeg]), [currentCutSeg, extractSegmentsFramesAsImages]);

const extractSelectedSegmentsFramesAsImages = useCallback(() => extractSegmentFramesAsImages(filterNonMarkers(selectedSegments)), [extractSegmentFramesAsImages, selectedSegments]);
const extractSelectedSegmentsFramesAsImages = useCallback(() => extractSegmentsFramesAsImages(selectedSegments), [extractSegmentsFramesAsImages, selectedSegments]);

const userChangePlaybackRate = useCallback((dir: number, rateMultiplier?: number) => {
if (compatPlayerEnabled) {
Expand Down Expand Up @@ -2532,7 +2532,8 @@ function App() {
onDeselectAllSegments={deselectAllSegments}
onSelectAllSegments={selectAllSegments}
onInvertSelectedSegments={invertSelectedSegments}
onExtractSegmentFramesAsImages={extractSegmentFramesAsImages}
onExtractSegmentFramesAsImages={extractSegmentsFramesAsImages}
onExtractSelectedSegmentsFramesAsImages={extractSelectedSegmentsFramesAsImages}
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
onSelectSegmentsByLabel={selectSegmentsByLabel}
Expand Down
16 changes: 11 additions & 5 deletions src/renderer/src/SegmentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { useSegColors } from './contexts';
import { mySpring } from './animations';
import { getSegmentTags } from './segments';
import TagEditor from './components/TagEditor';
import { ContextMenuTemplate, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentTags, StateSegment } from './types';
import { ContextMenuTemplate, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentBase, SegmentTags, StateSegment } from './types';
import { UseSegments } from './hooks/useSegments';


Expand Down Expand Up @@ -55,6 +55,7 @@ const Segment = memo(({
addSegment,
onEditSegmentTags,
onExtractSegmentFramesAsImages,
onExtractSelectedSegmentsFramesAsImages,
onInvertSelectedSegments,
onDuplicateSegmentClick,
}: {
Expand Down Expand Up @@ -83,7 +84,8 @@ const Segment = memo(({
jumpSegEnd: (i: number) => void,
addSegment: UseSegments['addSegment'],
onEditSegmentTags: (i: number) => void,
onExtractSegmentFramesAsImages: (segments: Pick<InverseCutSegment, 'start' | 'end'>[]) => Promise<void>,
onExtractSegmentFramesAsImages: (segments: Pick<SegmentBase, 'start' | 'end'>[]) => Promise<void>,
onExtractSelectedSegmentsFramesAsImages: () => void,
onInvertSelectedSegments: UseSegments['invertSelectedSegments'],
onDuplicateSegmentClick: UseSegments['duplicateSegment'],
}) => {
Expand Down Expand Up @@ -124,6 +126,7 @@ const Segment = memo(({
{ label: t('Label selected segments'), click: onLabelSelectedSegments },
{ label: t('Remove selected segments'), click: onRemoveSelected },
{ label: t('Edit segments by expression'), click: () => onMutateSegmentsByExpr() },
{ label: t('Extract frames from selected segments as image files'), click: onExtractSelectedSegmentsFramesAsImages },

{ type: 'separator' },

Expand All @@ -134,9 +137,9 @@ const Segment = memo(({
{ type: 'separator' },

{ label: t('Segment tags'), click: () => onEditSegmentTags(index) },
...(seg.end != null ? [{ label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg as Pick<InverseCutSegment, 'start' | 'end'>]) }] : []),
{ label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg]) },
];
}, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, seg, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectAllMarkers, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onInvertSelectedSegments, onMutateSegmentsByExpr, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]);
}, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, onExtractSelectedSegmentsFramesAsImages, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectAllMarkers, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onInvertSelectedSegments, onMutateSegmentsByExpr, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]);

useContextMenu(ref, contextMenuTemplate);

Expand Down Expand Up @@ -257,6 +260,7 @@ function SegmentList({
onMutateSegmentsByExpr,
onSelectAllMarkers,
onExtractSegmentFramesAsImages,
onExtractSelectedSegmentsFramesAsImages,
onLabelSelectedSegments,
onInvertSelectedSegments,
onDuplicateSegmentClick,
Expand Down Expand Up @@ -296,7 +300,8 @@ function SegmentList({
onSelectSegmentsByExpr: UseSegments['selectSegmentsByExpr'],
onSelectAllMarkers: UseSegments['selectAllMarkers'],
onMutateSegmentsByExpr: UseSegments['mutateSegmentsByExpr'],
onExtractSegmentFramesAsImages: (segments: Pick<InverseCutSegment, 'start' | 'end'>[]) => Promise<void>,
onExtractSegmentFramesAsImages: (segments: Pick<SegmentBase, 'start' | 'end'>[]) => Promise<void>,
onExtractSelectedSegmentsFramesAsImages: () => void,
onLabelSelectedSegments: UseSegments['labelSelectedSegments'],
onInvertSelectedSegments: UseSegments['invertSelectedSegments'],
onDuplicateSegmentClick: UseSegments['duplicateSegment'],
Expand Down Expand Up @@ -514,6 +519,7 @@ function SegmentList({
onSelectSegmentsByExpr={onSelectSegmentsByExpr}
onMutateSegmentsByExpr={onMutateSegmentsByExpr}
onExtractSegmentFramesAsImages={onExtractSegmentFramesAsImages}
onExtractSelectedSegmentsFramesAsImages={onExtractSelectedSegmentsFramesAsImages}
onLabelSelectedSegments={onLabelSelectedSegments}
onSelectAllMarkers={onSelectAllMarkers}
onInvertSelectedSegments={onInvertSelectedSegments}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/hooks/useFrameCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default ({ appendFfmpegCommandLog, formatTimecode, treatOutputFileModifie
filePath: string,
fps: number,
fromTime: number,
toTime: number,
toTime: number | undefined,
estimatedMaxNumFiles: number,
captureFormat: CaptureFormat,
quality: number,
Expand Down

0 comments on commit 2502204

Please sign in to comment.