Shinobi/libs/ffmpeg/utils.js

303 lines
12 KiB
JavaScript

const exec = require('child_process').exec;
const spawn = require('child_process').spawn;
const treekill = require('tree-kill');
module.exports = (s,config,lang) => {
const {
mergeDeep,
validateIntValue,
} = require('../common.js')
const getPossibleWarnings = require('./possibleWarnings.js')
const activeProbes = {}
const runFFprobe = (url,auth,callback,forceOverlap) => {
var endData = {ok: false, result: {}}
if(!url){
endData.error = 'Missing URL'
callback(endData)
return
}
if(!forceOverlap && activeProbes[auth]){
endData.error = 'Account is already probing'
callback(endData)
return
}
activeProbes[auth] = 1
var stderr = ''
var stdout = ''
const probeCommand = splitForFFPMEG(`-analyzeduration 100000 probesize 100000 -v quiet -print_format json -show_format -show_streams -i "${url}"`)
var processTimeout = null
var ffprobeLocation = config.ffmpegDir.split('/')
ffprobeLocation[ffprobeLocation.length - 1] = 'ffprobe'
ffprobeLocation = ffprobeLocation.join('/')
const probeProcess = spawn(ffprobeLocation, probeCommand)
const finishReponse = () => {
delete(activeProbes[auth])
if(!stdout){
endData.error = stderr
}else{
endData.ok = true
endData.result = s.parseJSON(stdout)
}
endData.probe = probeCommand
callback(endData)
}
probeProcess.stderr.on('data',function(data){
stderr += data.toString()
})
probeProcess.stdout.on('data',function(data){
stdout += data.toString()
})
probeProcess.on('close',function(){
clearTimeout(processTimeout)
finishReponse()
})
processTimeout = setTimeout(() => {
treekill(probeProcess.pid)
finishReponse()
},4000)
}
const probeMonitor = (monitor,timeoutAmount,forceOverlap) => {
return new Promise((resolve,reject) => {
const url = s.buildMonitorUrl(monitor);
runFFprobe(url,`${monitor.ke}${monitor.mid}`,(response) => {
setTimeout(() => {
resolve(response)
},timeoutAmount || 1000)
},forceOverlap)
})
}
const getStreamInfoFromProbe = (probeResult) => {
const streamIndex = {
video: [],
audio: [],
all: []
}
const streams = probeResult.streams || []
streams.forEach((stream) => {
try{
const codecType = stream.codec_type || 'video'
const simpleInfo = {
fps: eval(stream.r_frame_rate) || '',
width: stream.coded_width,
height: stream.coded_height,
streamType: stream.codec_type,
codec: (stream.codec_name || '').toLowerCase(),
}
streamIndex.all.push(simpleInfo)
streamIndex[codecType].push(simpleInfo)
}catch(err){
console.log(err)
}
})
if(streamIndex.video.length === 0){
streamIndex.video.push({
streamType: 'video',
codec: 'unknown',
})
}
return streamIndex
}
const createWarningsForConfiguration = (monitor,probeResult) => {
const warnings = []
const possibleWarnings = getPossibleWarnings(monitor,probeResult,config,lang)
possibleWarnings.forEach((warning) => {
if(warning.isTrue)warnings.push(warning)
})
return warnings
}
const buildMonitorConfigPartialFromWarnings = (warnings) => {
var configPartial = {}
warnings.forEach((warning) => {
if(warning.automaticChange)configPartial = mergeDeep(configPartial,warning.automaticChange)
})
return configPartial
}
const repairConfiguration = (monitor,probeResult) => {
const warnings = createWarningsForConfiguration(monitor,probeResult)
const configPartial = buildMonitorConfigPartialFromWarnings(warnings)
return mergeDeep(monitor,configPartial)
}
const applyPartialToConfiguration = (activeMonitor,configPartial) => {
Object.keys(configPartial).forEach((key) => {
if(key !== 'details'){
activeMonitor[key] = configPartial[key]
}else{
const details = s.parseJSON(configPartial.details)
Object.keys(details).forEach((key) => {
activeMonitor.details[key] = details[key]
})
}
})
}
const getInputTypeFlags = (inputType) => {
switch(inputType){
case'socket':case'jpeg':case'pipe'://case'webpage':
return `-pattern_type glob -f image2pipe -vcodec mjpeg`
break;
case'mjpeg':
return `-reconnect 1 -f mjpeg`
break;
case'mxpeg':
return `-reconnect 1 -f mxg`
break;
default:
return ``
break;
}
}
const buildConnectionFlagsFromConfiguration = (monitor) => {
const url = s.buildMonitorUrl(monitor);
switch(monitor.type){
case'dashcam':
return `-i -`
break;
case'socket':case'jpeg':case'pipe'://case'webpage':
return `-pattern_type glob -f image2pipe -vcodec mjpeg -i -`
break;
case'mjpeg':
return `-reconnect 1 -f mjpeg -i "${url}"`
break;
case'mxpeg':
return `-reconnect 1 -f mxg -i "${url}"`
break;
case'rtmp':
if(!monitor.details.rtmp_key)monitor.details.rtmp_key = ''
return `-i "rtmp://127.0.0.1:1935/${monitor.ke}_${monitor.mid}_${monitor.details.rtmp_key}"`
break;
case'h264':case'hls':case'mp4':
return `-i "${url}"`
break;
case'local':
return `-i "${monitor.path}"`
break;
}
}
const hasInputMaps = (e) => {
return (e.details.input_maps && e.details.input_maps.length > 0)
}
const buildInputMap = function(e,arrayOfMaps){
//`e` is the monitor object
var string = '';
if(hasInputMaps(e)){
if(arrayOfMaps && arrayOfMaps instanceof Array && arrayOfMaps.length>0){
arrayOfMaps.forEach(function(v){
if(v.map==='')v.map='0'
string += ' -map '+v.map
})
}else{
var primaryMap = '0:0'
if(e.details.primary_input && e.details.primary_input !== ''){
var primaryMap = e.details.primary_input || '0:0'
string += ' -map ' + primaryMap
}
}
}
return string;
}
const buildWatermarkFiltersFromConfiguration = (prefix,monitor,detail,detailKey) => {
prefix = prefix ? prefix : ''
const parameterContainer = detail ? detailKey ? monitor.details[detail][detailKey] : monitor.details[detail] : monitor.details
const watermarkLocation = parameterContainer[`${prefix}watermark_location`]
//bottom right is default
var watermarkPosition = '(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2'
switch(parameterContainer[`${prefix}watermark_position`]){
case'tl'://top left
watermarkPosition = '10:10'
break;
case'tr'://top right
watermarkPosition = 'main_w-overlay_w-10:10'
break;
case'bl'://bottom left
watermarkPosition = '10:main_h-overlay_h-10'
break;
}
return `movie=${watermarkLocation}[watermark],[in][watermark]overlay=${watermarkPosition}[out]`
}
const buildRotationFiltersFromConfiguration = (prefix,monitor,detail,detailKey) => {
prefix = prefix ? prefix : ''
const parameterContainer = detail ? detailKey ? monitor.details[detail][detailKey] : monitor.details[detail] : monitor.details
const userChoice = parameterContainer[`${prefix}rotate`]
switch(userChoice){
case'2,transpose=2':
case'0':
case'1':
case'2':
case'3':
return `transpose=${userChoice}`
break;
}
return ``
}
const buildTimestampFiltersFromConfiguration = (prefix,monitor,detail,detailKey) => {
prefix = prefix ? prefix : ''
const parameterContainer = detail ? detailKey ? monitor.details[detail][detailKey] : monitor.details[detail] : monitor.details
const timestampFont = parameterContainer[`${prefix}timestamp_font`] ? parameterContainer[`${prefix}timestamp_font`] : '/usr/share/fonts/truetype/freefont/FreeSans.ttf'
const timestampX = parameterContainer[`${prefix}timestamp_x`] ? parameterContainer[`${prefix}timestamp_x`] : '(w-tw)/2'
const timestampY = parameterContainer[`${prefix}timestamp_y`] ? parameterContainer[`${prefix}timestamp_y`] : '0'
const timestampColor = parameterContainer[`${prefix}timestamp_color`] ? parameterContainer[`${prefix}timestamp_color`] : 'white'
const timestampBackgroundColor = parameterContainer[`${prefix}timestamp_box_color`] ? parameterContainer[`${prefix}timestamp_box_color`] : '0x00000000@1'
const timestampFontSize = parameterContainer[`${prefix}timestamp_font_size`] ? parameterContainer[`${prefix}timestamp_font_size`] : '10'
return `'drawtext=fontfile=${timestampFont}:text=\'%{localtime}\':x=${timestampX}:y=${timestampY}:fontcolor=${timestampColor}:box=1:boxcolor=${timestampBackgroundColor}:fontsize=${timestampFontSize}`
}
const validateDimensions = (oldWidth,oldHeight) => {
const width = validateIntValue(oldWidth)
const height = validateIntValue(oldHeight)
return {
videoWidth: width,
videoHeight: height,
}
}
const sanitizedFfmpegCommand = (e,ffmpegCommandString) => {
var sanitizedCmd = `${ffmpegCommandString}`
if(e.details.muser && e.details.mpass){
sanitizedCmd = ffmpegCommandString
.replace(`//${e.details.muser}:${e.details.mpass}@`,'//')
.replace(`=${e.details.muser}`,'=USERNAME')
.replace(`=${e.details.mpass}`,'=PASSWORD')
}else if(e.details.muser){
sanitizedCmd = ffmpegCommandString.replace(`//${e.details.muser}:@`,'//').replace(`=${e.details.muser}`,'=USERNAME')
}
return sanitizedCmd
}
const createPipeArray = function(e){
const stdioPipes = [];
var times = config.pipeAddition;
if(e.details.stream_channels){
times+=e.details.stream_channels.length
}
for(var i=0; i < times; i++){
stdioPipes.push('pipe')
}
return stdioPipes
}
const splitForFFPMEG = function(ffmpegCommandAsString) {
return ffmpegCommandAsString.replace(/\s+/g,' ').trim().match(/\\?.|^$/g).reduce((p, c) => {
if(c === '"'){
p.quote ^= 1;
}else if(!p.quote && c === ' '){
p.a.push('');
}else{
p.a[p.a.length-1] += c.replace(/\\(.)/,"$1");
}
return p;
}, {a: ['']}).a
}
return {
ffprobe: runFFprobe,
probeMonitor: probeMonitor,
getStreamInfoFromProbe: getStreamInfoFromProbe,
createWarningsForConfiguration: createWarningsForConfiguration,
buildMonitorConfigPartialFromWarnings: buildMonitorConfigPartialFromWarnings,
applyPartialToConfiguration: applyPartialToConfiguration,
repairConfiguration: repairConfiguration,
buildTimestampFiltersFromConfiguration: buildTimestampFiltersFromConfiguration,
buildWatermarkFiltersFromConfiguration: buildWatermarkFiltersFromConfiguration,
validateDimensions: validateDimensions,
buildConnectionFlagsFromConfiguration: buildConnectionFlagsFromConfiguration,
buildInputMap: buildInputMap,
getInputTypeFlags: getInputTypeFlags,
sanitizedFfmpegCommand: sanitizedFfmpegCommand,
createPipeArray: createPipeArray,
splitForFFPMEG: splitForFFPMEG,
}
}