mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-25 11:43:17 +01:00
parent
095dafa374
commit
97dda50ab0
@ -52,6 +52,7 @@ The main feature is lossless trimming and cutting of video and audio files, whic
|
||||
- MKV/MP4 embedded chapters marks editor
|
||||
- View subtitles
|
||||
- Customizable keyboard hotkeys
|
||||
- Black scene detection
|
||||
|
||||
## Example lossless use cases
|
||||
|
||||
|
@ -278,6 +278,13 @@ module.exports = (app, mainWindow, newVersion) => {
|
||||
mainWindow.webContents.send('askSetStartTimeOffset');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.t('Detect black scenes'),
|
||||
click() {
|
||||
mainWindow.webContents.send('detectBlackScenes');
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'toggleDevTools', label: i18n.t('Toggle Developer Tools') },
|
||||
],
|
||||
},
|
||||
|
24
src/App.jsx
24
src/App.jsx
@ -54,7 +54,7 @@ import {
|
||||
getStreamFps, isCuttingStart, isCuttingEnd,
|
||||
readFileMeta, getSmarterOutFormat, renderThumbnails as ffmpegRenderThumbnails,
|
||||
extractStreams, runStartupCheck, setCustomFfPath as ffmpegSetCustomFfPath,
|
||||
isIphoneHevc, tryMapChaptersToEdl,
|
||||
isIphoneHevc, tryMapChaptersToEdl, blackDetect,
|
||||
getDuration, getTimecodeFromStreams, createChaptersFromSegments, extractSubtitleTrack,
|
||||
} from './ffmpeg';
|
||||
import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, defaultProcessedCodecTypes, isAudioDefinitelyNotSupported, doesPlayerSupportFile } from './util/streams';
|
||||
@ -1736,6 +1736,25 @@ const App = memo(() => {
|
||||
}
|
||||
}, [customOutDir, filePath, mainStreams, outputDir, setWorking]);
|
||||
|
||||
const detectBlackScenes = useCallback(async () => {
|
||||
if (!filePath) return;
|
||||
if (workingRef.current) return;
|
||||
try {
|
||||
setWorking(i18n.t('Detecting black scenes'));
|
||||
setCutProgress(0);
|
||||
|
||||
const blackSegments = await blackDetect({ filePath, duration, onProgress: setCutProgress });
|
||||
console.log('blackSegments', blackSegments);
|
||||
loadCutSegments(blackSegments.map(({ blackStart, blackEnd }) => ({ start: blackStart, end: blackEnd })));
|
||||
} catch (err) {
|
||||
errorToast(i18n.t('Failed to detect black scenes'));
|
||||
console.error('Failed to detect black scenes', err);
|
||||
} finally {
|
||||
setWorking();
|
||||
setCutProgress();
|
||||
}
|
||||
}, [duration, filePath, setWorking, loadCutSegments]);
|
||||
|
||||
const userHtml5ifyCurrentFile = useCallback(async () => {
|
||||
if (!filePath) return;
|
||||
|
||||
@ -2177,13 +2196,14 @@ const App = memo(() => {
|
||||
fixInvalidDuration: tryFixInvalidDuration,
|
||||
reorderSegsByStartTime,
|
||||
concatCurrentBatch,
|
||||
detectBlackScenes,
|
||||
shiftAllSegmentTimes,
|
||||
};
|
||||
|
||||
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, 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, 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 {
|
||||
|
@ -60,7 +60,7 @@ export function runFfmpeg(args) {
|
||||
}
|
||||
|
||||
|
||||
export function handleProgress(process, cutDuration, onProgress) {
|
||||
export function handleProgress(process, cutDuration, onProgress, customMatcher = () => {}) {
|
||||
if (!onProgress) return;
|
||||
onProgress(0);
|
||||
|
||||
@ -72,7 +72,10 @@ export function handleProgress(process, cutDuration, onProgress) {
|
||||
let match = line.match(/frame=\s*[^\s]+\s+fps=\s*[^\s]+\s+q=\s*[^\s]+\s+(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
|
||||
// Audio only looks like this: "line size= 233422kB time=01:45:50.68 bitrate= 301.1kbits/s speed= 353x "
|
||||
if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
|
||||
if (!match) return;
|
||||
if (!match) {
|
||||
customMatcher(line);
|
||||
return;
|
||||
}
|
||||
|
||||
const str = match[1];
|
||||
// console.log(str);
|
||||
@ -514,6 +517,26 @@ export async function renderWaveformPng({ filePath, aroundTime, window, color })
|
||||
}
|
||||
}
|
||||
|
||||
export async function blackDetect({ filePath, duration, minInterval = 0.05, onProgress }) {
|
||||
const args = ['-hide_banner', '-i', filePath, '-vf', `blackdetect=d=${minInterval}`, '-an', '-f', 'null', '-'];
|
||||
const process = execa(getFfmpegPath(), args, { encoding: null, buffer: false });
|
||||
|
||||
const blackSegments = [];
|
||||
|
||||
function customMatcher(line) {
|
||||
const match = line.match(/^[blackdetect @ 0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/);
|
||||
if (!match) return;
|
||||
const blackStart = parseFloat(match[1]);
|
||||
const blackEnd = parseFloat(match[2]);
|
||||
if (Number.isNaN(blackStart) || Number.isNaN(blackEnd)) return;
|
||||
blackSegments.push({ blackStart, blackEnd });
|
||||
}
|
||||
handleProgress(process, duration, onProgress, customMatcher);
|
||||
|
||||
await process;
|
||||
return blackSegments;
|
||||
}
|
||||
|
||||
export async function extractWaveform({ filePath, outPath }) {
|
||||
const numSegs = 10;
|
||||
const duration = 60 * 60;
|
||||
|
Loading…
Reference in New Issue
Block a user