diff --git a/libs/ffmpeg.js b/libs/ffmpeg.js index bb16c1af..fe8c9989 100644 --- a/libs/ffmpeg.js +++ b/libs/ffmpeg.js @@ -1,10 +1,23 @@ -var fs = require('fs'); -var spawn = require('child_process').spawn; -var execSync = require('child_process').execSync; +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')) { @@ -408,11 +421,8 @@ module.exports = function(s,config,lang,onFinish){ ffmpeg.buildMainInput = function(e,x){ //e = monitor object //x = temporary values - //check if CUDA is enabled - e.isStreamer = (e.type === 'dashcam'|| e.type === 'socket') - if(e.details.accelerator === '1' && e.details.hwaccel === 'cuvid' && e.details.hwaccel_vcodec === ('h264_cuvid' || 'hevc_cuvid' || 'mjpeg_cuvid' || 'mpeg4_cuvid')){ - e.cudaEnabled = true - } + const isStreamer = inputTypeIsStreamer(e) + const isCudaEnabled = hasCudaEnabled(e) // x.hwaccel = '' x.cust_input = '' @@ -438,7 +448,7 @@ module.exports = function(s,config,lang,onFinish){ break; } //hardware acceleration - if(e.details.accelerator && e.details.accelerator==='1' && e.isStreamer === false){ + if(e.details.accelerator && e.details.accelerator==='1' && !isStreamer){ if(e.details.hwaccel&&e.details.hwaccel!==''){ x.hwaccel+=' -hwaccel '+e.details.hwaccel; } @@ -473,150 +483,119 @@ module.exports = function(s,config,lang,onFinish){ ffmpeg.buildMainStream = function(e,x){ //e = monitor object //x = temporary values - x.stream_video_filters = [] - x.pipe = '' - x.cust_stream = ' -strict -2' + 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&&e.details.stream_timestamp=="1"&&e.details.vcodec!=='copy'){ - //font - if(e.details.stream_timestamp_font&&e.details.stream_timestamp_font!==''){x.stream_timestamp_font=e.details.stream_timestamp_font}else{x.stream_timestamp_font='/usr/share/fonts/truetype/freefont/FreeSans.ttf'} - //position x - if(e.details.stream_timestamp_x&&e.details.stream_timestamp_x!==''){x.stream_timestamp_x=e.details.stream_timestamp_x}else{x.stream_timestamp_x='(w-tw)/2'} - //position y - if(e.details.stream_timestamp_y&&e.details.stream_timestamp_y!==''){x.stream_timestamp_y=e.details.stream_timestamp_y}else{x.stream_timestamp_y='0'} - //text color - if(e.details.stream_timestamp_color&&e.details.stream_timestamp_color!==''){x.stream_timestamp_color=e.details.stream_timestamp_color}else{x.stream_timestamp_color='white'} - //box color - if(e.details.stream_timestamp_box_color&&e.details.stream_timestamp_box_color!==''){x.stream_timestamp_box_color=e.details.stream_timestamp_box_color}else{x.stream_timestamp_box_color='0x00000000@1'} - //text size - if(e.details.stream_timestamp_font_size&&e.details.stream_timestamp_font_size!==''){x.stream_timestamp_font_size=e.details.stream_timestamp_font_size}else{x.stream_timestamp_font_size='10'} - x.stream_video_filters.push('drawtext="fontfile='+x.stream_timestamp_font+':text=\'%{localtime}\':x='+x.stream_timestamp_x+':y='+x.stream_timestamp_y+':fontcolor='+x.stream_timestamp_color+':box=1:boxcolor='+x.stream_timestamp_box_color+':fontsize='+x.stream_timestamp_font_size + '"'); + if(e.details.stream_timestamp === "1" && !videoCodecisCopy){ + streamFilters.push(buildTimestampFiltersFromConfiguration('stream_',e)) } - //stream - watermark for -vf - if(e.details.stream_watermark&&e.details.stream_watermark=="1"&&e.details.stream_watermark_location&&e.details.stream_watermark_location!==''){ - switch(e.details.stream_watermark_position){ - case'tl'://top left - x.stream_watermark_position='10:10' - break; - case'tr'://top right - x.stream_watermark_position='main_w-overlay_w-10:10' - break; - case'bl'://bottom left - x.stream_watermark_position='10:main_h-overlay_h-10' - break; - default://bottom right - x.stream_watermark_position='(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' - break; - } - x.stream_video_filters.push('movie='+e.details.stream_watermark_location+'[watermark],[in][watermark]overlay='+x.stream_watermark_position+'[out]'); + if(e.details.stream_watermark === "1" && e.details.stream_watermark_location){ + streamFilters.push(buildWatermarkFiltersFromConfiguration(`stream_`,e)) } //stream - rotation - if(e.details.rotate_stream&&e.details.rotate_stream!==""&&e.details.rotate_stream!=="no"&&e.details.stream_vcodec!=='copy'){ - x.stream_video_filters.push('transpose='+e.details.rotate_stream); + if(e.details.stream_rotate && e.details.stream_rotate !== "no" && e.details.stream_vcodec !== 'copy'){ + streamFilters.push(buildRotationFiltersFromConfiguration(`stream_`,e)) } - //stream - hls vcodec - if(e.details.stream_vcodec&&e.details.stream_vcodec!=='no'){ - if(e.details.stream_vcodec!==''){x.stream_vcodec=' -c:v '+e.details.stream_vcodec}else{x.stream_vcodec=' -c:v libx264'} + if(videoCodec !== 'no'){ + streamFlags.push(`-c:v ` + videoCodec) + } + if(outputCanHaveAudio && audioCodec !== 'no'){ + streamFlags.push(`-c:a ` + videoCodec) }else{ - x.stream_vcodec=''; + streamFlags.push(`-an`) } - //stream - hls acodec - if(e.details.stream_acodec!=='no'){ - if(e.details.stream_acodec&&e.details.stream_acodec!==''){x.stream_acodec=' -c:a '+e.details.stream_acodec}else{x.stream_acodec=''} - }else{ - x.stream_acodec=' -an'; - } - //stream - hls segment time - if(e.details.hls_time&&e.details.hls_time!==''){x.hls_time=e.details.hls_time}else{x.hls_time="2"} //hls list size - if(e.details.hls_list_size&&e.details.hls_list_size!==''){x.hls_list_size=e.details.hls_list_size}else{x.hls_list_size=2} - //stream - custom flags - if(e.details.cust_stream&&e.details.cust_stream!==''){x.cust_stream=' '+e.details.cust_stream} //stream - preset - if(e.details.stream_type !== 'h265' && e.details.preset_stream && e.details.preset_stream !== ''){x.preset_stream=' -preset '+e.details.preset_stream;}else{x.preset_stream=''} + if(streamType !== 'h265' && e.details.preset_stream){ + streamFlags.push('-preset ' + e.details.preset_stream) + } - if(e.details.stream_vcodec==='h264_vaapi'){ - x.stream_video_filters=[] - x.stream_video_filters.push('format=nv12,hwupload'); - if(e.details.stream_scale_x&&e.details.stream_scale_x!==''&&e.details.stream_scale_y&&e.details.stream_scale_y!==''){ - x.stream_video_filters.push('scale_vaapi=w='+e.details.stream_scale_x+':h='+e.details.stream_scale_y) + 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(e.cudaEnabled && (e.details.stream_type === 'mjpeg' || e.details.stream_type === 'b64')){ - x.stream_video_filters.push('hwdownload,format=nv12') + }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.svf && e.details.svf !== ''){ - x.stream_video_filters.push(e.details.svf) + if(e.details.stream_vf){ + streamFilters.push(e.details.stream_vf) } - if(x.stream_video_filters.length>0){ - x.stream_video_filters=' -vf "'+x.stream_video_filters.join(',')+'"' + if(outputIsPresetCapable){ + if(streamPreset){ + streamFlags.push(`-preset ${streamPreset}`) + } + if(!videoCodecisCopy){ + streamFlags.push(`-crf ${videoQuality}`) + } }else{ - x.stream_video_filters='' + streamFlags.push(`-q:v ${videoQuality}`) } - //stream - pipe build - x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.stream) - if(e.details.stream_fps&&e.details.stream_fps!==''){x.stream_fps=' -r '+e.details.stream_fps}else{x.stream_fps = ''} - if(x.stream_fps && (e.details.stream_vcodec !== 'copy' || e.details.stream_type === 'mjpeg' || e.details.stream_type === 'b64')){ - x.cust_stream += x.stream_fps + if((!videoCodecisCopy || outputRequiresEncoding) && streamFilters.length > 0){ + streamFlags.push(`-vf ${streamFilters.join(',')}`) } - switch(e.details.stream_type){ + switch(streamType){ case'mp4': - x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream" -reset_timestamps 1' - if(e.details.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.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:1'; + 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': - if(e.details.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.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:1'; + streamFlags.push(`-f flv`,'pipe:1') break; case'hls': - if(e.details.stream_vcodec!=='h264_vaapi'&&e.details.stream_vcodec!=='copy'){ - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.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 + 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`) + } } - 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 "'+e.sdir+'s.m3u8"'; + 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': - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -q:v '+e.details.stream_quality; - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.pipe+=' -an -c:v mjpeg -f mpjpeg -boundary_tag shinobi'+x.cust_stream+x.stream_video_filters+' pipe:1'; + streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:1`) break; case'h265': - x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1' - if(e.details.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; - x.cust_stream+=x.preset_stream - x.cust_stream+=x.stream_video_filters - } - x.pipe+=' -f hevc'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:1'; + 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 - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -q:v '+e.details.stream_quality; - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.pipe+=' -an -c:v mjpeg -f image2pipe'+x.cust_stream+x.stream_video_filters+' pipe:1'; - break; - default: - x.pipe='' + 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) @@ -698,34 +677,14 @@ module.exports = function(s,config,lang,onFinish){ } //record - timestamp options for -vf if(e.details.timestamp === "1" && !videoCodecisCopy){ - const timestampFont = e.details.timestamp_font ? e.details.timestamp_font : '/usr/share/fonts/truetype/freefont/FreeSans.ttf' - const timestampX = e.details.timestamp_x ? e.details.timestamp_x : '(w-tw)/2' - const timestampY = e.details.timestamp_y ? e.details.timestamp_y : '0' - const timestampColor = e.details.timestamp_color ? e.details.timestamp_color : 'white' - const timestampBackgroundColor = e.details.timestamp_box_color ? e.details.timestamp_box_color : '0x00000000@1' - const timestampFontSize = e.details.timestamp_font_size ? e.details.timestamp_font_size : '10' - recordingFilters.push(`'drawtext=fontfile=${timestampFont}:text=\'%{localtime}\':x=${timestampX}:y=${timestampY}:fontcolor=${timestampColor}:box=1:boxcolor=${timestampBackgroundColor}:fontsize=${timestampFontSize}`); + recordingFilters.push(buildTimestampFiltersFromConfiguration('',e)) } //record - watermark for -vf if(e.details.watermark === "1" && e.details.watermark_location){ - const watermarkLocation = e.details.watermark_location - //bottom right is default - var watermarkPosition = '(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' - switch(e.details.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; - } - recordingFilters.push(`movie=${watermarkLocation}[watermark],[in][watermark]overlay=${watermarkPosition}[out]`); + recordingFilters.push(buildWatermarkFiltersFromConfiguration('',e)) } - if(e.details.rotate_record && e.details.rotate_record !== "no" && !videoCodecisCopy){ - recordingFilters.push('transpose=' + e.details.rotate_record); + if(e.details.rotate && e.details.rotate !== "no" && !videoCodecisCopy){ + recordingFilters.push(buildRotationFiltersFromConfiguration(``,e)) } if(e.details.vf){ recordingFilters.push(e.details.vf) diff --git a/libs/ffmpeg/utils.js b/libs/ffmpeg/utils.js index 00f6be35..f79291e2 100644 --- a/libs/ffmpeg/utils.js +++ b/libs/ffmpeg/utils.js @@ -127,6 +127,48 @@ module.exports = (s,config,lang) => { } }) } + const buildWatermarkFiltersFromConfiguration = (prefix,monitor) => { + prefix = prefix ? prefix : '' + const watermarkLocation = monitor.details[`${prefix}watermark_location`] + //bottom right is default + var watermarkPosition = '(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' + switch(monitor.details[`${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) => { + prefix = prefix ? prefix : '' + const userChoice = monitor.details[`${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) => { + prefix = prefix ? prefix : '' + const timestampFont = monitor.details[`${prefix}timestamp_font`] ? monitor.details[`${prefix}timestamp_font`] : '/usr/share/fonts/truetype/freefont/FreeSans.ttf' + const timestampX = monitor.details[`${prefix}timestamp_x`] ? monitor.details[`${prefix}timestamp_x`] : '(w-tw)/2' + const timestampY = monitor.details[`${prefix}timestamp_y`] ? monitor.details[`${prefix}timestamp_y`] : '0' + const timestampColor = monitor.details[`${prefix}timestamp_color`] ? monitor.details[`${prefix}timestamp_color`] : 'white' + const timestampBackgroundColor = monitor.details[`${prefix}timestamp_box_color`] ? monitor.details[`${prefix}timestamp_box_color`] : '0x00000000@1' + const timestampFontSize = monitor.details[`${prefix}timestamp_font_size`] ? monitor.details[`${prefix}timestamp_font_size`] : '10' + return `'drawtext=fontfile=${timestampFont}:text=\'%{localtime}\':x=${timestampX}:y=${timestampY}:fontcolor=${timestampColor}:box=1:boxcolor=${timestampBackgroundColor}:fontsize=${timestampFontSize}` + } return { ffprobe: runFFprobe, probeMonitor: probeMonitor, @@ -134,6 +176,8 @@ module.exports = (s,config,lang) => { createWarningsForConfiguration: createWarningsForConfiguration, buildMonitorConfigPartialFromWarnings: buildMonitorConfigPartialFromWarnings, applyPartialToConfiguration: applyPartialToConfiguration, - repairConfiguration: repairConfiguration + repairConfiguration: repairConfiguration, + buildTimestampFiltersFromConfiguration: buildTimestampFiltersFromConfiguration, + buildWatermarkFiltersFromConfiguration: buildWatermarkFiltersFromConfiguration } } diff --git a/libs/monitor.js b/libs/monitor.js index 2f1ebb52..c862c937 100644 --- a/libs/monitor.js +++ b/libs/monitor.js @@ -19,7 +19,10 @@ module.exports = function(s,config,lang){ createWarningsForConfiguration, buildMonitorConfigPartialFromWarnings, } = require('./ffmpeg/utils.js')(s,config,lang) - const { cameraDestroy } = require('./monitor/utils.js')(s,config,lang) + const { + cameraDestroy, + monitorConfigurationMigrator, + } = require('./monitor/utils.js')(s,config,lang) const { setPresetForCurrentPosition } = require('./control/ptz.js')(s,config,lang) @@ -1562,13 +1565,14 @@ module.exports = function(s,config,lang){ ke: e.ke, mid: e.id, },2,null,true) - },2000) + },2000) } s.onMonitorStopExtensions.forEach(function(extender){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) }) break; case'start':case'record'://watch or record monitor url + monitorConfigurationMigrator(e) s.initiateMonitorObject({ke:e.ke,mid:e.id}) const activeMonitor = s.group[e.ke].activeMonitors[e.id] if(!s.group[e.ke].rawMonitorConfigurations[e.id]){s.group[e.ke].rawMonitorConfigurations[e.id]=s.cleanMonitorObject(e);}