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 { buildTimestampFilters, buildWatermarkFiltersFromConfiguration, } = 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 ffmpeg.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 ffmpeg.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 ffmpeg.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 ffmpeg.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.'); ffmpeg.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 ffmpeg.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 ffmpeg.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() } ffmpeg.completeCheck = function(){ ffmpeg.checkVersion(function(){ ffmpeg.checkHwAccelMethods(function(){ s.onFFmpegLoadedExtensions.forEach(function(extender){ extender(ffmpeg) }) onFinish(ffmpeg) }) }) } //ffmpeg string cleaner, splits for use with spawn() s.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 }; const hasInputMaps = (e) => { return (e.details.input_maps && e.details.input_maps.length > 0) } s.createFFmpegMap = 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; } s.createInputMap = function(e,number,input){ //`e` is the monitor object //`x` is an object used to contain temporary values. var x = {} x.cust_input = '' x.hwaccel = '' if(input.cust_input&&input.cust_input!==''){x.cust_input+=' '+input.cust_input} //input - analyze duration if(input.aduration&&input.aduration!==''){x.cust_input+=' -analyzeduration '+input.aduration} //input - probe size if(input.probesize&&input.probesize!==''){x.cust_input+=' -probesize '+input.probesize} //input - stream loop (good for static files/lists) if(input.stream_loop === '1'){x.cust_input+=' -stream_loop -1'} //input - fps if(x.cust_input.indexOf('-r ')===-1&&input.sfps&&input.sfps!==''){ input.sfps=parseFloat(input.sfps); if(isNaN(input.sfps)){input.sfps=1} x.cust_input+=' -r '+input.sfps } //input - is mjpeg if(input.type==='mjpeg'){ if(x.cust_input.indexOf('-f ')===-1){ x.cust_input+=' -f mjpeg' } //input - frames per second x.cust_input+=' -reconnect 1' }else //input - is h264 has rtsp in address and transport method is chosen if((input.type==='h264'||input.type==='mp4')&&input.fulladdress.indexOf('rtsp://')>-1&&input.rtsp_transport!==''&&input.rtsp_transport!=='no'){ x.cust_input += ' -rtsp_transport '+input.rtsp_transport }else if((input.type==='mp4'||input.type==='mjpeg')&&x.cust_input.indexOf('-re')===-1){ x.cust_input += ' -re' } //hardware acceleration if(input.accelerator&&input.accelerator==='1'){ if(input.hwaccel&&input.hwaccel!==''){ x.hwaccel+=' -hwaccel '+input.hwaccel; } if(input.hwaccel_vcodec&&input.hwaccel_vcodec!==''&&input.hwaccel_vcodec!=='auto'&&input.hwaccel_vcodec!=='no'){ x.hwaccel+=' -c:v '+input.hwaccel_vcodec; } if(input.hwaccel_device&&input.hwaccel_device!==''){ switch(input.hwaccel){ case'vaapi': x.hwaccel+=' -vaapi_device '+input.hwaccel_device+' -hwaccel_output_format vaapi'; break; default: x.hwaccel+=' -hwaccel_device '+input.hwaccel_device; break; } } } //custom - input flags return x.hwaccel+x.cust_input+' -i "'+input.fulladdress+'"'; } //create sub stream channel s.createStreamChannel = function(e,number,channel){ //`e` is the monitor object //`x` is an object used to contain temporary values. var x = { pipe: '', cust_stream: ' -strict -2' } if(!number||number==''){ x.channel_sdir = e.sdir; }else{ x.channel_sdir = e.sdir+'channel'+number+'/'; if (!fs.existsSync(x.channel_sdir)){ fs.mkdirSync(x.channel_sdir); } } x.stream_video_filters=[] //stream - frames per second if(channel.stream_vcodec!=='copy'){ if(!channel.stream_fps||channel.stream_fps===''){ switch(channel.stream_type){ case'rtmp': channel.stream_fps=30 break; default: // channel.stream_fps=5 break; } } } if(channel.stream_fps&&channel.stream_fps!==''){x.stream_fps=' -r '+channel.stream_fps}else{x.stream_fps=''} //stream - hls vcodec if(channel.stream_vcodec&&channel.stream_vcodec!=='no'){ if(channel.stream_vcodec!==''){x.stream_vcodec=' -c:v '+channel.stream_vcodec}else{x.stream_vcodec=' -c:v libx264'} }else{ x.stream_vcodec=''; } //stream - hls acodec if(channel.stream_acodec!=='no'){ if(channel.stream_acodec&&channel.stream_acodec!==''){x.stream_acodec=' -c:a '+channel.stream_acodec}else{x.stream_acodec=''} }else{ x.stream_acodec=' -an'; } //stream - resolution if(channel.stream_scale_x&&channel.stream_scale_x!==''&&channel.stream_scale_y&&channel.stream_scale_y!==''){ x.dimensions = channel.stream_scale_x+'x'+channel.stream_scale_y; } //stream - hls segment time if(channel.hls_time&&channel.hls_time!==''){x.hls_time=channel.hls_time}else{x.hls_time="2"} //hls list size if(channel.hls_list_size&&channel.hls_list_size!==''){x.hls_list_size=channel.hls_list_size}else{x.hls_list_size=2} //stream - custom flags if(channel.cust_stream&&channel.cust_stream!==''){x.cust_stream=' '+channel.cust_stream} //stream - preset if(channel.stream_type !== 'h265' && channel.preset_stream && channel.preset_stream!==''){x.preset_stream=' -preset '+channel.preset_stream;}else{x.preset_stream=''} //hardware acceleration if(e.details.accelerator&&e.details.accelerator==='1'){ if(e.details.hwaccel === 'auto')e.details.hwaccel = '' if(e.details.hwaccel && e.details.hwaccel!==''){ x.hwaccel+=' -hwaccel '+e.details.hwaccel; } if(e.details.hwaccel_vcodec&&e.details.hwaccel_vcodec!==''){ x.hwaccel+=' -c:v '+e.details.hwaccel_vcodec; } if(e.details.hwaccel_device&&e.details.hwaccel_device!==''){ switch(e.details.hwaccel){ case'vaapi': x.hwaccel+=' -vaapi_device '+e.details.hwaccel_device+' -hwaccel_output_format vaapi'; break; default: x.hwaccel+=' -hwaccel_device '+e.details.hwaccel_device; break; } } // else{ // if(e.details.hwaccel==='vaapi'){ // x.hwaccel+=' -hwaccel_device 0'; // } // } } if(channel.rotate_stream&&channel.rotate_stream!==""&&channel.rotate_stream!=="no"){ x.stream_video_filters.push('transpose='+channel.rotate_stream); } //stream - video filter if(channel.svf&&channel.svf!==''){ x.stream_video_filters.push(channel.svf) } if(x.stream_video_filters.length>0){ var string = x.stream_video_filters.join(',').trim() if(string===''){ x.stream_video_filters='' }else{ x.stream_video_filters=' -vf '+string } }else{ x.stream_video_filters='' } x.pipe += s.createFFmpegMap(e,e.details.input_map_choices['stream_channel-'+(number-config.pipeAddition)]) if(channel.stream_vcodec !== 'copy' || channel.stream_type === 'mjpeg' || channel.stream_type === 'b64'){ x.cust_stream += x.stream_fps } switch(channel.stream_type){ case'mp4': x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream" -reset_timestamps 1' if(channel.stream_vcodec!=='copy'){ if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; x.cust_stream+=x.preset_stream x.cust_stream+=x.stream_video_filters } x.pipe+=' -f mp4'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; break; case'rtmp': x.rtmp_server_url=s.checkCorrectPathEnding(channel.rtmp_server_url); if(channel.stream_vcodec!=='copy'){ if(channel.stream_vcodec==='libx264'){ channel.stream_vcodec = 'h264' } if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; x.cust_stream+=x.preset_stream if(channel.stream_v_br&&channel.stream_v_br!==''){x.cust_stream+=' -b:v '+channel.stream_v_br} } if(channel.stream_vcodec!=='no'&&channel.stream_vcodec!==''){ x.cust_stream+=' -vcodec '+channel.stream_vcodec } if(channel.stream_acodec!=='copy'){ if(!channel.stream_acodec||channel.stream_acodec===''||channel.stream_acodec==='no'){ channel.stream_acodec = 'aac' } if(!channel.stream_a_br||channel.stream_a_br===''){channel.stream_a_br='128k'} x.cust_stream+=' -ab '+channel.stream_a_br } if(channel.stream_acodec!==''){ x.cust_stream+=' -acodec '+channel.stream_acodec } x.pipe+=' -f flv'+x.stream_video_filters+x.cust_stream+' "'+x.rtmp_server_url+channel.rtmp_stream_key+'"'; break; case'h264': if(channel.stream_vcodec!=='copy'){ if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; x.cust_stream+=x.preset_stream x.cust_stream+=x.stream_video_filters } x.pipe+=' -f mpegts'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; break; case'flv': if(channel.stream_vcodec!=='copy'){ if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; x.cust_stream+=x.preset_stream x.cust_stream+=x.stream_video_filters } x.pipe+=' -f flv'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; break; case'hls': if(channel.stream_vcodec!=='h264_vaapi'&&channel.stream_vcodec!=='copy'){ if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; if(x.cust_stream.indexOf('-tune')===-1){x.cust_stream+=' -tune zerolatency'} if(x.cust_stream.indexOf('-g ')===-1){x.cust_stream+=' -g 1'} if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} x.cust_stream+=x.stream_video_filters } x.pipe+=x.preset_stream+x.stream_acodec+x.stream_vcodec+' -f hls'+x.cust_stream+' -hls_time '+x.hls_time+' -hls_list_size '+x.hls_list_size+' -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+x.channel_sdir+'s.m3u8"'; break; case'mjpeg': if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -q:v '+channel.stream_quality; if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} x.pipe+=' -c:v mjpeg -f mpjpeg -boundary_tag shinobi'+x.cust_stream+x.stream_video_filters+' pipe:'+number; break; default: x.pipe='' break; } return x.pipe } ffmpeg.buildMainInput = function(e,x){ //e = monitor object //x = temporary values const isStreamer = inputTypeIsStreamer(e) const isCudaEnabled = hasCudaEnabled(e) // x.hwaccel = '' x.cust_input = '' //wallclock fix for strangely long, single frame videos if((config.wallClockTimestampAsDefault || e.details.wall_clock_timestamp_ignore !== '1') && e.type === 'h264' && x.cust_input.indexOf('-use_wallclock_as_timestamps 1') === -1){x.cust_input+=' -use_wallclock_as_timestamps 1';} //input - frame rate (capture rate) if(e.details.sfps && e.details.sfps!==''){x.input_fps=' -r '+e.details.sfps}else{x.input_fps=''} //input - analyze duration if(e.details.aduration&&e.details.aduration!==''){x.cust_input+=' -analyzeduration '+e.details.aduration}; //input - probe size if(e.details.probesize&&e.details.probesize!==''){x.cust_input+=' -probesize '+e.details.probesize}; //input - stream loop (good for static files/lists) if(e.details.stream_loop === '1' && (e.type === 'mp4' || e.type === 'local')){x.cust_input+=' -stream_loop -1'}; //input if(e.details.cust_input.indexOf('-fflags') === -1){x.cust_input+=' -fflags +igndts'} switch(e.type){ case'h264': switch(e.protocol){ case'rtsp': if(e.details.rtsp_transport&&e.details.rtsp_transport!==''&&e.details.rtsp_transport!=='no'){x.cust_input+=' -rtsp_transport '+e.details.rtsp_transport;} break; } break; } //hardware acceleration if(e.details.accelerator && e.details.accelerator==='1' && !isStreamer){ if(e.details.hwaccel&&e.details.hwaccel!==''){ x.hwaccel+=' -hwaccel '+e.details.hwaccel; } if(e.details.hwaccel_vcodec&&e.details.hwaccel_vcodec!==''){ x.hwaccel+=' -c:v '+e.details.hwaccel_vcodec; } if(e.details.hwaccel_device&&e.details.hwaccel_device!==''){ switch(e.details.hwaccel){ case'vaapi': x.hwaccel+=' -vaapi_device '+e.details.hwaccel_device; break; default: x.hwaccel+=' -hwaccel_device '+e.details.hwaccel_device; break; } } // else{ // if(e.details.hwaccel==='vaapi'){ // x.hwaccel+=' -hwaccel_device 0'; // } // } } //logging - level if(e.details.loglevel&&e.details.loglevel!==''){x.loglevel='-loglevel '+e.details.loglevel;}else{x.loglevel='-loglevel error'} //custom - input flags if(e.details.cust_input&&e.details.cust_input!==''){x.cust_input+=' '+e.details.cust_input;} //add main input if((e.type === 'mp4' || e.type === 'mjpeg') && x.cust_input.indexOf('-re') === -1){ x.cust_input += ' -re' } } ffmpeg.buildMainStream = function(e,x){ //e = monitor object //x = temporary values const isCudaEnabled = hasCudaEnabled(e) const streamFlags = [] const streamFilters = [] const customStreamFlags = [] 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 = s.createFFmpegMap(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 if(inputMap)streamFlags.push(inputMap) if(e.details.cust_stream)customStreamFlags.push(...e.details.cust_stream.split(' ')) // // if(e.details.stream_scale_x&&e.details.stream_scale_x!==''&&e.details.stream_scale_y&&e.details.stream_scale_y!==''){ // x.dimensions = e.details.stream_scale_x+'x'+e.details.stream_scale_y; // } if(customStreamFlags.indexOf('-strict -2') === -1)customStreamFlags.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(videoCodec !== 'no'){ streamFlags.push(`-c:v ` + videoCodec) } if(outputCanHaveAudio && audioCodec !== 'no'){ streamFlags.push(`-c:a ` + videoCodec) }else{ streamFlags.push(`-an`) } //stream - preset if(streamType !== 'h265' && e.details.preset_stream){ streamFlags.push('-preset ' + e.details.preset_stream) } 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) } }else if(isCudaEnabled && (streamType === 'mjpeg' || streamType === 'b64')){ streamFilters.push('hwdownload,format=nv12') } if(!videoCodecisCopy){ // if( // !isNaN(parseInt(e.details.stream_scale_x)) && // !isNaN(parseInt(e.details.stream_scale_y)) // ){ // streamingFlags.push(`-s ${e.details.stream_scale_x}x${e.details.stream_scale_y}`) // } if(videoFps && streamType === 'mjpeg' || streamType === 'b64'){ streamFilters.push(`fps=${videoFps}`) } } const streamPreset = streamType !== 'h265' && e.details.preset_stream ? e.details.preset_stream : null //stream - video filter if(e.details.stream_vf){ streamFilters.push(e.details.stream_vf) } if(outputIsPresetCapable){ 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; } x.pipe += ' ' + streamFlags.join(' ') if(e.details.stream_channels){ e.details.stream_channels.forEach(function(v,n){ x.pipe += s.createStreamChannel(e,n+config.pipeAddition,v) }) } //api - snapshot bin/ cgi.bin (JPEG Mode) if(e.details.snap === '1'){ x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.snap) var snapVf = e.details.snap_vf ? e.details.snap_vf.split(',') : [] if(e.details.snap_vf === '')snapVf.shift() if(e.cudaEnabled){ snapVf.push('hwdownload,format=nv12') } snapVf.push(`fps=${e.details.snap_fps || '1'}`) //-vf "thumbnail_cuda=2,hwdownload,format=nv12" x.pipe += ` -vf "${snapVf.join(',')}"` if(e.details.snap_scale_x && e.details.snap_scale_x !== '' && e.details.snap_scale_y && e.details.snap_scale_y !== '')x.pipe += ' -s '+e.details.snap_scale_x+'x'+e.details.snap_scale_y if(e.details.cust_snap)x.pipe += ' ' + e.details.cust_snap x.pipe += ` -update 1 "${e.sdir}s.jpg" -y` } //custom - output if(e.details.custom_output&&e.details.custom_output!==''){x.pipe+=' '+e.details.custom_output;} } ffmpeg.buildMainRecording = function(e,x){ //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 = s.createFFmpegMap(e,e.details.input_map_choices.record) if(inputMap)recordingFlags.push(inputMap) if(e.details.cust_record)customRecordingFlags.push(...e.details.cust_record.split(' ')) //record - resolution if(customRecordingFlags.indexOf('-strict -2') === -1)customRecordingFlags.push(`-strict -2`) // if(customRecordingFlags.indexOf('-threads') === -1)customRecordingFlags.push(`-threads 10`) if(!videoCodecisCopy){ if( !isNaN(parseInt(e.details.record_scale_x)) && !isNaN(parseInt(e.details.record_scale_y)) ){ recordingFlags.push(`-s ${e.details.record_scale_x}x${e.details.record_scale_y}`) } 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(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'}"`); x.pipe += ' ' + recordingFlags.join(' ') } } ffmpeg.buildAudioDetector = function(e,x){ if(e.details.detector_audio === '1'){ if(e.details.input_map_choices&&e.details.input_map_choices.detector_audio){ //add input feed map x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector_audio) }else{ x.pipe += ' -map 0:a' } x.pipe += ' -acodec pcm_s16le -f s16le -ac 1 -ar 16000 pipe:6' } } ffmpeg.buildMainDetector = function(e,x){ //e = monitor object //x = temporary values 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 isCudaEnabled = false || e.cudaEnabled 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(s.createFFmpegMap(e,e.details.input_map_choices.detector)) } const addObjectDetectorInputMap = () => { detectorFlags.push(s.createFFmpegMap(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') } x.pipe += ' ' + detectorFlags.join(' ') } //Traditional Recording Buffer if(e.details.detector === '1' && e.details.detector_trigger === '1' && e.details.detector_record_method === 'sip'){ if(e.details.cust_sip_record && e.details.cust_sip_record !== ''){x.pipe += ' ' + e.details.cust_sip_record} x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector_sip_buffer || e.details.input_map_choices.detector) x.detector_buffer_filters=[] if(!e.details.detector_buffer_vcodec||e.details.detector_buffer_vcodec===''||e.details.detector_buffer_vcodec==='auto'){ switch(e.type){ case'h264':case'hls':case'mp4': e.details.detector_buffer_vcodec = 'copy' break; default: if(e.details.accelerator === '1' && e.cudaEnabled){ e.details.detector_buffer_vcodec = 'h264_nvenc' }else{ e.details.detector_buffer_vcodec = 'libx264' } break; } } if(!e.details.detector_buffer_acodec||e.details.detector_buffer_acodec===''||e.details.detector_buffer_acodec==='auto'){ switch(e.type){ case'mjpeg':case'jpeg':case'socket': e.details.detector_buffer_acodec = 'no' break; case'h264':case'hls':case'mp4': e.details.detector_buffer_acodec = 'copy' break; default: e.details.detector_buffer_acodec = 'aac' break; } } if(e.details.detector_buffer_acodec === 'no'){ x.detector_buffer_acodec = ' -an' }else{ x.detector_buffer_acodec = ' -c:a '+e.details.detector_buffer_acodec } if(!e.details.detector_buffer_tune||e.details.detector_buffer_tune===''){e.details.detector_buffer_tune='zerolatency'} if(!e.details.detector_buffer_g||e.details.detector_buffer_g===''){e.details.detector_buffer_g='1'} if(!e.details.detector_buffer_hls_time||e.details.detector_buffer_hls_time===''){e.details.detector_buffer_hls_time='2'} if(!e.details.detector_buffer_hls_list_size||e.details.detector_buffer_hls_list_size===''){e.details.detector_buffer_hls_list_size='4'} if(!e.details.detector_buffer_start_number||e.details.detector_buffer_start_number===''){e.details.detector_buffer_start_number='0'} if(!e.details.detector_buffer_live_start_index||e.details.detector_buffer_live_start_index===''){e.details.detector_buffer_live_start_index='-3'} if(e.details.detector_buffer_vcodec.indexOf('_vaapi')>-1){ if(x.hwaccel.indexOf('-vaapi_device')>-1){ x.detector_buffer_filters.push('format=nv12') x.detector_buffer_filters.push('hwupload') }else{ e.details.detector_buffer_vcodec='libx264' } } if(e.details.detector_buffer_vcodec!=='copy'){ if(e.details.detector_buffer_fps&&e.details.detector_buffer_fps!==''){ x.detector_buffer_fps=' -r '+e.details.detector_buffer_fps }else{ x.detector_buffer_fps=' -r 30' } }else{ x.detector_buffer_fps='' } if(x.detector_buffer_filters.length>0){ x.pipe+=' -vf '+x.detector_buffer_filters.join(',') } x.pipe += x.detector_buffer_fps+x.detector_buffer_acodec+' -c:v '+e.details.detector_buffer_vcodec+' -f hls -tune '+e.details.detector_buffer_tune+' -g '+e.details.detector_buffer_g+' -hls_time '+e.details.detector_buffer_hls_time+' -hls_list_size '+e.details.detector_buffer_hls_list_size+' -start_number '+e.details.detector_buffer_start_number+' -live_start_index '+e.details.detector_buffer_live_start_index+' -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'detectorStream.m3u8"' } } ffmpeg.buildTimelapseOutput = function(e,x){ if(e.details.record_timelapse === '1'){ var recordTimelapseVideoFilters = [] var flags = [] x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.record_timelapse) recordTimelapseVideoFilters.push('fps=1/' + (e.details.record_timelapse_fps ? e.details.record_timelapse_fps : '900')) if(e.details.record_timelapse_vf && e.details.record_timelapse_vf !== '')flags.push('-vf ' + e.details.record_timelapse_vf) if(e.details.record_timelapse_scale_x && e.details.record_timelapse_scale_x !== '' && e.details.record_timelapse_scale_y && e.details.record_timelapse_scale_y !== '')flags.push(`-s ${e.details.record_timelapse_scale_x}x${e.details.record_timelapse_scale_y}`) //record - watermark for -vf if(e.details.record_timelapse_watermark&&e.details.record_timelapse_watermark=="1"&&e.details.record_timelapse_watermark_location&&e.details.record_timelapse_watermark_location!==''){ switch(e.details.record_timelapse_watermark_position){ case'tl'://top left x.record_timelapse_watermark_position = '10:10' break; case'tr'://top right x.record_timelapse_watermark_position = 'main_w-overlay_w-10:10' break; case'bl'://bottom left x.record_timelapse_watermark_position = '10:main_h-overlay_h-10' break; default://bottom right x.record_timelapse_watermark_position = '(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' break; } recordTimelapseVideoFilters.push( 'movie=' + e.details.record_timelapse_watermark_location, `[watermark],[in][watermark]overlay=${x.record_timelapse_watermark_position}[out]` ) } if(recordTimelapseVideoFilters.length > 0){ var videoFilter = `-vf "${recordTimelapseVideoFilters.join(',').trim()}"` flags.push(videoFilter) } x.pipe += ` -f singlejpeg ${flags.join(' ')} -an -q:v 1 pipe:7` } } ffmpeg.assembleMainPieces = function(e,x){ //create executeable FFMPEG command x.ffmpegCommandString = x.loglevel+x.input_fps; //progress pipe x.ffmpegCommandString += ' -progress pipe:5'; const url = s.buildMonitorUrl(e); switch(e.type){ case'dashcam': x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i -'; break; case'socket':case'jpeg':case'pipe'://case'webpage': x.ffmpegCommandString += ' -pattern_type glob -f image2pipe'+x.record_fps+' -vcodec mjpeg'+x.cust_input+x.hwaccel+' -i -'; break; case'mjpeg': x.ffmpegCommandString += ' -reconnect 1 -f mjpeg'+x.cust_input+x.hwaccel+' -i "'+url+'"'; break; case'mxpeg': x.ffmpegCommandString += ' -reconnect 1 -f mxg'+x.cust_input+x.hwaccel+' -i "'+url+'"'; break; case'rtmp': if(!e.details.rtmp_key)e.details.rtmp_key = '' x.ffmpegCommandString += x.cust_input+x.hwaccel+` -i "rtmp://127.0.0.1:1935/${e.ke + '_' + e.mid + '_' + e.details.rtmp_key}"`; break; case'h264':case'hls':case'mp4': x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+url+'"'; break; case'local': x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+e.path+'"'; break; } //add extra input maps if(e.details.input_maps){ e.details.input_maps.forEach(function(v,n){ x.ffmpegCommandString += s.createInputMap(e,n+1,v) }) } //add recording and stream outputs x.ffmpegCommandString += x.pipe } ffmpeg.createPipeArray = function(e,x){ //create additional pipes from ffmpeg x.stdioPipes = []; var times = config.pipeAddition; if(e.details.stream_channels){ times+=e.details.stream_channels.length } for(var i=0; i < times; i++){ x.stdioPipes.push('pipe') } } s.ffmpeg = function(e){ //set X for temporary values so we don't break our main monitor object. var x = {tmp : ''} //set some placeholding values to avoid "undefined" in ffmpeg string. ffmpeg.buildMainInput(e,x) ffmpeg.buildMainStream(e,x) ffmpeg.buildMainRecording(e,x) ffmpeg.buildAudioDetector(e,x) ffmpeg.buildMainDetector(e,x) ffmpeg.buildTimelapseOutput(e,x) s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){ extender(e,x) }) ffmpeg.assembleMainPieces(e,x) ffmpeg.createPipeArray(e,x) //hold ffmpeg command for log stream var sanitizedCmd = x.ffmpegCommandString if(e.details.muser && e.details.mpass){ sanitizedCmd = sanitizedCmd .replace(`//${e.details.muser}:${e.details.mpass}@`,'//') .replace(`=${e.details.muser}`,'=USERNAME') .replace(`=${e.details.mpass}`,'=PASSWORD') }else if(e.details.muser){ sanitizedCmd = sanitizedCmd.replace(`//${e.details.muser}:@`,'//').replace(`=${e.details.muser}`,'=USERNAME') } s.group[e.ke].activeMonitors[e.mid].ffmpeg = sanitizedCmd //clean the string of spatial impurities and split for spawn() x.ffmpegCommandString = s.splitForFFPMEG(x.ffmpegCommandString) //launch that bad boy // return spawn(config.ffmpegDir,x.ffmpegCommandString,{detached: true,stdio:x.stdioPipes}) try{ fs.unlinkSync(e.sdir + 'cmd.txt') }catch(err){ } fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({ cmd: x.ffmpegCommandString, pipes: x.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:x.stdioPipes}) } if(!config.ffmpegDir){ ffmpeg.checkForWindows(function(){ ffmpeg.checkForFfbinary(function(){ ffmpeg.checkForNpmStatic(function(){ ffmpeg.checkForUnix(function(){ console.log('No FFmpeg found.') }) }) }) }) } if(downloadingFfmpeg === false){ //not downloading ffmpeg ffmpeg.completeCheck() } return ffmpeg }