mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-25 03:33:14 +01:00
parent
3f794c68b0
commit
bb304c8fd7
@ -53,6 +53,7 @@ The main feature is lossless trimming and cutting of video and audio files, whic
|
||||
- View subtitles
|
||||
- Customizable keyboard hotkeys
|
||||
- Black scene detection
|
||||
- Divide timeline into segments with length L or into N segments or even randomized segments!
|
||||
|
||||
## Example lossless use cases
|
||||
|
||||
|
@ -214,6 +214,12 @@ module.exports = (app, mainWindow, newVersion) => {
|
||||
mainWindow.webContents.send('createFixedDurationSegments');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.t('Create random segments'),
|
||||
click() {
|
||||
mainWindow.webContents.send('createRandomSegments');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.t('Invert all segments on timeline'),
|
||||
click() {
|
||||
|
11
src/App.jsx
11
src/App.jsx
@ -69,7 +69,7 @@ import {
|
||||
} from './util';
|
||||
import { formatDuration } from './util/duration';
|
||||
import { adjustRate } from './util/rate-calculator';
|
||||
import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages } from './dialogs';
|
||||
import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages } from './dialogs';
|
||||
import { openSendReportDialog } from './reporting';
|
||||
import { fallbackLng } from './i18n';
|
||||
import { createSegment, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, convertSegmentsToChapters, hasAnySegmentOverlap } from './segments';
|
||||
@ -1815,6 +1815,12 @@ const App = memo(() => {
|
||||
if (segments) loadCutSegments(segments);
|
||||
}, [checkFileOpened, duration, loadCutSegments]);
|
||||
|
||||
const createRandomSegments = useCallback(async () => {
|
||||
if (!checkFileOpened() || !isDurationValid(duration)) return;
|
||||
const segments = await createRandomSegmentsDialog(duration);
|
||||
if (segments) loadCutSegments(segments);
|
||||
}, [checkFileOpened, duration, loadCutSegments]);
|
||||
|
||||
const askSetStartTimeOffset = useCallback(async () => {
|
||||
const newStartTimeOffset = await promptTimeOffset({
|
||||
initialValue: startTimeOffset !== undefined ? formatDuration({ seconds: startTimeOffset }) : undefined,
|
||||
@ -2199,6 +2205,7 @@ const App = memo(() => {
|
||||
shuffleSegments,
|
||||
createNumSegments,
|
||||
createFixedDurationSegments,
|
||||
createRandomSegments,
|
||||
invertAllSegments,
|
||||
fillSegmentsGaps,
|
||||
fixInvalidDuration: tryFixInvalidDuration,
|
||||
@ -2211,7 +2218,7 @@ const App = memo(() => {
|
||||
const entries = Object.entries(action);
|
||||
entries.forEach(([key, value]) => electron.ipcRenderer.on(key, value));
|
||||
return () => entries.forEach(([key, value]) => electron.ipcRenderer.removeListener(key, value));
|
||||
}, [apparentCutSegments, askSetStartTimeOffset, checkFileOpened, clearSegments, closeBatch, closeFileWithConfirm, concatCurrentBatch, createFixedDurationSegments, createNumSegments, customOutDir, cutSegments, detectBlackScenes, detectedFps, extractAllStreams, fileFormat, filePath, fillSegmentsGaps, getFrameCount, invertAllSegments, loadCutSegments, loadMedia, openSendReportDialogWithState, reorderSegsByStartTime, setWorking, shiftAllSegmentTimes, shuffleSegments, toggleHelp, toggleSettings, tryFixInvalidDuration, userHtml5ifyCurrentFile, userOpenFiles]);
|
||||
}, [apparentCutSegments, askSetStartTimeOffset, checkFileOpened, clearSegments, closeBatch, closeFileWithConfirm, concatCurrentBatch, createFixedDurationSegments, createNumSegments, createRandomSegments, customOutDir, cutSegments, detectBlackScenes, detectedFps, extractAllStreams, fileFormat, filePath, fillSegmentsGaps, getFrameCount, invertAllSegments, loadCutSegments, loadMedia, openSendReportDialogWithState, reorderSegsByStartTime, setWorking, shiftAllSegmentTimes, shuffleSegments, toggleHelp, toggleSettings, tryFixInvalidDuration, userHtml5ifyCurrentFile, userOpenFiles]);
|
||||
|
||||
const showAddStreamSourceDialog = useCallback(async () => {
|
||||
try {
|
||||
|
@ -239,6 +239,40 @@ async function askForSegmentDuration(fileDuration) {
|
||||
return parseDuration(value);
|
||||
}
|
||||
|
||||
// https://github.com/mifi/lossless-cut/issues/1153
|
||||
async function askForSegmentsRandomDurationRange() {
|
||||
function parse(str) {
|
||||
const match = str.replace(/\s/g, '').match(/^duration([\d.]+)to([\d.]+),gap([-\d.]+)to([-\d.]+)$/i);
|
||||
if (!match) return undefined;
|
||||
const values = match.slice(1);
|
||||
const parsed = values.map((val) => parseFloat(val));
|
||||
|
||||
const durationMin = parsed[0];
|
||||
const durationMax = parsed[1];
|
||||
const gapMin = parsed[2];
|
||||
const gapMax = parsed[3];
|
||||
|
||||
if (!(parsed.every((val) => !Number.isNaN(val)) && durationMin <= durationMax && gapMin <= gapMax && durationMin > 0)) return undefined;
|
||||
return { durationMin, durationMax, gapMin, gapMax };
|
||||
}
|
||||
|
||||
const { value } = await Swal.fire({
|
||||
input: 'text',
|
||||
showCancelButton: true,
|
||||
inputValue: 'Duration 3 to 5, Gap 0 to 2',
|
||||
text: i18n.t('Divide timeline into segments with randomized durations and gaps between sergments, in a range specified in seconds with the correct format.'),
|
||||
inputValidator: (v) => {
|
||||
const parsed = parse(v);
|
||||
if (!parsed) return i18n.t('Invalid input');
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
if (value == null) return undefined;
|
||||
|
||||
return parse(value);
|
||||
}
|
||||
|
||||
async function askForShiftSegmentsVariant(time) {
|
||||
const { value } = await Swal.fire({
|
||||
input: 'radio',
|
||||
@ -376,6 +410,23 @@ export async function createFixedDurationSegments(fileDuration) {
|
||||
return edl;
|
||||
}
|
||||
|
||||
export async function createRandomSegments(fileDuration) {
|
||||
const response = await askForSegmentsRandomDurationRange();
|
||||
if (response == null) return undefined;
|
||||
|
||||
const { durationMin, durationMax, gapMin, gapMax } = response;
|
||||
|
||||
const randomInRange = (min, max) => min + Math.random() * (max - min);
|
||||
|
||||
const edl = [];
|
||||
for (let start = 0; start < fileDuration && edl.length < maxSegments; start += randomInRange(gapMin, gapMax)) {
|
||||
const end = start + randomInRange(durationMin, durationMax);
|
||||
edl.push({ start, end });
|
||||
start = end;
|
||||
}
|
||||
return edl;
|
||||
}
|
||||
|
||||
export async function showCutFailedDialog({ detectedFileFormat }) {
|
||||
const html = (
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
|
Loading…
Reference in New Issue
Block a user