1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 11:43:17 +01:00

implement black scene detection

closes #623
This commit is contained in:
Mikael Finstad 2022-03-18 17:51:24 +08:00
parent 095dafa374
commit 97dda50ab0
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
4 changed files with 55 additions and 4 deletions

View File

@ -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

View File

@ -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') },
],
},

View File

@ -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 {

View File

@ -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;