Shinobi/libs/ffmpeg.js

846 lines
40 KiB
JavaScript

const fs = require('fs');
const spawn = require('child_process').spawn;
const execSync = require('child_process').execSync;
const {
arrayContains,
} = require('./common.js')
module.exports = function(s,config,lang,onFinish){
const {
buildTimestampFiltersFromConfiguration,
buildWatermarkFiltersFromConfiguration,
buildConnectionFlagsFromConfiguration,
validateDimensions,
getInputTypeFlags,
buildInputMap,
sanitizedFfmpegCommand,
createPipeArray,
splitForFFPMEG,
} = require('./ffmpeg/utils.js')(s,config,lang)
if(config.ffmpegBinary)config.ffmpegDir = config.ffmpegBinary
var ffmpeg = {}
var downloadingFfmpeg = false;
const hasCudaEnabled = (monitor) => {
return monitor.details.accelerator === '1' && monitor.details.hwaccel === 'cuvid' && monitor.details.hwaccel_vcodec === ('h264_cuvid' || 'hevc_cuvid' || 'mjpeg_cuvid' || 'mpeg4_cuvid')
}
const inputTypeIsStreamer = (monitor) => {
return monitor.type === 'dashcam'|| monitor.type === 'socket'
}
//check local ffmpeg
const checkForWindows = function(failback){
if (s.isWin && fs.existsSync(s.mainDirectory+'/ffmpeg/ffmpeg.exe')) {
config.ffmpegDir = s.mainDirectory+'/ffmpeg/ffmpeg.exe'
}else{
failback()
}
}
//check local ffmpeg
const checkForUnix = function(failback){
if(s.isWin === false){
if (fs.existsSync('/usr/bin/ffmpeg')) {
config.ffmpegDir = '/usr/bin/ffmpeg'
}else{
if (fs.existsSync('/usr/local/bin/ffmpeg')) {
config.ffmpegDir = '/usr/local/bin/ffmpeg'
}else{
failback()
}
}
}else{
failback()
}
}
//check node module : ffmpeg-static
const checkForNpmStatic = function(failback){
try{
var staticFFmpeg = require('ffmpeg-static').path;
if (fs.statSync(staticFFmpeg)) {
config.ffmpegDir = staticFFmpeg
}else{
console.log('"ffmpeg-static" from NPM has failed to provide a compatible library or has been corrupted.')
console.log('Run "npm uninstall ffmpeg-static" to remove it.')
console.log('Run "npm install ffbinaries" to get a different static FFmpeg downloader.')
}
}catch(err){
console.log('No "ffmpeg-static".')
failback()
}
}
//check node module : ffbinaries
const checkForFfbinary = function(failback){
try{
ffbinaries = require('ffbinaries')
var ffbinaryDir = s.mainDirectory + '/ffmpeg/'
var downloadFFmpeg = function(){
downloadingFfmpeg = true
console.log('ffbinaries : Downloading FFmpeg. Please Wait...');
ffbinaries.downloadBinaries(['ffmpeg', 'ffprobe'], {
destination: ffbinaryDir,
version : '3.4'
},function () {
config.ffmpegDir = ffbinaryDir + 'ffmpeg'
console.log('ffbinaries : FFmpeg Downloaded.');
completeCheck()
})
}
if (!fs.existsSync(ffbinaryDir + 'ffmpeg')) {
downloadFFmpeg()
}else{
config.ffmpegDir = ffbinaryDir + 'ffmpeg'
}
}catch(err){
console.log('No "ffbinaries". Continuing.')
console.log('Run "npm install ffbinaries" to get this static FFmpeg downloader.')
failback()
}
}
//ffmpeg version
const checkVersion = function(callback){
try{
s.ffmpegVersion = execSync(config.ffmpegDir+" -version").toString().split('Copyright')[0].replace('ffmpeg version','').trim()
if(s.ffmpegVersion.indexOf(': 2.')>-1){
s.systemLog('FFMPEG is too old : '+s.ffmpegVersion+', Needed : 3.2+',err)
throw (new Error())
}
}catch(err){
console.log('No FFmpeg found.')
// process.exit()
}
callback()
}
//check available hardware acceleration methods
const checkHwAccelMethods = function(callback){
if(config.availableHWAccels === undefined){
hwAccels = execSync(config.ffmpegDir+" -loglevel quiet -hwaccels").toString().split('\n')
hwAccels.shift()
availableHWAccels = []
hwAccels.forEach(function(method){
if(method && method !== '')availableHWAccels.push(method.trim())
})
config.availableHWAccels = availableHWAccels
config.availableHWAccels = ['auto'].concat(config.availableHWAccels)
console.log('Available Hardware Acceleration Methods : ',availableHWAccels.join(', '))
var methods = {
auto: {label:lang['Auto'],value:'auto'},
drm: {label:lang['drm'],value:'drm'},
cuvid: {label:lang['cuvid'],value:'cuvid'},
cuda: {label:lang['cuda'],value:'cuda'},
opencl: {label:lang['opencl'],value:'opencl'},
vaapi: {label:lang['vaapi'],value:'vaapi'},
qsv: {label:lang['qsv'],value:'qsv'},
vdpau: {label:lang['vdpau'],value:'vdpau'},
dxva2: {label:lang['dxva2'],value:'dxva2'},
vdpau: {label:lang['vdpau'],value:'vdpau'},
videotoolbox: {label:lang['videotoolbox'],value:'videotoolbox'}
}
s.listOfHwAccels = []
config.availableHWAccels.forEach(function(availibleMethod){
if(methods[availibleMethod]){
var method = methods[availibleMethod]
s.listOfHwAccels.push({
name: method.label,
value: method.value,
})
}
})
}
callback()
}
const completeCheck = function(){
checkVersion(function(){
checkHwAccelMethods(function(){
s.onFFmpegLoadedExtensions.forEach(function(extender){
extender(ffmpeg)
})
onFinish(ffmpeg)
})
})
}
const createInputMap = (e, number, input) => {
//`e` is the monitor object
//`x` is an object used to contain temporary values.
const inputFlags = []
const inputTypeIsH264 = input.type === 'h264'
const inputTypeCanLoop = input.type === 'mp4' || input.type === 'local'
const hardwareAccelerationEnabled = input.accelerator==='1'
const rtspTransportIsManual = input.rtsp_transport && input.rtsp_transport !== 'no'
const monitorCaptureRate = !isNaN(parseFloat(input.sfps)) && input.sfps !== '0' ? parseFloat(input.sfps) : null
const casualDecodingRequired = input.type === 'mp4' || input.type === 'mjpeg'
if(input.cust_input)inputFlags.push(input.cust_input)
if(monitorCaptureRate){
inputFlags.push(`-r ${monitorCaptureRate}`)
}
if(input.aduration){
inputFlags.push(`-analyzeduration ${input.aduration}`)
}
if(input.probesize){
inputFlags.push(`-probesize ${input.probesize}`)
}
if(input.stream_loop === '1' && inputTypeCanLoop){
inputFlags.push(`-stream_loop -1`)
}
if(
(input.type === 'h264' || input.type === 'mp4') &&
input.fulladdress.indexOf('rtsp://') >- 1 &&
input.rtsp_transport &&
input.rtsp_transport !== 'no'
){
inputFlags.push(`-rtsp_transport ${input.rtsp_transport}`)
}
//hardware acceleration
if(hardwareAccelerationEnabled){
if(input.hwaccel){
inputFlags.push(`-hwaccel ${input.hwaccel}`)
}
if(input.hwaccel_vcodec){
inputFlags.push(`-c:v ${input.hwaccel_vcodec}`)
}
if(input.hwaccel_device){
switch(input.hwaccel){
case'vaapi':
inputFlags.push(`-vaapi_device ${input.hwaccel_device}`)
break;
default:
inputFlags.push(`-hwaccel_device ${input.hwaccel_device}`)
break;
}
}
}
//custom - input flags
return `${getInputTypeFlags(input.type)} ${inputFlags.join(' ')} -i "${input.fulladdress}"`
}
//create sub stream channel
const createStreamChannel = function(e,number,channel){
//`e` is the monitor object
//`x` is an object used to contain temporary values.
const channelStreamDirectory = !isNaN(parseInt(number)) ? `${e.sdir}channel${number}/` : e.sdir
if(channelStreamDirectory !== e.sdir && !fs.existsSync(channelStreamDirectory)){
fs.mkdirSync(channelStreamDirectory)
}
const channelNumber = number - config.pipeAddition
const isCudaEnabled = hasCudaEnabled(e)
const streamFlags = []
const streamFilters = []
const videoCodecisCopy = channel.stream_vcodec === 'copy'
const audioCodecisCopy = channel.stream_acodec === 'copy'
const videoCodec = channel.stream_vcodec ? channel.stream_vcodec : 'libx264'
const audioCodec = channel.stream_acodec ? channel.stream_acodec : 'aac'
const videoQuality = channel.stream_quality ? channel.stream_quality : '1'
const streamType = channel.stream_type ? channel.stream_type : 'hls'
const videoFps = !isNaN(parseFloat(channel.stream_fps)) && channel.stream_fps !== '0' ? parseFloat(channel.stream_fps) : streamType === 'rtmp' ? '30' : null
const inputMap = buildInputMap(e,e.details.input_map_choices[`stream_channel-${channelNumber}`])
const outputCanHaveAudio = (streamType === 'hls' || streamType === 'mp4' || streamType === 'flv' || streamType === 'h265' || streamType === 'rtmp')
const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64'
const outputIsPresetCapable = outputCanHaveAudio
const { videoWidth, videoHeight } = validateDimensions(channel.stream_scale_x,channel.stream_scale_y)
if(inputMap)streamFlags.push(inputMap)
if(channel.cust_stream)streamFlags.push(channel.cust_stream)
if(streamFlags.indexOf('-strict -2') === -1)streamFlags.push(`-strict -2`)
if(channel.stream_timestamp === "1" && !videoCodecisCopy){
streamFilters.push(buildTimestampFiltersFromConfiguration('stream_',e,`stream_channels`,channelNumber))
}
if(channel.stream_watermark === "1" && channel.stream_watermark_location){
streamFilters.push(buildWatermarkFiltersFromConfiguration(`stream_`,e,`stream_channels`,channelNumber))
}
if(channel.stream_rotate && channel.stream_rotate !== "no" && channel.stream_vcodec !== 'copy'){
streamFilters.push(buildRotationFiltersFromConfiguration(`stream_`,e,`stream_channels`,channelNumber))
}
if(outputCanHaveAudio && audioCodec !== 'no'){
streamFlags.push(`-c:a ` + audioCodec)
}else{
streamFlags.push(`-an`)
}
if(videoCodec === 'h264_vaapi'){
streamFilters.push('format=nv12,hwupload');
if(channel.stream_scale_x && channel.stream_scale_y){
streamFilters.push('scale_vaapi=w='+channel.stream_scale_x+':h='+channel.stream_scale_y)
}
}
if(isCudaEnabled && (streamType === 'mjpeg' || streamType === 'b64')){
streamFilters.push('hwdownload,format=nv12')
}
if(!outputRequiresEncoding && videoCodec !== 'no'){
streamFlags.push(`-c:v ${videoCodec === 'libx264' ? 'h264' : videoCodec}`)
}
if(!videoCodecisCopy || outputRequiresEncoding){
if(videoWidth && videoHeight)streamFlags.push(`-s ${videoWidth}x${videoHeight}`)
if(videoFps && streamType === 'mjpeg' || streamType === 'b64'){
streamFilters.push(`fps=${videoFps}`)
}
}
if(channel.stream_vf){
streamFilters.push(channel.stream_vf)
}
if(outputIsPresetCapable){
const streamPreset = streamType !== 'h265' && channel.preset_stream ? channel.preset_stream : null
if(streamPreset){
streamFlags.push(`-preset ${streamPreset}`)
}
if(!videoCodecisCopy){
streamFlags.push(`-crf ${videoQuality}`)
}
}else{
streamFlags.push(`-q:v ${videoQuality}`)
}
if((!videoCodecisCopy || outputRequiresEncoding) && streamFilters.length > 0){
streamFlags.push(`-vf "${streamFilters.join(',')}"`)
}
switch(streamType){
case'rtmp':
const rtmpServerUrl = s.checkCorrectPathEnding(channel.rtmp_server_url)
if(channel.stream_v_br && !videoCodecisCopy){
streamFlags.push(`-b:v ${channel.stream_v_br}`)
}
if(!audioCodecisCopy && audioCodec !== 'no'){
streamFlags.push(`-ab ${channel.stream_a_br || '128k'}`)
}
streamFlags.push(`-f flv "${rtmpServerUrl + channel.rtmp_stream_key}"`)
break;
case'mp4':
streamFlags.push('-f mp4 -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream from Shinobi" -reset_timestamps 1 pipe:${number}')
break;
case'flv':
streamFlags.push(`-f flv`,'pipe:${number}')
break;
case'hls':
const hlsTime = !isNaN(parseInt(channel.hls_time)) ? `${parseInt(channel.hls_time)}` : '2'
const hlsListSize = !isNaN(parseInt(channel.hls_list_size)) ? `${parseInt(channel.hls_list_size)}` : '2'
if(videoCodec !== 'h264_vaapi' && !videoCodecisCopy){
if(!arrayContains('-tune',streamFlags)){
streamFlags.push(`-tune zerolatency`)
}
if(!arrayContains('-g ',streamFlags)){
streamFlags.push(`-g 1`)
}
}
streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${channelStreamDirectory}s.m3u8"`)
break;
case'mjpeg':
streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:${number}`)
break;
case'h265':
streamFlags.push(`-movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1 -f hevc pipe:${number}`)
break;
case'b64':case'':case undefined:case null://base64
streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:${number}`)
break;
}
return ' ' + streamFlags.join(' ')
}
const buildMainInput = function(e){
//e = monitor object
//x = temporary values
const isStreamer = inputTypeIsStreamer(e)
const isCudaEnabled = hasCudaEnabled(e)
const inputFlags = []
const useWallclockTimestamp = config.wallClockTimestampAsDefault || e.details.wall_clock_timestamp_ignore !== '1'
const inputTypeIsH264 = e.type === 'h264'
const protocolIsRtsp = e.protocol === 'rtsp'
const inputTypeCanLoop = e.type === 'mp4' || e.type === 'local'
const hardwareAccelerationEnabled = e.details.accelerator==='1'
const rtspTransportIsManual = e.details.rtsp_transport && e.details.rtsp_transport !== 'no'
const monitorCaptureRate = !isNaN(parseFloat(e.details.sfps)) && e.details.sfps !== '0' ? parseFloat(e.details.sfps) : null
const logLevel = e.details.loglevel ? e.details.loglevel : 'warning'
const casualDecodingRequired = e.type === 'mp4' || e.type === 'mjpeg'
if(e.details.cust_input)inputFlags.push(e.details.cust_input)
if(useWallclockTimestamp && inputTypeIsH264 && !arrayContains('-use_wallclock_as_timestamps',inputFlags)){
inputFlags.push('-use_wallclock_as_timestamps 1')
}
if(monitorCaptureRate){
inputFlags.push(`-r ${monitorCaptureRate}`)
}
if(e.details.aduration){
inputFlags.push(`-analyzeduration ${e.details.aduration}`)
}
if(e.details.probesize){
inputFlags.push(`-probesize ${e.details.probesize}`)
}
if(e.details.stream_loop === '1' && inputTypeCanLoop){
inputFlags.push(`-stream_loop -1`)
}
if(!arrayContains('-fflags',inputFlags)){
inputFlags.push(`-fflags +igndts`)
}
if(inputTypeIsH264 && protocolIsRtsp && rtspTransportIsManual){
inputFlags.push(`-rtsp_transport ${e.details.rtsp_transport}`)
}
//hardware acceleration
if(hardwareAccelerationEnabled && !isStreamer){
if(e.details.hwaccel){
inputFlags.push(`-hwaccel ${e.details.hwaccel}`)
}
if(e.details.hwaccel_vcodec){
inputFlags.push(`-c:v ${e.details.hwaccel_vcodec}`)
}
if(e.details.hwaccel_device){
switch(e.details.hwaccel){
case'vaapi':
inputFlags.push(`-vaapi_device ${e.details.hwaccel_device}`)
break;
default:
inputFlags.push(`-hwaccel_device ${e.details.hwaccel_device}`)
break;
}
}
}
inputFlags.push(`-loglevel ${logLevel}`)
//add main input
if(casualDecodingRequired && !arrayContains('-re',inputFlags)){
inputFlags.push(`-re`)
}
inputFlags.push(buildConnectionFlagsFromConfiguration(e))
if(e.details.input_maps){
e.details.input_maps.forEach(function(v,n){
inputFlags.push(createInputMap(e,n+1,v))
})
}
return inputFlags.join(' ')
}
const buildMainStream = function(e){
//e = monitor object
//x = temporary values
const isCudaEnabled = hasCudaEnabled(e)
const streamFlags = []
const streamFilters = []
const videoCodecisCopy = e.details.stream_vcodec === 'copy'
const videoCodec = e.details.stream_vcodec ? e.details.stream_vcodec : 'no'
const audioCodec = e.details.stream_acodec ? e.details.stream_acodec : 'no'
const videoQuality = e.details.stream_quality ? e.details.stream_quality : '1'
const streamType = e.details.stream_type ? e.details.stream_type : 'hls'
const videoFps = !isNaN(parseFloat(e.details.stream_fps)) && e.details.stream_fps !== '0' ? parseFloat(e.details.stream_fps) : null
const inputMap = buildInputMap(e,e.details.input_map_choices.stream)
const outputCanHaveAudio = (streamType === 'hls' || streamType === 'mp4' || streamType === 'flv' || streamType === 'h265')
const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64'
const outputIsPresetCapable = outputCanHaveAudio
const { videoWidth, videoHeight } = validateDimensions(e.details.stream_scale_x,e.details.stream_scale_y)
if(inputMap)streamFlags.push(inputMap)
if(e.details.cust_stream)streamFlags.push(e.details.cust_stream)
if(streamFlags.indexOf('-strict -2') === -1)streamFlags.push(`-strict -2`)
//stream - timestamp
if(e.details.stream_timestamp === "1" && !videoCodecisCopy){
streamFilters.push(buildTimestampFiltersFromConfiguration('stream_',e))
}
if(e.details.stream_watermark === "1" && e.details.stream_watermark_location){
streamFilters.push(buildWatermarkFiltersFromConfiguration(`stream_`,e))
}
//stream - rotation
if(e.details.stream_rotate && e.details.stream_rotate !== "no" && e.details.stream_vcodec !== 'copy'){
streamFilters.push(buildRotationFiltersFromConfiguration(`stream_`,e))
}
if(outputCanHaveAudio && audioCodec !== 'no'){
streamFlags.push(`-c:a ` + audioCodec)
}else{
streamFlags.push(`-an`)
}
if(videoCodec === 'h264_vaapi'){
streamFilters.push('format=nv12,hwupload');
if(e.details.stream_scale_x && e.details.stream_scale_y){
streamFilters.push('scale_vaapi=w='+e.details.stream_scale_x+':h='+e.details.stream_scale_y)
}
}
if(isCudaEnabled && (streamType === 'mjpeg' || streamType === 'b64')){
streamFilters.push('hwdownload,format=nv12')
}
if(!outputRequiresEncoding && videoCodec !== 'no'){
streamFlags.push(`-c:v ` + videoCodec)
}
if(!videoCodecisCopy || outputRequiresEncoding){
if(videoWidth && videoHeight)streamFlags.push(`-s ${videoWidth}x${videoHeight}`)
if(videoFps && streamType === 'mjpeg' || streamType === 'b64'){
streamFilters.push(`fps=${videoFps}`)
}
}
if(e.details.stream_vf){
streamFilters.push(e.details.stream_vf)
}
if(outputIsPresetCapable){
const streamPreset = streamType !== 'h265' && e.details.preset_stream ? e.details.preset_stream : null
if(streamPreset){
streamFlags.push(`-preset ${streamPreset}`)
}
if(!videoCodecisCopy){
streamFlags.push(`-crf ${videoQuality}`)
}
}else{
streamFlags.push(`-q:v ${videoQuality}`)
}
if((!videoCodecisCopy || outputRequiresEncoding) && streamFilters.length > 0){
streamFlags.push(`-vf "${streamFilters.join(',')}"`)
}
switch(streamType){
case'mp4':
streamFlags.push('-f mp4 -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream from Shinobi" -reset_timestamps 1 pipe:1')
break;
case'flv':
streamFlags.push(`-f flv`,'pipe:1')
break;
case'hls':
const hlsTime = !isNaN(parseInt(e.details.hls_time)) ? `${parseInt(e.details.hls_time)}` : '2'
const hlsListSize = !isNaN(parseInt(e.details.hls_list_size)) ? `${parseInt(e.details.hls_list_size)}` : '2'
if(videoCodec !== 'h264_vaapi' && !videoCodecisCopy){
if(!arrayContains('-tune',streamFlags)){
streamFlags.push(`-tune zerolatency`)
}
if(!arrayContains('-g ',streamFlags)){
streamFlags.push(`-g 1`)
}
}
streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${e.sdir}s.m3u8"`)
break;
case'mjpeg':
streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:1`)
break;
case'h265':
streamFlags.push(`-movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1 -f hevc pipe:1`)
break;
case'b64':case'':case undefined:case null://base64
streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:1`)
break;
}
if(e.details.custom_output){
streamFlags.push(e.details.custom_output)
}
if(e.details.stream_channels){
e.details.stream_channels.forEach(function(v,n){
streamFlags.push(createStreamChannel(e,n + config.pipeAddition,v))
})
}
return streamFlags.join(' ')
}
const buildJpegApiOutput = function(e){
if(e.details.snap === '1'){
const isCudaEnabled = hasCudaEnabled(e)
const videoFlags = []
const videoFilters = []
const inputMap = buildInputMap(e,e.details.input_map_choices.stream)
const { videoWidth, videoHeight } = validateDimensions(e.details.snap_scale_x,e.details.snap_scale_y)
if(inputMap)streamFlags.push(inputMap)
if(e.details.snap_vf)videoFilters.push(e.details.snap_vf)
if(isCudaEnabled){
videoFilters.push('hwdownload,format=nv12')
}
videoFilters.push(`fps=${e.details.snap_fps || '1'}`)
//-vf "thumbnail_cuda=2,hwdownload,format=nv12"
videoFlags.push(`-vf "${videoFilters.join(',')}"`)
if(videoWidth && videoHeight)videoFlags.push(`-s ${videoWidth}x${videoHeight}`)
if(e.details.cust_snap)videoFlags.push(e.details.cust_snap)
videoFlags.push(`-update 1 "${e.sdir}s.jpg" -y`)
return videoFlags.join(' ')
}
return ``
}
const buildMainRecording = function(e){
//e = monitor object
//x = temporary values
if(e.mode === 'record'){
const recordingFlags = []
const recordingFilters = []
const customRecordingFlags = []
const videoCodecisCopy = e.details.vcodec === 'copy'
const videoExtIsMp4 = e.ext === 'mp4'
const defaultVideoCodec = videoExtIsMp4 ? 'libx264' : 'libvpx'
const defaultAudioCodec = videoExtIsMp4 ? 'aac' : 'libvorbis'
const videoCodec = e.details.vcodec === 'default' ? defaultVideoCodec : e.details.vcodec ? e.details.vcodec : defaultVideoCodec
const audioCodec = e.details.acodec === 'default' ? defaultAudioCodec : e.details.acodec ? e.details.acodec : defaultAudioCodec
const videoQuality = e.details.crf ? e.details.crf : '1'
const videoFps = !isNaN(parseFloat(e.fps)) && e.fps !== '0' ? parseFloat(e.fps) : null
const segmentLengthInMinutes = !isNaN(parseFloat(e.details.cutoff)) ? parseFloat(e.details.cutoff) : '15'
const inputMap = buildInputMap(e,e.details.input_map_choices.record)
const { videoWidth, videoHeight } = validateDimensions(e.details.record_scale_x,e.details.record_scale_y)
if(inputMap)recordingFlags.push(inputMap)
if(e.details.cust_record)customRecordingFlags.push(e.details.cust_record)
//record - resolution
if(customRecordingFlags.indexOf('-strict -2') === -1)customRecordingFlags.push(`-strict -2`)
// if(customRecordingFlags.indexOf('-threads') === -1)customRecordingFlags.push(`-threads 10`)
if(!videoCodecisCopy){
if(videoWidth && videoHeight){
recordingFlags.push(`-s ${videoWidth}x${videoHeight}`)
}
if(videoExtIsMp4){
recordingFlags.push(`-crf ${videoQuality}`)
}else{
recordingFlags.push(`-q:v ${videoQuality}`)
}
if(videoFps){
recordingFilters.push(`fps=${videoFps}`)
}
}
if(videoExtIsMp4){
customRecordingFlags.push(`-segment_format_options movflags=faststart+frag_keyframe+empty_moov`)
}
if(videoCodec === 'h264_vaapi'){
recordingFilters.push('format=nv12,hwupload')
}
switch(e.type){
case'h264':case'hls':case'mp4':case'local':
if(audioCodec === 'no'){
recordingFlags.push(`-an`)
}else if(audioCodec !== 'none'){
recordingFlags.push(`-acodec ` + audioCodec)
}
break;
}
if(videoCodec !== 'none'){
recordingFlags.push(`-vcodec ` + videoCodec)
}
//record - timestamp options for -vf
if(e.details.timestamp === "1" && !videoCodecisCopy){
recordingFilters.push(buildTimestampFiltersFromConfiguration('',e))
}
//record - watermark for -vf
if(e.details.watermark === "1" && e.details.watermark_location){
recordingFilters.push(buildWatermarkFiltersFromConfiguration('',e))
}
if(e.details.rotate && e.details.rotate !== "no" && !videoCodecisCopy){
recordingFilters.push(buildRotationFiltersFromConfiguration(``,e))
}
if(e.details.vf){
recordingFilters.push(e.details.vf)
}
if(recordingFilters.length > 0){
recordingFlags.push(`-vf "${recordingFilters.join(',')}"`)
}
if(videoExtIsMp4 && e.details.preset_record){
recordingFlags.push(`-preset ${e.details.preset_record}`)
}
if(customRecordingFlags.length > 0){
recordingFlags.push(...customRecordingFlags)
}
//record - segmenting
recordingFlags.push(`-f segment -segment_atclocktime 1 -reset_timestamps 1 -strftime 1 -segment_list pipe:8 -segment_time ${(60 * segmentLengthInMinutes)} "${e.dir}%Y-%m-%dT%H-%M-%S.${e.ext || 'mp4'}"`);
return recordingFlags.join(' ')
}
return ``
}
const buildAudioDetector = function(e){
const outputFlags = []
if(e.details.detector_audio === '1'){
if(e.details.input_map_choices&&e.details.input_map_choices.detector_audio){
//add input feed map
outputFlags.push(buildInputMap(e,e.details.input_map_choices.detector_audio))
}else{
outputFlags.push('-map 0:a')
}
outputFlags.push('-acodec pcm_s16le -f s16le -ac 1 -ar 16000 pipe:6')
}
return outputFlags.join(' ')
}
const buildMainDetector = function(e){
//e = monitor object
//x = temporary values
const isCudaEnabled = hasCudaEnabled(e)
const detectorFlags = []
const inputMapsRequired = (e.details.input_map_choices && e.details.input_map_choices.detector)
const sendFramesGlobally = (e.details.detector_send_frames === '1')
const objectDetectorOutputIsEnabled = (e.details.detector_use_detect_object === '1')
const builtInMotionDetectorIsEnabled = (e.details.detector_pam === '1')
const sendFramesToObjectDetector = (e.details.detector_send_frames_object !== '0' && e.details.detector_use_detect_object === '1')
const baseWidth = e.details.detector_scale_x ? e.details.detector_scale_x : '640'
const baseHeight = e.details.detector_scale_y ? e.details.detector_scale_y : '480'
const baseDimensionsFlag = `-s ${baseWidth}x${baseHeight}`
const baseFps = e.details.detector_fps ? e.details.detector_fps : '2'
const baseFpsFilter = 'fps=' + baseFps
const objectDetectorDimensionsFlag = `-s ${e.details.detector_scale_x_object ? e.details.detector_scale_x_object : baseWidth}x${e.details.detector_scale_y_object ? e.details.detector_scale_y_object : baseHeight}`
const objectDetectorFpsFilter = 'fps=' + (e.details.detector_fps_object ? e.details.detector_fps_object : baseFps)
const cudaVideoFilters = 'hwdownload,format=nv12'
const videoFilters = []
if(e.details.detector === '1' && (sendFramesGlobally || sendFramesToObjectDetector)){
const addVideoFilters = () => {
if(videoFilters.length > 0)detectorFlags.push(' -vf "' + videoFilters.join(',') + '"')
}
const addInputMap = () => {
detectorFlags.push(buildInputMap(e,e.details.input_map_choices.detector))
}
const addObjectDetectorInputMap = () => {
detectorFlags.push(buildInputMap(e,e.details.input_map_choices.detector_object || e.details.input_map_choices.detector))
}
const addObjectDetectValues = () => {
const objVideoFilters = [objectDetectorFpsFilter]
if(e.details.cust_detect_object)detectorFlags.push(e.details.cust_detect_object)
if(isCudaEnabled)objVideoFilters.push(cudaVideoFilters)
detectorFlags.push(objectDetectorDimensionsFlag + ' -vf "' + objVideoFilters.join(',') + '"')
}
if(sendFramesGlobally){
if(builtInMotionDetectorIsEnabled)addInputMap();
detectorFlags.push(baseDimensionsFlag)
if(isCudaEnabled)videoFilters.push(cudaVideoFilters);
videoFilters.push(baseFpsFilter)
if(e.details.cust_detect)detectorFlags.push(e.details.cust_detect)
addVideoFilters()
if(builtInMotionDetectorIsEnabled){
detectorFlags.push('-an -c:v pam -pix_fmt gray -f image2pipe pipe:3')
if(objectDetectorOutputIsEnabled){
addObjectDetectorInputMap()
addObjectDetectValues()
detectorFlags.push('-an -f singlejpeg pipe:4')
}
}else if(sendFramesToObjectDetector){
addObjectDetectorInputMap()
addObjectDetectValues()
detectorFlags.push('-an -f singlejpeg pipe:4')
}else{
addInputMap()
detectorFlags.push('-an -f singlejpeg pipe:4')
}
}else if(sendFramesToObjectDetector){
addObjectDetectorInputMap()
addObjectDetectValues()
detectorFlags.push('-an -f singlejpeg pipe:4')
}
return detectorFlags.join(' ')
}
return ``
}
const buildEventRecordingOutput = (e) => {
const outputFlags = []
if(e.details.detector === '1' && e.details.detector_trigger === '1' && e.details.detector_record_method === 'sip'){
const isCudaEnabled = hasCudaEnabled(e)
const outputFilters = []
var videoCodec = e.details.detector_buffer_vcodec
var audioCodec = e.details.detector_buffer_acodec ? e.details.detector_buffer_acodec : 'no'
const videoCodecisCopy = videoCodec === 'copy'
const videoFps = !isNaN(parseFloat(e.details.stream_fps)) && e.details.stream_fps !== '0' ? parseFloat(e.details.stream_fps) : null
const inputMap = buildInputMap(e,e.details.input_map_choices.detector_sip_buffer)
const { videoWidth, videoHeight } = validateDimensions(e.details.record_scale_x,e.details.record_scale_y)
const hlsTime = !isNaN(parseInt(e.details.detector_buffer_hls_list_time)) ? `${parseInt(e.details.detector_buffer_hls_list_time)}` : '2'
const hlsListSize = !isNaN(parseInt(e.details.detector_buffer_hls_list_size)) ? `${parseInt(e.details.detector_buffer_hls_list_size)}` : '4'
if(inputMap)outputFlags.push(inputMap)
if(e.details.cust_record)outputFlags.push(e.details.cust_sip_record)
if(videoCodec === 'auto'){
if(e.type === 'h264' || e.type === 'hls' || e.type === 'mp4'){
videoCodec = `copy`
}else if(e.details.accelerator === '1' && isCudaEnabled){
videoCodec = 'h264_nvenc'
}else{
videoCodec = 'libx264'
}
}
if(audioCodec === 'auto'){
if(e.type === 'mjpeg' || e.type === 'jpeg' || e.type === 'socket'){
videoCodec = `no`
}else if(e.type === 'h264' || e.type === 'hls' || e.type === 'mp4'){
videoCodec = 'copy'
}else{
videoCodec = 'aac'
}
}
if(videoCodec !== 'copy'){
if(videoCodec.indexOf('_vaapi') >- 1){
if(!arrayContains('-vaapi_device',outputFlags)){
outputFilters.push('format=nv12')
outputFilters.push('hwupload')
}
}
if(videoFps){
outputFilters.push(`fps=${videoFps}`)
}
if(videoWidth && videoHeight){
outputFlags.push(`-s ${videoWidth}x${videoHeight}`)
}
}
if(videoCodec !== 'none'){
outputFlags.push(`-vcodec ` + videoCodec)
}
if(audioCodec === 'no'){
outputFlags.push(`-an`)
}else if(audioCodec){
outputFlags.push(`-c:a ` + audioCodec)
}
if(outputFilters.length > 0){
outputFlags.push(`-vf "${outputFilters.join(',')}"`)
}
if(videoCodec !== 'h264_vaapi' && !videoCodecisCopy){
if(!arrayContains('-tune',outputFlags)){
outputFlags.push(`-tune zerolatency`)
}
if(!arrayContains('-g ',outputFlags)){
outputFlags.push(`-g 1`)
}
}
outputFlags.push(`-f hls -live_start_index -3 -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${e.sdir}detectorStream.m3u8"`)
}
return outputFlags.join(' ')
}
const buildTimelapseOutput = function(e){
if(e.details.record_timelapse === '1'){
const videoFilters = []
const videoFlags = []
const inputMap = buildInputMap(e,e.details.input_map_choices.record_timelapse)
const { videoWidth, videoHeight } = validateDimensions(e.details.record_scale_x,e.details.record_scale_y)
if(videoWidth && videoHeight)streamFlags.push(`-s ${videoWidth}x${videoHeight}`)
if(inputMap)videoFlags.push(inputMap)
videoFilters.push(`fps=${(1 / (!isNaN(parseFloat(e.details.record_timelapse_fps)) ? parseFloat(e.details.record_timelapse_fps) : 900)).toFixed(3)}`)
if(e.details.record_timelapse_vf)videoFilters.push(e.details.record_timelapse_vf)
if(e.details.record_timelapse_watermark === "1" && e.details.record_timelapse_watermark_location){
videoFilters.push(buildWatermarkFiltersFromConfiguration('record_timelapse_',e))
}
if(videoFilters.length > 0){
videoFlags.push(`-vf "${videoFilters.join(',')}"`)
}
videoFlags.push(`-f singlejpeg -an -q:v 1 pipe:7`)
return videoFlags.join(' ')
}
return ``
}
s.ffmpeg = function(e){
const ffmpegCommand = [`-progress pipe:5`];
//set some placeholding values to avoid "undefined" in ffmpeg string.
([
buildMainInput(e),
buildMainStream(e),
buildJpegApiOutput(e),
buildMainRecording(e),
buildAudioDetector(e),
buildMainDetector(e),
buildEventRecordingOutput(e),
buildTimelapseOutput(e),
]).forEach(function(commandStringPart){
ffmpegCommand.push(commandStringPart)
})
s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){
extender(e,ffmpegCommand)
})
const stdioPipes = createPipeArray(e)
const ffmpegCommandString = ffmpegCommand.join(' ')
//hold ffmpeg command for log stream
s.group[e.ke].activeMonitors[e.mid].ffmpeg = sanitizedFfmpegCommand(e,ffmpegCommandString)
//clean the string of spatial impurities and split for spawn()
const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString)
try{
fs.unlinkSync(e.sdir + 'cmd.txt')
}catch(err){
}
fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({
cmd: ffmpegCommandParsed,
pipes: stdioPipes.length,
rawMonitorConfig: s.group[e.ke].rawMonitorConfigurations[e.id],
globalInfo: {
config: config,
isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected
}
},null,3),'utf8')
var cameraCommandParams = [
'./libs/cameraThread/singleCamera.js',
config.ffmpegDir,
e.sdir + 'cmd.txt'
]
return spawn('node',cameraCommandParams,{detached: true,stdio: stdioPipes})
}
if(!config.ffmpegDir){
checkForWindows(function(){
checkForFfbinary(function(){
checkForNpmStatic(function(){
checkForUnix(function(){
console.log('No FFmpeg found.')
})
})
})
})
}
if(downloadingFfmpeg === false){
//not downloading ffmpeg
completeCheck()
}
return ffmpeg
}