845 lines
42 KiB
JavaScript
845 lines
42 KiB
JavaScript
const fs = require('fs');
|
|
const exec = require('child_process').exec;
|
|
const spawn = require('child_process').spawn;
|
|
const treekill = require('tree-kill');
|
|
const {
|
|
arrayContains,
|
|
} = require('../common.js')
|
|
module.exports = (s,config,lang) => {
|
|
const {
|
|
validateDimensions,
|
|
} = require('./utils.js')(s,config,lang)
|
|
const {
|
|
parseJSON,
|
|
} = require('../basic/utils.js')(process.cwd(),config)
|
|
if(!config.outputsWithAudio)config.outputsWithAudio = ['hls','flv','mp4','rtmp'];
|
|
if(!config.outputsNotCapableOfPresets)config.outputsNotCapableOfPresets = [];
|
|
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'
|
|
}
|
|
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'
|
|
if(e.details.primary_input && e.details.primary_input !== ''){
|
|
var primaryMap = e.details.primary_input || '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 createInputMap = (e, number, input) => {
|
|
// inputs, input map
|
|
//`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 || s.getStreamsDirectory(e)}channel${number}/` : e.sdir
|
|
if(channelStreamDirectory !== e.sdir && !fs.existsSync(channelStreamDirectory)){
|
|
try{
|
|
fs.mkdirSync(channelStreamDirectory)
|
|
}catch(err){
|
|
// s.debugLog(err)
|
|
}
|
|
}
|
|
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 = config.outputsWithAudio.indexOf(streamType) > -1;
|
|
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 = config.outputsNotCapableOfPresets.indexOf(streamType) === -1 && channel.preset_stream ? channel.preset_stream : null
|
|
if(streamPreset){
|
|
streamFlags.push(`-preset ${streamPreset}`)
|
|
}
|
|
if(!videoCodecisCopy){
|
|
streamFlags.push(`-q:v ${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+discont_start "${channelStreamDirectory}s.m3u8"`)
|
|
break;
|
|
case'mjpeg':
|
|
streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:${number}`)
|
|
break;
|
|
case'b64':case'':case undefined:case null://base64
|
|
streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:${number}`)
|
|
break;
|
|
}
|
|
s.onFfmpegBuildStreamChannelExtensions.forEach(function(extender){
|
|
extender(streamType,streamFlags,number,e)
|
|
});
|
|
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 = e.details.wall_clock_timestamp_ignore !== '1' || config.wallClockTimestampAsDefault && !e.details.wall_clock_timestamp_ignore
|
|
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'
|
|
const inputMaps = s.parseJSON(e.details.input_maps) || []
|
|
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(inputMaps){
|
|
inputMaps.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 streamFlags = []
|
|
const streamType = e.details.stream_type ? e.details.stream_type : 'hls'
|
|
if(streamType !== 'jpeg' && streamType !== 'useSubstream'){
|
|
const isCudaEnabled = hasCudaEnabled(e)
|
|
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 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 = config.outputsWithAudio.indexOf(streamType) > -1;
|
|
const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64'
|
|
const outputIsPresetCapable = outputCanHaveAudio
|
|
const streamChannels = s.parseJSON(e.details.stream_channels) || []
|
|
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 = config.outputsNotCapableOfPresets.indexOf(streamType) === -1 && e.details.preset_stream ? e.details.preset_stream : null
|
|
if(streamPreset){
|
|
streamFlags.push(`-preset ${streamPreset}`)
|
|
}
|
|
if(!videoCodecisCopy){
|
|
streamFlags.push(`-q:v ${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+discont_start "${e.sdir}s.m3u8"`)
|
|
break;
|
|
case'mjpeg':
|
|
streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:1`)
|
|
break;
|
|
case'b64':case'':case undefined:case null://base64
|
|
streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:1`)
|
|
break;
|
|
}
|
|
s.onFfmpegBuildMainStreamExtensions.forEach(function(extender){
|
|
extender(streamType,streamFlags,e)
|
|
});
|
|
if(e.details.custom_output){
|
|
streamFlags.push(e.details.custom_output)
|
|
}
|
|
if(streamChannels){
|
|
streamChannels.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)videoFlags.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(`-movflags +faststart`)
|
|
}
|
|
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 = []
|
|
let addedVideoFilters = false
|
|
if(e.details.detector === '1' && (sendFramesGlobally || sendFramesToObjectDetector)){
|
|
const addVideoFilters = () => {
|
|
if(addedVideoFilters)return;
|
|
addedVideoFilters = true
|
|
if(videoFilters.length > 0)detectorFlags.push(' -vf "' + videoFilters.join(',') + '"');
|
|
detectorFlags.push(baseDimensionsFlag)
|
|
}
|
|
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();
|
|
if(isCudaEnabled)videoFilters.push(cudaVideoFilters);
|
|
videoFilters.push(baseFpsFilter)
|
|
if(e.details.cust_detect)detectorFlags.push(e.details.cust_detect)
|
|
if(!objectDetectorOutputIsEnabled && !sendFramesToObjectDetector){
|
|
addVideoFilters()
|
|
}
|
|
if(builtInMotionDetectorIsEnabled){
|
|
addVideoFilters()
|
|
detectorFlags.push('-an -c:v pam -pix_fmt gray -f image2pipe pipe:3')
|
|
if(objectDetectorOutputIsEnabled){
|
|
addObjectDetectorInputMap()
|
|
addObjectDetectValues()
|
|
detectorFlags.push('-an -f mjpeg pipe:4')
|
|
}
|
|
}else if(sendFramesToObjectDetector){
|
|
addObjectDetectorInputMap()
|
|
addObjectDetectValues()
|
|
detectorFlags.push('-an -f mjpeg pipe:4')
|
|
}else{
|
|
addInputMap()
|
|
detectorFlags.push('-an -f mjpeg pipe:4')
|
|
}
|
|
}else if(sendFramesToObjectDetector){
|
|
addObjectDetectorInputMap()
|
|
addObjectDetectValues()
|
|
detectorFlags.push('-an -f mjpeg 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 liveStartIndex = e.details.detector_buffer_live_start_index || '-3'
|
|
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.event_record_scale_x,e.details.event_record_scale_y)
|
|
const hlsTime = !isNaN(parseInt(e.details.detector_buffer_hls_time)) ? `${parseInt(e.details.detector_buffer_hls_time)}` : '2'
|
|
// const hlsListSize = !isNaN(parseInt(e.details.detector_buffer_hls_list_size)) ? `${parseInt(e.details.detector_buffer_hls_list_size)}` : '4'
|
|
const secondsBefore = parseInt(e.details.detector_buffer_seconds_before) || 5
|
|
let hlsListSize = parseInt(secondsBefore / 2 + 3)
|
|
// hlsListSize = hlsListSize < 5 ? 5 : hlsListSize;
|
|
if(inputMap)outputFlags.push(inputMap)
|
|
if(e.details.cust_sip_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'){
|
|
audioCodec = `no`
|
|
}else if(e.type === 'h264' || e.type === 'hls' || e.type === 'mp4'){
|
|
audioCodec = 'copy'
|
|
}else{
|
|
audioCodec = '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 && audioCodec !== 'auto'){
|
|
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+discont_start "${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_timelapse_scale_x,e.details.record_timelapse_scale_y)
|
|
const creationFps = (1 / (!isNaN(parseFloat(e.details.record_timelapse_fps)) ? parseFloat(e.details.record_timelapse_fps) : 900)).toFixed(3);
|
|
if(videoWidth && videoHeight)videoFlags.push(`-s ${videoWidth}x${videoHeight}`)
|
|
if(inputMap)videoFlags.push(inputMap)
|
|
videoFilters.push(`fps=${creationFps}`)
|
|
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 mjpeg -an -q:v 1 pipe:7`)
|
|
return videoFlags.join(' ')
|
|
}
|
|
return ``
|
|
}
|
|
const getDefaultSubstreamFields = function(monitor){
|
|
const subStreamFields = parseJSON(monitor.details.substream || {input:{},output:{}})
|
|
const inputAndConnectionFields = Object.assign({
|
|
"type":"h264",
|
|
"fulladdress":"",
|
|
"sfps":"",
|
|
"aduration":"",
|
|
"probesize":"",
|
|
"stream_loop":"0",
|
|
"rtsp_transport":"",
|
|
"accelerator":"0",
|
|
"hwaccel":"",
|
|
"hwaccel_vcodec":"auto",
|
|
"hwaccel_device":"",
|
|
"cust_input":""
|
|
},subStreamFields.input);
|
|
const outputFields = Object.assign({
|
|
"stream_type":"hls",
|
|
"rtmp_server_url":"",
|
|
"rtmp_stream_key":"",
|
|
"stream_mjpeg_clients":"",
|
|
"stream_vcodec":"copy",
|
|
"stream_acodec":"no",
|
|
"stream_fps":"",
|
|
"hls_time":"",
|
|
"preset_stream":"",
|
|
"hls_list_size":"",
|
|
"stream_quality":"",
|
|
"stream_v_br":"",
|
|
"stream_a_br":"",
|
|
"stream_scale_x":"",
|
|
"stream_scale_y":"",
|
|
"rotate_stream":"no",
|
|
"svf":"",
|
|
"cust_stream":""
|
|
},subStreamFields.output);
|
|
return {
|
|
inputAndConnectionFields,
|
|
outputFields,
|
|
}
|
|
}
|
|
const buildSubstreamString = function(channelNumber,monitor){
|
|
let ffmpegParts = []
|
|
const {
|
|
inputAndConnectionFields,
|
|
outputFields,
|
|
} = getDefaultSubstreamFields(monitor)
|
|
ffmpegParts.push(`-loglevel ${monitor.details.loglevel || 'warning'}`)
|
|
ffmpegParts.push(createInputMap(monitor,channelNumber,inputAndConnectionFields))
|
|
ffmpegParts.push(createStreamChannel(monitor,channelNumber,outputFields))
|
|
return ffmpegParts.join(' ')
|
|
}
|
|
return {
|
|
createStreamChannel: createStreamChannel,
|
|
buildMainInput: buildMainInput,
|
|
buildMainStream: buildMainStream,
|
|
buildJpegApiOutput: buildJpegApiOutput,
|
|
buildMainRecording: buildMainRecording,
|
|
buildAudioDetector: buildAudioDetector,
|
|
buildMainDetector: buildMainDetector,
|
|
buildEventRecordingOutput: buildEventRecordingOutput,
|
|
buildTimelapseOutput: buildTimelapseOutput,
|
|
getDefaultSubstreamFields: getDefaultSubstreamFields,
|
|
buildSubstreamString: buildSubstreamString,
|
|
}
|
|
}
|