diff --git a/definitions/en_CA.js b/definitions/en_CA.js
index c2dcbb97..505741d4 100644
--- a/definitions/en_CA.js
+++ b/definitions/en_CA.js
@@ -18,6 +18,7 @@ module.exports = function(s,config,lang){
"description": "This is the primary task of the monitor.",
"default": "stop",
"example": "",
+ "selector": "h_m",
"possible": [
{
"name": lang.Disabled,
@@ -45,13 +46,13 @@ module.exports = function(s,config,lang){
"name": "mid",
"field": lang["Monitor ID"],
"description": "This is a non-changeable identifier for the monitor. You can duplicate a monitor by double clicking the Monitor ID and changing it.",
- "example": s.gid(),
+ "example": s.gid()
},
{
"name": "name",
"field": lang.Name,
"description": "This is the human-readable display name for the monitor.",
- "example": "Bunny",
+ "example": "Bunny"
},
{
"name": "detail=max_keep_days",
@@ -1242,7 +1243,7 @@ module.exports = function(s,config,lang){
"possible": ""
},
]
- },
+ },
"Recording": {
"id": "monSectionRecording",
"name": lang.Recording,
@@ -1251,6 +1252,7 @@ module.exports = function(s,config,lang){
"input-mapping": "record",
"blockquote": lang.RecordingText,
"blockquoteClass": 'global_tip',
+ "section-class": 'h_m_input h_m_record h_m_idle',
"info": [
// {
// "name": "height",
@@ -1707,6 +1709,111 @@ module.exports = function(s,config,lang){
},
]
},
+ "Timelapse": {
+ "name": lang['Timelapse'],
+ "id": "monSectionTimelapse",
+ "color": "red",
+ "isSection": true,
+ "input-mapping": "record_timelapse",
+ "info": [
+ {
+ "name": "detail=record_timelapse",
+ "field": lang.Enabled,
+ "description": "Create a JPEG based timelapse.",
+ "default": "0",
+ "example": "",
+ "fieldType": "select",
+ "selector": "h_rec_ti",
+ "possible": [
+ {
+ "name": "No",
+ "value": "0"
+ },
+ {
+ "name": "Yes",
+ "value": "1"
+ }
+ ]
+ },
+ {
+ hidden: true,
+ "name": "detail=record_timelapse_mp4",
+ "field": lang.Enabled,
+ "description": "Create an MP4 file at the end of each day for the timelapse.",
+ "default": "0",
+ "example": "",
+ "fieldType": "select",
+ "possible": [
+ {
+ "name": "No",
+ "value": "0"
+ },
+ {
+ "name": "Yes",
+ "value": "1"
+ }
+ ]
+ },
+ {
+ hidden: true,
+ "name": "detail=record_timelapse_fps",
+ "field": lang['Creation Interval'],
+ "description": "",
+ "default": "900",
+ "example": "",
+ "form-group-class": "h_rec_ti_input h_rec_ti_1",
+ "fieldType": "select",
+ "possible": [
+ {
+ "name": `15 ${lang.minutes}`,
+ "value": "900"
+ },
+ {
+ "name": `30 ${lang.minutes}`,
+ "value": "1800"
+ },
+ {
+ "name": `45 ${lang.minutes}`,
+ "value": "2700"
+ },
+ {
+ "name": `60 ${lang.minutes}`,
+ "value": "3600"
+ }
+ ]
+ },
+ {
+ hidden: true,
+ "name": "detail=record_timelapse_scale_x",
+ "field": lang['Image Width'],
+ "description": "",
+ "default": "",
+ "example": "",
+ "form-group-class": "h_rec_ti_input h_rec_ti_1",
+ "possible": ""
+ },
+ {
+ hidden: true,
+ "name": "detail=record_timelapse_scale_y",
+ "field": lang['Image Height'],
+ "description": "",
+ "default": "",
+ "example": "",
+ "form-group-class": "h_rec_ti_input h_rec_ti_1",
+ "possible": ""
+ },
+ {
+ hidden: true,
+ "name": "detail=record_timelapse_vf",
+ "field": lang['Video Filter'],
+ "description": "",
+ "default": "",
+ "example": "",
+ "form-group-class": "h_rec_ti_input h_rec_ti_1",
+ "possible": ""
+ },
+ ]
+ },
"Custom": {
"name": "Custom",
"color": "navy",
@@ -1976,16 +2083,6 @@ module.exports = function(s,config,lang){
{
hidden: true,
"name": "detail=detector_send_video_length",
- "field": lang['Notification Video Length'],
- "description": "",
- "default": "10",
- "example": "",
- "form-group-class": "h_det_input h_det_1",
- "form-group-class-pre-layer": "h_rec_mtd_input h_rec_mtd_hot h_rec_mtd_sip",
- "possible": ""
- },
- {
- "name": "detail=detector_send_video_length",
"field": lang["Notification Video Length"],
"description": "In seconds. The length of the video that gets sent to your Notification service, like Email or Discord.",
"default": "10",
@@ -2522,15 +2619,13 @@ module.exports = function(s,config,lang){
]
},
{
- hidden: true,
"name": lang['Object Detection'],
"color": "orange",
id: "monSectionDetectorObject",
- headerTitle: `${lang['Object Detection']} ${lang['Plugin']} : ${lang['Not Connected']}${lang['Connected']}`,
+ headerTitle: `${lang['Object Detection']} ${lang['Not Connected']}${lang['Connected']}`,
isFormGroupGroup: true,
isSection: true,
- "section-pre-class": "h_det_input h_det_1",
- "section-class": "shinobi-detector-opencv shinobi-detector-openalpr shinobi-detector-yolo shinobi-detector-dlib shinobi-detector_plug",
+ "section-class": "h_det_input h_det_1",
"info": [
{
"name": "detail=detector_use_detect_object",
@@ -2723,7 +2818,7 @@ module.exports = function(s,config,lang){
hidden: true,
"name": lang['Traditional Recording'],
"color": "orange",
- id: "monSectionLisencePlateDetector",
+ id: "monSectionDetectorTraditionalRecording",
isSection: true,
isAdvanced: true,
isFormGroupGroup: true,
diff --git a/languages/en_CA.json b/languages/en_CA.json
index 39e26417..ce229c12 100644
--- a/languages/en_CA.json
+++ b/languages/en_CA.json
@@ -389,6 +389,9 @@
"Detector Recording Complete": "Detector Recording Complete",
"Clear Recorder Process": "Clear Recorder Process",
"Logging": "Logging",
+ "Timelapse": "Timelapse",
+ "Creation Interval": "Creation Interval",
+ "Plugin": "Plugin",
"IdentityText1": "This is how the system will identify the data for this stream. You cannot change the Monitor ID once you have pressed save. If you want you can make the Monitor ID more human readable before you continue.",
"IdentityText2": "You can duplicate a monitor by modifying the Monitor ID then pressing save. You cannot use the ID of a monitor that already exists or it will save over that monitor's database information.",
"opencvCascadesText": "If you see nothing here then just download this package of cascades. Drop them into plugins/opencv/cascades then press refresh .",
diff --git a/libs/ffmpeg.js b/libs/ffmpeg.js
index 343bdb07..879ab7e9 100644
--- a/libs/ffmpeg.js
+++ b/libs/ffmpeg.js
@@ -905,6 +905,24 @@ module.exports = function(s,config,lang,onFinish){
x.pipe += ' -q:v 1 -an -c:v copy -f hls -tune zerolatency -g 1 -hls_time 2 -hls_list_size 3 -start_number 0 -live_start_index 3 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'coProcessor.m3u8"'
}
}
+ ffmpeg.buildTimelapseOutput = function(e,x){
+ if(e.details.record_timelapse === '1'){
+ if(e.details.input_map_choices&&e.details.input_map_choices.record_timelapse){
+ //add input feed map
+ x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.record_timelapse)
+ }
+ var flags = []
+ if(e.details.record_timelapse_fps && e.details.record_timelapse_fps !== ''){
+ flags.push('-r 1/' + e.details.record_timelapse_fps)
+ }else{
+ flags.push('-r 1/900') // 15 minutes
+ }
+ 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}`)
+ // x.pipe+=` -strftime 1 ${flags.join(' ')} -an -q:v 1 "${e.dirTimelapse}%Y-%m-%d/%Y-%m-%dT%H-%M-%S.jpg"`
+ 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;
@@ -962,6 +980,7 @@ module.exports = function(s,config,lang,onFinish){
ffmpeg.buildAudioDetector(e,x)
ffmpeg.buildMainDetector(e,x)
ffmpeg.buildCoProcessorFeed(e,x)
+ ffmpeg.buildTimelapseOutput(e,x)
s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){
extender(e,x)
})
diff --git a/libs/monitor.js b/libs/monitor.js
index f1820b42..5ad4c41d 100644
--- a/libs/monitor.js
+++ b/libs/monitor.js
@@ -625,13 +625,15 @@ module.exports = function(s,config,lang){
}
s.createCameraFolders = function(e){
//set the recording directory
+ var monitorRecordingFolder
if(e.details && e.details.dir && e.details.dir !== '' && config.childNodes.mode !== 'child'){
//addStorage choice
e.dir=s.checkCorrectPathEnding(e.details.dir)+e.ke+'/';
if (!fs.existsSync(e.dir)){
fs.mkdirSync(e.dir);
}
- e.dir=e.dir+e.id+'/';
+ monitorRecordingFolder = e.dir + e.id + ''
+ e.dir = monitorRecordingFolder + '/'
if (!fs.existsSync(e.dir)){
fs.mkdirSync(e.dir);
}
@@ -645,6 +647,12 @@ module.exports = function(s,config,lang){
if (!fs.existsSync(e.dir)){
fs.mkdirSync(e.dir);
}
+ monitorRecordingFolder = s.dir.videos + e.ke + '/' + e.id
+ }
+ //
+ e.dirTimelapse = monitorRecordingFolder + '_timelapse/'
+ if (!fs.existsSync(e.dirTimelapse)){
+ fs.mkdirSync(e.dirTimelapse)
}
// exec('chmod -R 777 '+e.dir,function(err){
//
@@ -896,6 +904,52 @@ module.exports = function(s,config,lang){
audioDetector.start()
s.group[e.ke].mon[e.id].spawn.stdio[6].pipe(audioDetector.streamDecoder)
}
+ if(e.details.record_timelapse === '1'){
+ s.group[e.ke].mon[e.id].spawn.stdio[7].on('data',function(data){
+ var fileStream = s.group[e.ke].mon[e.id].recordTimelapseWriter
+ if(!fileStream){
+ var currentDate = s.formattedTime(null,'YYYY-MM-DD')
+ var filename = s.formattedTime() + '.jpg'
+ var location = e.dirTimelapse + currentDate + '/'
+ if(!fs.existsSync(location)){
+ fs.mkdirSync(location)
+ }
+ fileStream = fs.createWriteStream(location + filename)
+ fileStream.on('close', function () {
+ s.group[e.ke].mon[e.id].recordTimelapseWriter = null
+ var fileStats = fs.statSync(location + filename)
+ var fileInfo = {}
+ if(e.details && e.details.dir && e.details.dir !== ''){
+ fileInfo.dir = e.details.dir
+ }
+ fileInfo.size = fileStats.size
+ s.sqlQuery('SELECT * FROM Timelapses WHERE ke=? AND mid=? AND date=?',[e.ke,e.id,currentDate],function(err,rows){
+ if(rows && rows[0]){
+ var row = rows[0]
+ var details = s.parseJSON(row.details)
+ details.files[filename] = fileInfo
+ row.size += fileStats.size
+ s.sqlQuery('UPDATE Timelapses SET details=?,size=? WHERE ke=? AND mid=? AND date=?',[s.s(details),row.size,e.ke,e.id,currentDate],function(){
+
+ })
+ }else{
+ var details = {
+ files: {}
+ }
+ details.files[filename] = fileInfo
+ s.sqlQuery('INSERT INTO Timelapses (ke,mid,details,date,size) VALUES (?,?,?,?,?)',[e.ke,e.id,s.s(details),currentDate,fileStats.size])
+ }
+ })
+ })
+ s.group[e.ke].mon[e.id].recordTimelapseWriter = fileStream
+ }
+ fileStream.write(data)
+ clearTimeout(s.group[e.ke].mon[e.id].recordTimelapseWriterTimeout)
+ s.group[e.ke].mon[e.id].recordTimelapseWriterTimeout = setTimeout(function(){
+ fileStream.end()
+ },900)
+ })
+ }
if(e.details.detector === '1' && e.coProcessor === false){
s.ocvTx({f:'init_monitor',id:e.id,ke:e.ke})
//frames from motion detect
diff --git a/libs/sql.js b/libs/sql.js
index 92d3e187..a3d65f43 100644
--- a/libs/sql.js
+++ b/libs/sql.js
@@ -46,8 +46,12 @@ module.exports = function(s,config){
}
return newQuery
}
+ s.getUnixDate = function(value){
+ newValue = new Date(value).valueOf()
+ return newValue
+ }
s.stringToSqlTime = function(value){
- newValue = new Date(value.replace('T',' '))
+ newValue = s.getUnixDate(s.nameToTime(value))
return newValue
}
s.sqlQuery = function(query,values,onMoveOn,hideLog){
@@ -57,6 +61,11 @@ module.exports = function(s,config){
var values = [];
}
if(!onMoveOn){onMoveOn=function(){}}
+ // if(s.databaseOptions.client === 'pg'){
+ // query = query
+ // .replace(/ NOT LIKE /g," NOT ILIKE ")
+ // .replace(/ LIKE /g," ILIKE ")
+ // }
var mergedQuery = s.mergeQueryValues(query,values)
s.debugLog('s.sqlQuery QUERY',mergedQuery)
if(!s.databaseEngine || !s.databaseEngine.raw){
@@ -109,6 +118,10 @@ module.exports = function(s,config){
s.sqlQuery("CREATE TABLE IF NOT EXISTS `Schedules` (`ke` varchar(50) DEFAULT NULL,`name` text,`details` text,`start` varchar(10) DEFAULT NULL,`end` varchar(10) DEFAULT NULL,`enabled` int(1) NOT NULL DEFAULT '1')" + mySQLtail + ';',[],function(err){
if(err)console.error(err)
},true)
+ //add Schedules table, will remove in future
+ s.sqlQuery("CREATE TABLE IF NOT EXISTS `Timelapses` (`ke`varchar(50)NOT NULL,`mid`varchar(50)NOT NULL,`details`longtext,`date`date NOT NULL,`time`timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,`end`timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,`size`int(11)NOT NULL)" + mySQLtail + ';',[],function(err){
+ if(err)console.error(err)
+ },true)
//add Cloud Videos table, will remove in future
s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Videos` (`mid` varchar(50) NOT NULL,`ke` varchar(50) DEFAULT NULL,`href` text NOT NULL,`size` float DEFAULT NULL,`time` timestamp NULL DEFAULT NULL,`end` timestamp NULL DEFAULT NULL,`status` int(1) DEFAULT \'0\',`details` text)' + mySQLtail + ';',[],function(err){
if(err)console.error(err)
diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js
index 982a94a3..97c79925 100644
--- a/libs/webServerPaths.js
+++ b/libs/webServerPaths.js
@@ -1094,6 +1094,165 @@ module.exports = function(s,config,lang,app,io){
},res,req);
});
/**
+ * API : Get Timelapse images
+ */
+ app.get([
+ config.webPaths.apiPrefix+':auth/timelapse/:ke',
+ config.webPaths.apiPrefix+':auth/timelapse/:ke/:id',
+ ], function (req,res){
+ res.setHeader('Content-Type', 'application/json');
+ s.auth(req.params,function(user){
+ var hasRestrictions = user.details.sub && user.details.allmonitors !== '1'
+ if(
+ user.permissions.watch_videos==="0" ||
+ hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1)
+ ){
+ res.end(s.prettyPrint([]))
+ return
+ }
+ req.sql='SELECT * FROM `Timelapses` WHERE ke=?';req.ar=[req.params.ke];
+ if(req.query.archived=='1'){
+ req.sql+=' AND details LIKE \'%"archived":"1"\''
+ }
+ if(!req.params.id){
+ if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){
+ try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){}
+ req.or=[];
+ user.details.monitors.forEach(function(v,n){
+ req.or.push('mid=?');req.ar.push(v)
+ })
+ req.sql+=' AND ('+req.or.join(' OR ')+')'
+ }
+ }else{
+ if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){
+ req.sql+=' and mid=?'
+ req.ar.push(req.params.id)
+ }else{
+ res.end('[]');
+ return;
+ }
+ }
+ if(req.query.start||req.query.end){
+ if(req.query.start && req.query.start !== ''){
+ req.query.start = s.stringToSqlTime(req.query.start)
+ }
+ if(req.query.end && req.query.end !== ''){
+ req.query.end = s.stringToSqlTime(req.query.end)
+ }
+ if(!req.query.startOperator||req.query.startOperator==''){
+ req.query.startOperator='>='
+ }
+ if(!req.query.endOperator||req.query.endOperator==''){
+ req.query.endOperator='<='
+ }
+ var endIsStartTo
+ var theEndParameter = '`end`'
+ if(req.query.endIsStartTo){
+ endIsStartTo = true
+ theEndParameter = '`time`'
+ }
+ switch(true){
+ case(req.query.start&&req.query.start!==''&&req.query.end&&req.query.end!==''):
+ req.sql+=' AND `time` '+req.query.startOperator+' ? AND '+theEndParameter+' '+req.query.endOperator+' ?'
+ req.ar.push(req.query.start)
+ req.ar.push(req.query.end)
+ break;
+ case(req.query.start&&req.query.start!==''):
+ req.sql+=' AND `time` '+req.query.startOperator+' ?'
+ req.ar.push(req.query.start)
+ break;
+ case(req.query.end&&req.query.end!==''):
+ req.sql+=' AND '+theEndParameter+' '+req.query.endOperator+' ?'
+ req.ar.push(req.query.end)
+ break;
+ }
+ }
+ req.sql+=' ORDER BY `time` DESC'
+ s.sqlQuery(req.sql,req.ar,function(err,r){
+ if(!r){
+ res.end(s.prettyPrint([]))
+ return
+ }
+ r.forEach(function(row){
+ row.details = s.parseJSON(row.details)
+ })
+ res.end(s.prettyPrint(r))
+ })
+ },res,req);
+ });
+ /**
+ * API : Get Timelapse images
+ */
+ app.get([
+ config.webPaths.apiPrefix+':auth/timelapse/:ke/:id/:date',
+ config.webPaths.apiPrefix+':auth/timelapse/:ke/:id/:date/:filename',
+ ], function (req,res){
+ res.setHeader('Content-Type', 'application/json');
+ s.auth(req.params,function(user){
+ var hasRestrictions = user.details.sub && user.details.allmonitors !== '1'
+ if(
+ user.permissions.watch_videos==="0" ||
+ hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1)
+ ){
+ res.end(s.prettyPrint([]))
+ return
+ }
+ req.sql='SELECT * FROM `Timelapses` WHERE ke=?';req.ar=[req.params.ke];
+ if(req.query.archived=='1'){
+ req.sql+=' AND details LIKE \'%"archived":"1"\''
+ }
+ if(!req.params.id){
+ if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){
+ try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){}
+ req.or=[];
+ user.details.monitors.forEach(function(v,n){
+ req.or.push('mid=?');req.ar.push(v)
+ })
+ req.sql+=' AND ('+req.or.join(' OR ')+')'
+ }
+ }else{
+ if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){
+ req.sql+=' and mid=?'
+ req.ar.push(req.params.id)
+ }else{
+ res.end('[]');
+ return;
+ }
+ }
+ req.sql+=' and date=?'
+ req.ar.push(req.params.date)
+ req.sql+=' ORDER BY `time` DESC'
+ s.sqlQuery(req.sql,req.ar,function(err,r){
+ if(!r || !r[0]){
+ res.end(s.prettyPrint([]))
+ return
+ }
+ var timelapse = r[0]
+ timelapse.details = s.parseJSON(timelapse.details)
+ if(req.params.filename){
+ var fileInfo = timelapse.details.files[req.params.filename]
+ if(fileInfo){
+ res.contentType('image/jpeg')
+ var fileLocation
+ var currentDate = req.params.date
+ if(fileInfo.dir){
+ fileLocation = `${s.checkCorrectPathEnding(fileInfo.dir)}`
+ }else{
+ fileLocation = `${s.dir.videos}`
+ }
+ fileLocation = `${fileLocation}${timelapse.ke}/${timelapse.mid}_timelapse/${currentDate}/${req.params.filename}`
+ res.on('finish',function(){res.end()})
+ fs.createReadStream(fileLocation).pipe(res)
+ }else{
+ res.end(s.prettyPrint({ok: false, msg: lang['File Not Exist']}))
+ }
+ }else{
+ res.end(s.prettyPrint(timelapse))
+ }
+ })
+ },res,req);
+ });
+ /**
* API : Get Events
*/
app.get([config.webPaths.apiPrefix+':auth/events/:ke',config.webPaths.apiPrefix+':auth/events/:ke/:id',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit/:start',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit/:start/:end'], function (req,res){
diff --git a/web/libs/css/dash2.forms.css b/web/libs/css/dash2.forms.css
index 15ff11dd..a513f6e3 100644
--- a/web/libs/css/dash2.forms.css
+++ b/web/libs/css/dash2.forms.css
@@ -42,3 +42,4 @@ form.modal-body{margin:0}
.form-group-group.grey{border-color:#777}
.form-group-group.grey > h4{background:#777;color:#fff}
.dark .form-group-group{background:#222}
+.form-group-group:last-child {margin-bottom: 0}