framework for Monitor Configuration Warnings and Automatic Repair
parent
40928b0439
commit
588b183dae
|
@ -573,6 +573,15 @@
|
||||||
"Record File Type": "Record File Type",
|
"Record File Type": "Record File Type",
|
||||||
"Notification Video Length": "Notification Video Length",
|
"Notification Video Length": "Notification Video Length",
|
||||||
"Video Codec": "Video Codec",
|
"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 Monitor States Preset": "Delete Monitor States Preset",
|
||||||
"Delete Schedule": "Delete Schedule",
|
"Delete Schedule": "Delete Schedule",
|
||||||
"Delete Monitor State?": "Delete Monitor State",
|
"Delete Monitor State?": "Delete Monitor State",
|
||||||
|
|
|
@ -9,3 +9,26 @@ exports.createQueue = (timeoutInSeconds, queueItemsRunningInParallel) => {
|
||||||
},timeoutInSeconds * 1000 || 1000)
|
},timeoutInSeconds * 1000 || 1000)
|
||||||
},queueItemsRunningInParallel || 3)
|
},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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,14 @@ const async = require("async");
|
||||||
const URL = require('url')
|
const URL = require('url')
|
||||||
const { copyObject, createQueue } = require('./common.js')
|
const { copyObject, createQueue } = require('./common.js')
|
||||||
module.exports = function(s,config,lang){
|
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 { cameraDestroy } = require('./monitor/utils.js')(s,config,lang)
|
||||||
const {
|
const {
|
||||||
setPresetForCurrentPosition
|
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
|
// x = function or mode
|
||||||
// e = monitor object
|
// e = monitor object
|
||||||
// cn = socket connection or callback or options (depends on function chosen)
|
// 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
|
//stop action, monitor already started or recording
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//lock this function
|
const probeResponse = await probeMonitor(s.group[e.ke].rawMonitorConfigurations[e.id],2000)
|
||||||
s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Starting});
|
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
|
activeMonitor.isStarted = true
|
||||||
if(e.details && e.details.dir && e.details.dir !== ''){
|
if(e.details && e.details.dir && e.details.dir !== ''){
|
||||||
activeMonitor.addStorageId = e.details.dir
|
activeMonitor.addStorageId = e.details.dir
|
||||||
|
|
|
@ -687,7 +687,12 @@ module.exports = function(s,config,lang,io){
|
||||||
if(cn.jpeg_on !== true){
|
if(cn.jpeg_on !== true){
|
||||||
cn.join('MON_STREAM_'+d.ke+d.id);
|
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)
|
s.camera('watch_on',d,cn)
|
||||||
break;
|
break;
|
||||||
case'watch_off':
|
case'watch_off':
|
||||||
|
|
|
@ -269,8 +269,20 @@ $.ccio.globalWebsocket=function(d,user){
|
||||||
if(d.e.length === 1){
|
if(d.e.length === 1){
|
||||||
$.ccio.init('closeVideo',{mid:d.id,ke:d.ke},user);
|
$.ccio.init('closeVideo',{mid:d.id,ke:d.ke},user);
|
||||||
}
|
}
|
||||||
|
console.log(d.warnings)
|
||||||
if(d.e.length === 0){
|
if(d.e.length === 0){
|
||||||
$.ccio.tm(2,$.ccio.mon[d.ke+d.id+user.auth_token],'#monitors_live',user);
|
$.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);
|
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);
|
$.ccio.tm('stream-element',$.ccio.mon[d.ke+d.id+user.auth_token],null,user);
|
||||||
|
|
Loading…
Reference in New Issue