framework for Monitor Configuration Warnings and Automatic Repair

fix-non-showing-inputs
Moe Alam 2020-10-25 22:07:36 -07:00
parent 40928b0439
commit 588b183dae
6 changed files with 253 additions and 5 deletions

View File

@ -573,6 +573,15 @@
"Record File Type": "Record File Type",
"Notification Video Length": "Notification Video Length",
"Video Codec": "Video Codec",
"Performance Optimization Possible": "Performance Optimization Possible",
"performanceOptimizeText1": "Your camera is providing H.264 stream data. You can set the Stream Type to HLS, Poseidon and Video Codec to copy.",
"Codec Mismatch": "Codec Mismatch",
"Automatic Codec Repair": "Automatic Codec Repair",
"Field Missing Value": "Field Missing Value",
"fieldMissingValueText1": "Your camera is providing MJPEG stream data. You need to set the Monitor Capture Rate. Shinobi will attempt to detect it and fill it automatically.",
"codecMismatchText1": "Your camera is providing H.265 (HEVC) stream data and you are using copy as the Video Codec for the Stream section. Your stream from Shinobi may not appear on devices that cannot use this codec. The Shinobi Mobile App can view these streams.",
"codecMismatchText2": "Your selected Video Codec is not applicable. Your camera is providing MJPEG stream data and you are using copy as the Video Codec for the Stream section. Changed the Stream Type to MJPEG.",
"codecMismatchText3": "Your selected Video Codec is not applicable. Your camera is providing MJPEG stream data and you are using copy as the Video Codec for the Recording section. Changed the Video Codec to libx264.",
"Delete Monitor States Preset": "Delete Monitor States Preset",
"Delete Schedule": "Delete Schedule",
"Delete Monitor State?": "Delete Monitor State",

View File

@ -9,3 +9,26 @@ exports.createQueue = (timeoutInSeconds, queueItemsRunningInParallel) => {
},timeoutInSeconds * 1000 || 1000)
},queueItemsRunningInParallel || 3)
}
const mergeDeep = function(...objects) {
const isObject = obj => obj && typeof obj === 'object';
return objects.reduce((prev, obj) => {
Object.keys(obj).forEach(key => {
const pVal = prev[key];
const oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = pVal.concat(...oVal);
}
else if (isObject(pVal) && isObject(oVal)) {
prev[key] = mergeDeep(pVal, oVal);
}
else {
prev[key] = oVal;
}
});
return prev;
}, {});
}
exports.mergeDeep = mergeDeep

177
libs/ffmpeg/utils.js Normal file
View File

@ -0,0 +1,177 @@
module.exports = (s,config,lang) => {
const { mergeDeep } = require('../common.js')
var exec = require('child_process').exec;
const activeProbes = {}
const runFFprobe = (url,auth,callback) => {
var endData = {ok: false, result: {}}
if(!url){
endData.error = 'Missing URL'
callback(endData)
return
}
if(activeProbes[auth]){
endData.error = 'Account is already probing'
callback(endData)
return
}
activeProbes[auth] = 1
const probeCommand = s.splitForFFPMEG(`-v quiet -print_format json -show_format -show_streams -i "${url}"`).join(' ')
exec('ffprobe ' + probeCommand,function(err,stdout,stderr){
delete(activeProbes[auth])
if(err){
endData.error = err
}else{
endData.ok = true
endData.result = s.parseJSON(stdout)
}
endData.probe = probeCommand
callback(endData)
})
}
const probeMonitor = (monitor,timeoutAmount) => {
return new Promise((resolve,reject) => {
const url = s.buildMonitorUrl(monitor);
runFFprobe(url,`${monitor.ke}${monitor.mid}`,(response) => {
setTimeout(() => {
resolve(response)
},timeoutAmount || 1000)
})
})
}
const getStreamInfoFromProbe = (probeResult) => {
const streamIndex = {
video: [],
audio: [],
all: []
}
const streams = probeResult.streams || []
streams.forEach((stream) => {
const codecType = stream.codec_type || 'video'
const simpleInfo = {
fps: eval(stream.r_frame_rate) || '',
width: stream.coded_width,
height: stream.coded_height,
streamType: stream.codec_type,
codec: stream.codec_name.toLowerCase(),
}
streamIndex.all.push(simpleInfo)
streamIndex[codecType].push(simpleInfo)
})
if(streamIndex.video.length === 0){
streamIndex.video.push({
streamType: 'video',
codec: 'unknown',
})
}
return streamIndex
}
const createWarningsForConfiguration = (monitor,probeResult) => {
const primaryVideoStream = probeResult.video[0]
const warnings = []
const possibleWarnings = [
{
isTrue: monitor.details.stream_vcodec === 'copy' && primaryVideoStream.codec === 'hevc',
title: lang['Codec Mismatch'],
text: lang.codecMismatchText1,
level: 5,
},
{
isTrue: (
(
monitor.details.stream_type === 'mp4' ||
monitor.details.stream_type === 'flv' ||
monitor.details.stream_type === 'hls'
) &&
monitor.details.stream_vcodec === 'copy' &&
primaryVideoStream.codec === 'mjpeg'
),
title: lang['Automatic Codec Repair'],
text: lang.codecMismatchText2,
level: 10,
automaticChange: {
details: {
stream_type: 'mjpeg'
}
}
},
{
isTrue: (
(
monitor.details.stream_type === 'mjpeg' ||
monitor.details.stream_vcodec === 'libx264'
) &&
primaryVideoStream.codec === 'h264'
),
title: lang['Performance Optimization Possible'],
text: lang.performanceOptimizeText1,
level: 1,
},
{
isTrue: (
monitor.details.vcodec === 'copy' &&
primaryVideoStream.codec === 'mjpeg'
),
title: lang['Codec Mismatch'],
text: lang.codecMismatchText3,
level: 10,
automaticChange: {
fps: probeResult.fps,
details: {
vcodec: 'libx264',
}
}
},
{
isTrue: (
!monitor.details.sfps &&
primaryVideoStream.codec === 'mjpeg'
),
title: lang['Field Missing Value'],
text: lang.fieldMissingValueText1,
level: 10,
automaticChange: {
details: {
sfps: probeResult.fps,
}
}
},
];
possibleWarnings.forEach((warning) => {
if(warning.isTrue)warnings.push(warning)
})
return warnings
}
const buildMonitorConfigPartialFromWarnings = (warnings) => {
var configPartial = {}
warnings.forEach((warning) => {
if(warning.automaticChange)configPartial = mergeDeep(configPartial,warning.automaticChange)
})
return configPartial
}
const repairConfiguration = (monitor,probeResult) => {
const warnings = createWarningsForConfiguration(monitor,probeResult)
const configPartial = buildMonitorConfigPartialFromWarnings(warnings)
return mergeDeep(monitor,configPartial)
}
const applyPartialToConfiguration = (activeMonitor,configPartial) => {
Object.keys(configPartial).forEach((key) => {
if(key !== 'details'){
activeMonitor[key] = configPartial[key]
}else{
const details = s.parseJSON(configPartial.details)
Object.keys(details).forEach((key) => {
activeMonitor.details[key] = details[key]
})
}
})
}
return {
ffprobe: runFFprobe,
probeMonitor: probeMonitor,
getStreamInfoFromProbe: getStreamInfoFromProbe,
createWarningsForConfiguration: createWarningsForConfiguration,
buildMonitorConfigPartialFromWarnings: buildMonitorConfigPartialFromWarnings,
applyPartialToConfiguration: applyPartialToConfiguration,
repairConfiguration: repairConfiguration
}
}

View File

@ -12,6 +12,14 @@ const async = require("async");
const URL = require('url')
const { copyObject, createQueue } = require('./common.js')
module.exports = function(s,config,lang){
const {
ffprobe,
probeMonitor,
getStreamInfoFromProbe,
applyPartialToConfiguration,
createWarningsForConfiguration,
buildMonitorConfigPartialFromWarnings,
} = require('./ffmpeg/utils.js')(s,config,lang)
const { cameraDestroy } = require('./monitor/utils.js')(s,config,lang)
const {
setPresetForCurrentPosition
@ -1454,7 +1462,7 @@ module.exports = function(s,config,lang){
})
})
}
s.camera = function(x,e,cn){
s.camera = async (x,e,cn) => {
// x = function or mode
// e = monitor object
// cn = socket connection or callback or options (depends on function chosen)
@ -1551,8 +1559,22 @@ module.exports = function(s,config,lang){
//stop action, monitor already started or recording
return
}
//lock this function
s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Starting});
const probeResponse = await probeMonitor(s.group[e.ke].rawMonitorConfigurations[e.id],2000)
const probeStreams = getStreamInfoFromProbe(probeResponse.result)
activeMonitor.probeResult = probeStreams
const warnings = createWarningsForConfiguration(activeMonitor,probeStreams)
activeMonitor.warnings = warnings
if(warnings.length > 0){
console.log(JSON.stringify(warnings,null,3))
const configPartial = buildMonitorConfigPartialFromWarnings(warnings)
applyPartialToConfiguration(activeMonitor,configPartial)
applyPartialToConfiguration(s.group[e.ke].rawMonitorConfigurations[e.id],configPartial)
}
s.sendMonitorStatus({
id: e.id,
ke: e.ke,
status: lang.Starting,
});
activeMonitor.isStarted = true
if(e.details && e.details.dir && e.details.dir !== ''){
activeMonitor.addStorageId = e.details.dir

View File

@ -687,7 +687,12 @@ module.exports = function(s,config,lang,io){
if(cn.jpeg_on !== true){
cn.join('MON_STREAM_'+d.ke+d.id);
}
tx({f:'monitor_watch_on',id:d.id,ke:d.ke})
tx({
f: 'monitor_watch_on',
id: d.id,
ke: d.ke,
warnings: s.group[d.ke].activeMonitors[d.id].warnings || []
})
s.camera('watch_on',d,cn)
break;
case'watch_off':

View File

@ -269,8 +269,20 @@ $.ccio.globalWebsocket=function(d,user){
if(d.e.length === 1){
$.ccio.init('closeVideo',{mid:d.id,ke:d.ke},user);
}
console.log(d.warnings)
if(d.e.length === 0){
$.ccio.tm(2,$.ccio.mon[d.ke+d.id+user.auth_token],'#monitors_live',user);
$.each(d.warnings,function(n,warning){
console.log(warning)
$.logWriter.draw('[mid="'+d.id+'"][ke="'+d.ke+'"][auth="'+user.auth_token+'"]',{
mid: d.id,
ke: d.ke,
log: {
type: warning.title,
msg: warning.text,
}
},user)
})
}
d.d=JSON.parse($.ccio.mon[d.ke+d.id+user.auth_token].details);
$.ccio.tm('stream-element',$.ccio.mon[d.ke+d.id+user.auth_token],null,user);