Shinobi/libs/video/utils.js

318 lines
14 KiB
JavaScript

const fs = require('fs')
const { spawn } = require('child_process')
module.exports = (s,config,lang) => {
const {
splitForFFPMEG,
} = require('../ffmpeg/utils.js')(s,config,lang)
// orphanedVideoCheck : new function
const checkIfVideoIsOrphaned = (monitor,videosDirectory,filename) => {
const response = {ok: true}
return new Promise((resolve,reject) => {
fs.stat(videosDirectory + filename,(err,stats) => {
if(!err && stats.size > 10){
s.knexQuery({
action: "select",
columns: "*",
table: "Videos",
where: [
['ke','=',monitor.ke],
['mid','=',monitor.mid],
['time','=',s.nameToTime(filename)],
],
limit: 1
},(err,rows) => {
if(!err && (!rows || !rows[0])){
//row does not exist, create one for video
var video = rows[0]
s.insertCompletedVideo(monitor,{
file : filename
},() => {
response.status = 2
resolve(response)
})
}else{
//row exists, no errors
response.status = 1
resolve(response)
}
})
}else{
response.status = 0
resolve(response)
}
})
})
}
const scanForOrphanedVideos = (monitor,options) => {
// const options = {
// checkMax: 2
// }
options = options ? options : {}
return new Promise((resolve,reject) => {
const response = {ok: false}
if(options.forceCheck === true || config.insertOrphans === true){
if(!options.checkMax){
options.checkMax = config.orphanedVideoCheckMax || 2
}
let finished = false
let orphanedFilesCount = 0;
let videosFound = 0;
const videosDirectory = s.getVideoDirectory(monitor)
const tempDirectory = s.getStreamsDirectory(monitor)
// const findCmd = [videosDirectory].concat(options.flags || ['-maxdepth','1'])
fs.writeFileSync(
tempDirectory + 'orphanCheck.sh',
`find "${videosDirectory}" -maxdepth 1 -type f -exec stat -c "%n" {} + | sort -r | head -n ${options.checkMax}`
);
let listing = spawn('sh',[tempDirectory + 'orphanCheck.sh'])
// const onData = options.onData ? options.onData : () => {}
const onError = options.onError ? options.onError : s.systemLog
const onExit = async () => {
try{
listing.kill('SIGTERM')
await fs.promises.rm(tempDirectory + 'orphanCheck.sh')
}catch(err){
s.debugLog(err)
}
delete(listing)
}
const onFinish = () => {
if(!finished){
finished = true
response.ok = true
response.orphanedFilesCount = orphanedFilesCount
resolve(response)
onExit()
}
}
const processLine = async (filePath) => {
let filename = filePath.split('/')
filename = `${filename[filename.length - 1]}`.trim()
if(filename && filename.indexOf('-') > -1 && filename.indexOf('.') > -1){
const { status } = await checkIfVideoIsOrphaned(monitor,videosDirectory,filename)
if(status === 2){
++orphanedFilesCount
}
++videosFound
if(videosFound === options.checkMax){
onFinish()
}
}
}
listing.stdout.on('data', async (d) => {
const filePathLines = d.toString().split('\n')
var i;
for (i = 0; i < filePathLines.length; i++) {
await processLine(filePathLines[i])
}
})
listing.stderr.on('data', d=>onError(d.toString()))
listing.on('close', (code) => {
// s.debugLog(`findOrphanedVideos ${monitor.ke} : ${monitor.mid} process exited with code ${code}`);
setTimeout(() => {
onFinish()
},1000)
});
}else{
resolve(response)
}
})
}
// orphanedVideoCheck : old function
const orphanedVideoCheck = (monitor,checkMax,callback,forceCheck) => {
var finish = function(orphanedFilesCount){
if(callback)callback(orphanedFilesCount)
}
if(forceCheck === true || config.insertOrphans === true){
if(!checkMax){
checkMax = config.orphanedVideoCheckMax || 2
}
var videosDirectory = s.getVideoDirectory(monitor)// + s.formattedTime(video.time) + '.' + video.ext
fs.readdir(videosDirectory,function(err,files){
if(files && files.length > 0){
var fiveRecentFiles = files.slice(files.length - checkMax,files.length)
var completedFile = 0
var orphanedFilesCount = 0
var fileComplete = function(){
++completedFile
if(fiveRecentFiles.length === completedFile){
finish(orphanedFilesCount)
}
}
fiveRecentFiles.forEach(function(filename){
if(/T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(filename)){
fs.stat(videosDirectory + filename,(err,stats) => {
if(!err && stats.size > 10){
s.knexQuery({
action: "select",
columns: "*",
table: "Videos",
where: [
['ke','=',monitor.ke],
['mid','=',monitor.mid],
['time','=',s.nameToTime(filename)],
],
limit: 1
},(err,rows) => {
if(!err && (!rows || !rows[0])){
++orphanedFilesCount
var video = rows[0]
s.insertCompletedVideo(monitor,{
file : filename
},() => {
fileComplete()
})
}else{
fileComplete()
}
})
}
})
}
})
}else{
finish()
}
})
}else{
finish()
}
}
function cutVideoLength(options){
return new Promise((resolve,reject) => {
const response = {ok: false}
const inputFilePath = options.filePath
const monitorId = options.mid
const groupKey = options.ke
const cutLength = options.cutLength || 10
const tempDirectory = s.getStreamsDirectory(options)
let fileExt = inputFilePath.split('.')
fileExt = fileExt[fileExt.length -1]
const filename = `${s.gid(10)}.${fileExt}`
const videoOutPath = `${tempDirectory}${filename}`
const cuttingProcess = spawn(config.ffmpegDir,['-loglevel','warning','-i', inputFilePath, '-c','copy','-t',`${cutLength}`,videoOutPath])
cuttingProcess.stderr.on('data',(data) => {
const err = data.toString()
s.debugLog('cutVideoLength',options,err)
})
cuttingProcess.on('close',(data) => {
fs.stat(videoOutPath,(err) => {
if(!err){
response.ok = true
response.filename = filename
response.filePath = videoOutPath
setTimeout(() => {
s.file('delete',videoOutPath)
},1000 * 60 * 3)
}else{
s.debugLog('cutVideoLength:readFile',options,err)
}
resolve(response)
})
})
})
}
async function getVideosBasedOnTagFoundInMatrixOfAssociatedEvent({
groupKey,
monitorId,
startTime,
endTime,
searchQuery,
monitorRestrictions
}){
const initialEventQuery = [
['ke','=',groupKey],
['objects','LIKE',`%${searchQuery}%`],
]
if(monitorId)initialEventQuery.push(['mid','=',monitorId]);
if(startTime)initialEventQuery.push(['time','>',startTime]);
if(endTime)initialEventQuery.push(['end','<',endTime]);
if(monitorRestrictions)initialEventQuery.push(monitorRestrictions);
const videoSelectResponse = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Videos",
orderBy: ['time','desc'],
where: initialEventQuery
});
return videoSelectResponse
}
async function stitchMp4Files(options){
return new Promise((resolve,reject) => {
const concatListFile = options.listFile
const finalMp4OutputLocation = options.output
const commandString = `-y -threads 1 -f concat -safe 0 -i "${concatListFile}" -c:v copy -an -preset ultrafast "${finalMp4OutputLocation}"`
s.debugLog("stitchMp4Files",commandString)
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString))
videoBuildProcess.stdout.on('data',function(data){
s.debugLog('stdout',finalMp4OutputLocation,data.toString())
})
videoBuildProcess.stderr.on('data',function(data){
s.debugLog('stderr',finalMp4OutputLocation,data.toString())
})
videoBuildProcess.on('exit',async function(data){
resolve()
})
})
}
const fixingAlready = {}
async function reEncodeVideoAndReplace(videoRow){
return new Promise((resolve,reject) => {
const response = {ok: true}
const fixingId = `${videoRow.ke}${videoRow.mid}${videoRow.time}`
if(fixingAlready[fixingId]){
response.ok = false
response.msg = lang['Already Processing']
resolve(response)
}else{
const filename = s.formattedTime(videoRow.time)+'.'+videoRow.ext
const tempFilename = s.formattedTime(videoRow.time)+'.reencoding.'+videoRow.ext
const videoFolder = s.getVideoDirectory(videoRow)
const inputFilePath = `${videoFolder}${filename}`
const outputFilePath = `${videoFolder}${tempFilename}`
const commandString = `-y -threads 1 -re -i "${inputFilePath}" -c:v copy -c:a copy -preset ultrafast "${outputFilePath}"`
fixingAlready[fixingId] = true
const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString))
videoBuildProcess.stdout.on('data',function(data){
s.debugLog('stdout',outputFilePath,data.toString())
})
videoBuildProcess.stderr.on('data',function(data){
s.debugLog('stderr',outputFilePath,data.toString())
})
videoBuildProcess.on('exit',async function(data){
fixingAlready[fixingId] = false
try{
function failed(err){
response.ok = false
response.err = err
resolve(response)
}
const newFileStats = await fs.promises.stat(outputFilePath)
await fs.promises.rm(inputFilePath)
let readStream = fs.createReadStream(outputFilePath);
let writeStream = fs.createWriteStream(inputFilePath);
readStream.pipe(writeStream);
writeStream.on('finish', async () => {
resolve(response)
await fs.promises.rm(outputFilePath)
});
writeStream.on('error', failed);
readStream.on('error', failed);
}catch(err){
failed()
}
})
}
})
}
return {
reEncodeVideoAndReplace,
stitchMp4Files,
orphanedVideoCheck: orphanedVideoCheck,
scanForOrphanedVideos: scanForOrphanedVideos,
cutVideoLength: cutVideoLength,
getVideosBasedOnTagFoundInMatrixOfAssociatedEvent: getVideosBasedOnTagFoundInMatrixOfAssociatedEvent,
}
}