shinobi-video-synopsis
Version:
Create Video Synopsis from provided videos. Frames are extracted from each video provided and backgrounds are subtracted around moving elements and merged into a single video for quick review.
183 lines (182 loc) • 7.35 kB
JavaScript
const EventEmitter = require('events');
exports.newEngine = (engineOptions) => {
engineOptions = engineOptions ? engineOptions : {}
const engine = {}
const {
mainDirectory,
createBaseFolder,
deleteFile,
grabFirstFrame,
getVideoFileName,
} = require('./utils.js');
const fs = require('fs');
const { createNewBackgroundSubtractor } = require('./extractFramesFromVideosAndRemoveBackgrounds.js');
const { mergeFramePairs } = require('./mergeFramePairs.js');
const { createVideoFromJpegImages } = require('./createVideoFromMergedFrames.js');
const extractFramesFromVideo = createNewBackgroundSubtractor()
const initiatedAt = new Date();
const std = new EventEmitter();
var percentCompleted = 0;
const subtractedBackgrounds = engineOptions.subtractedBackgroundsDirectory || mainDirectory + 'rawSubtractions/'
const mergedFramesDirectory = engineOptions.mergedFramesDirectory || mainDirectory + 'mergedFrames/'
const firstFrameDirectory = engineOptions.firstFrameDirectory || mainDirectory + 'firstFrame/'
const ffmpegBinary = engineOptions.ffmpegBinary || 'ffmpeg'
var alreadyRunning = false;
var processLogs = []
const sendPercentComplete = (percent) => {
percentCompleted = percent;
std.emit('percent',percent)
if(engineOptions.debugLog)console.log('shinobi-video-synopsis',percent)
}
engine.std = std
engine.logStderr = (...args) => {
processLogs.push(args)
std.emit('stderr',args)
if(engineOptions.debugLog)console.log(...(['shinobi-video-synopsis'].concat(args)))
}
engine.extractFramesFromVideo = extractFramesFromVideo
engine.mergeFramePairs = mergeFramePairs
engine.createVideoFromJpegImages = createVideoFromJpegImages
engine.about = async () => {
return {
runningProcesses: alreadyRunning,
percentCompleted: percentCompleted,
initiatedAt: initiatedAt,
debugLog: engineOptions.debugLog || false,
cancelledProcess: engine.cancelledProcess,
logs: processLogs,
}
}
engine.cleanBuildFiles = async () => {
await deleteFile(subtractedBackgrounds)
await deleteFile(firstFrameDirectory)
await deleteFile(mergedFramesDirectory)
}
engine.cancel = () => {
let message = 'Cancelling...'
if(!engine.cancelledProcess){
engine.cancelledProcess = true
try{
if(engine.videoBuildProcess)engine.videoBuildProcess.kill('SIGTERM')
}catch(err){
engine.logStderr(err)
}
}else if(!engine.runningProcesses){
message = 'Already Cancelled!'
}
return {
ok: true,
msg: message
}
}
engine.run = async (options) => {
if(!options.videos)return console.log('No Videos Provided')
if(alreadyRunning)return "Already Running";
processLogs = []
engine.runningProcesses = true
engine.cancelledProcess = false
alreadyRunning = true;
const videos = options.videos
// create folders
sendPercentComplete(0)
createBaseFolder(subtractedBackgrounds)
createBaseFolder(firstFrameDirectory)
createBaseFolder(mergedFramesDirectory)
// extract frames and remove backgrounds, BackgroundSubtractorMOG2
sendPercentComplete(5)
var extractionPercent = 0
for (let i = 0; i < videos.length; i++) {
if(engine.cancelledProcess){
alreadyRunning = false;
engine.runningProcesses = false
return {
ok: false,
msg: "Cancelled"
}
}
const videoName = videos[i];
const videoFilename = videoName + '.mp4';
createBaseFolder(subtractedBackgrounds + getVideoFileName(videoName))
grabFirstFrame(firstFrameDirectory,videoFilename,getVideoFileName(videoName));
engine.logStderr(`Extracting frames from video named "${videoName}.mp4"...`)
await extractFramesFromVideo({
engine: engine,
videoName: videoName,
subtractedBackgroundsDirectory: subtractedBackgrounds,
delayBetweenFrames: 50,
});
extractionPercent += 20 / videos.length
sendPercentComplete(parseInt(5 + extractionPercent))
engine.logStderr(`Done extracting frames from "${videoName}.mp4"!`)
}
// create listing of which frames to overlap then merge them and write into ${subtractedBackgrounds}
if(engine.cancelledProcess){
alreadyRunning = false;
engine.runningProcesses = false
return {
ok: false,
msg: "Cancelled"
}
}
engine.logStderr("Merging Frames Extracted...")
sendPercentComplete(35)
var extractionPercent = 0
const mergeFramePairsResponse = await mergeFramePairs({
//engine running the request
engine: engine,
//folder where files are
subtractedBackgroundsDirectory: subtractedBackgrounds,
//file list (videoNames only, not full path)
videoNames: fs.readdirSync(subtractedBackgrounds),
//
mergedFramesDirectory: mergedFramesDirectory,
firstFrameDirectory: firstFrameDirectory,
onFrameComplete: (currentNumber,maxNumber) => {
extractionPercent += 15 / maxNumber
sendPercentComplete(parseInt(35 + extractionPercent))
}
})
sendPercentComplete(50)
if(engine.cancelledProcess){
alreadyRunning = false;
engine.runningProcesses = false
return {
ok: false,
msg: "Cancelled"
}
}
// create a video from merged frames
engine.logStderr("Create Video From Merged Frames...")
sendPercentComplete(65)
options.fps = options.fps || 25
const selectedFps = mergeFramePairsResponse.numberOfFrames < options.fps ? mergeFramePairsResponse.numberOfFrames : options.fps
const buildResponse = await createVideoFromJpegImages({
//engine running the request
engine: engine,
//ffmpeg binary path
ffmpegBinary: ffmpegBinary,
//folder where files are
mergedFramesDirectory: mergedFramesDirectory,
//file list (filenames only, not full path)
frames: fs.readdirSync(mergedFramesDirectory),
//fps
fps: selectedFps,
//final video location
output: options.output
})
sendPercentComplete(85)
engine.logStderr("Completed Building!")
engine.logStderr({
mergeFramePairs: mergeFramePairsResponse,
videoBuild: buildResponse,
})
sendPercentComplete(100)
alreadyRunning = false;
engine.runningProcesses = false
return {
mergeFramePairs: mergeFramePairsResponse,
videoBuild: buildResponse,
}
}
return engine
}