diff --git a/definitions/base.js b/definitions/base.js index ee77123f..0bc47c5e 100644 --- a/definitions/base.js +++ b/definitions/base.js @@ -725,6 +725,15 @@ module.exports = function(s,config,lang){ "form-group-class": "h_gpud_input h_gpud_1", "possible": "" }, + { + "name": "detail=hwaccel_format", + "field": lang.hwaccel_format, + "description": "Hardware Acceleration Format", + "default": "", + "example": "vaapi", + "form-group-class": "h_gpud_input h_gpud_1", + "possible": "" + }, ] }, "Input Maps": { @@ -4931,26 +4940,6 @@ module.exports = function(s,config,lang){ "placeholder": "3", attribute:'localStorage="montage"', }, - { - "field": lang['Cycle Monitors per row'], - "placeholder": "2", - attribute:'localStorage="cycleLivePerRow"', - }, - { - "field": lang['Number of Cycle Monitors'], - "placeholder": "4", - attribute:'localStorage="cycleLiveNumberOfMonitors"', - }, - { - "field": lang['Cycle Monitor Height'], - "placeholder": "4", - attribute:'localStorage="cycleLiveMonitorHeight"', - }, - { - "field": lang['Cycle Interval'], - "placeholder": "30000", - attribute:'localStorage="cycleLiveTimerAmount"', - }, ] }, "Preferences": { @@ -7723,12 +7712,6 @@ module.exports = function(s,config,lang){ attributes: 'shinobi-switch="monitorMuteAudio" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', color: 'grey', }, - { - label: lang['Cycle Monitors'], - class: 'cursor-pointer', - attributes: 'shinobi-switch="cycleLiveGrid" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', - color: 'grey', - }, // { // label: lang['JPEG Mode'], // class: 'cursor-pointer', diff --git a/languages/de.json b/languages/de.json index 185ec88c..739ca7c7 100644 --- a/languages/de.json +++ b/languages/de.json @@ -385,6 +385,7 @@ "Started": "Gestartet", "Status Indicator": "Statusanzeige", "Stop URL": "Stop-URL", + "Storage Class": "Speicherklassen", "Stream": "Stream", "Stream Flags": "Stream-Flags", "Stream Timestamp": "Stream-Timestamp", diff --git a/languages/en_CA.json b/languages/en_CA.json index 418c67fa..39b0af44 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -47,6 +47,7 @@ "Session Key": "Session Key", "Active Monitors": "Active Monitors", "Storage Use": "Storage Use", + "Storage Class": "Storage Class", "Use Raw Snapshot": "Use Raw Snapshot", "Failed to Edit Account": "Failed to Edit Account", "How to Connect": "How to Connect", @@ -635,6 +636,7 @@ "NoVideosFoundForDateRange": "No Videos found in this date range. Try setting the start date further back.", "NoLogsFoundForDateRange": "No Logs found in this date range. Try widening the date range.", "monitorEditFailedMaxReached": "Your account has reached the maximum number of cameras that can be created. Speak to an administrator if you would like this changed.", + "monitorEditFailedMaxReachedUnactivated": "Your system has reached the maximum number of cameras that can be created. You must activate your installation to create more.", "Sub-Accounts": "Sub-Accounts", "Stream in Background": "Stream in Background", "Carousel in Background": "Carousel in Background", @@ -1161,6 +1163,7 @@ "startUpText3": "waiting to give unfinished video check some time. 3 seconds.", "startUpText4": "Starting Monitors... Please Wait...", "startUpText5": "Shinobi is ready.", + "notReadyYet": "Shinobi is not ready yet to do this.", "startUpText6": "Orphaned Videos Found and Inserted", "Migrator": "Migrator", "Thumbnail": "Thumbnail", @@ -1269,6 +1272,7 @@ "hwaccel": "Acceleration Engine", "hwaccel_vcodec": "Video Decoder", "hwaccel_device": "HWAccel Device", + "hwaccel_format": "HWAccel Format", "Get Logs to Client": "Get Logs to Client", "Hardware Accelerated": "Hardware Accelerated", "Accelerator": "Accelerator", diff --git a/languages/fr.json b/languages/fr.json index 332fc774..9bc85f0d 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -504,6 +504,7 @@ "Stop": "Arrêt", "Stop URL": "URL d'arrêt", "Storage Location": "Emplacement de stockage", + "Storage Class": "Classes de Stockage", "Stream": "Flux", "Stream Channel": "Canal du flux de données", "Stream Flags": "Etiquettes du flux", diff --git a/languages/it.json b/languages/it.json index c67e1c41..424faf02 100644 --- a/languages/it.json +++ b/languages/it.json @@ -943,6 +943,7 @@ "Stopping": "Fermarsi", "Storage Location": "Posizione di archiviazione", "Storage Use": "Uso di archiviazione", + "Storage Class": "Classi di Archiviazione", "Stream": "Flusso", "Stream Channel": "Canale di flusso", "Stream Flags": "Flag di streaming", diff --git a/languages/ja.json b/languages/ja.json index 606e6a7b..f0bc29cf 100644 --- a/languages/ja.json +++ b/languages/ja.json @@ -1489,6 +1489,7 @@ "Stopping": "停止中", "Storage Location": "Storage Location", "Storage Use": "使用ストレージ", + "Storage Class": "ストレージクラス", "Stream Channel": "Stream Channel", "Stream Channels": "Stream Channels", "Stream Flags": "ストリームフラグ", diff --git a/libs/auth.js b/libs/auth.js index 43a4b88f..6e1e219f 100644 --- a/libs/auth.js +++ b/libs/auth.js @@ -169,7 +169,7 @@ module.exports = function(s,config,lang){ activeSession && ( activeSession.ip.indexOf('0.0.0.0') > -1 || - params.ip.indexOf(activeSession.ip) > -1 + params.ip && (params.ip.indexOf(activeSession.ip) > -1) ) ){ if(!user.lang){ @@ -218,6 +218,13 @@ module.exports = function(s,config,lang){ onFail() } } + s.authPromise = function(params,res,req){ + return new Promise((resolve) => { + s.auth(params, (user) => { + resolve(user) + },res,req) + }) + } //super user authentication handler s.superAuth = function(params,callback,res,req){ var userFound = false diff --git a/libs/checker/utils.js b/libs/checker/utils.js new file mode 100644 index 00000000..29602d1b --- /dev/null +++ b/libs/checker/utils.js @@ -0,0 +1,106 @@ +const fetch = require('node-fetch'); +const { AbortController } = require('node-abort-controller') +module.exports = (s,config,lang) => { + const fetchTimeout = (url, ms, { signal, ...options } = {}) => { + const controller = new AbortController(); + const promise = fetch(url, { signal: controller.signal, ...options }); + if (signal) signal.addEventListener("abort", () => controller.abort()); + const timeout = setTimeout(() => controller.abort(), ms); + return promise.finally(() => clearTimeout(timeout)); + } + function canAddMoreMonitors() { + const cameraCountChecks = [ + { kind: 'ec2', maxCameras: 2, condition: config.isEC2 }, + { kind: 'highCoreCount', maxCameras: 50, condition: config.isHighCoreCount }, + { kind: 'default', maxCameras: 30, condition: true }, + ]; + if (!config.userHasSubscribed) { + const monitorCountOnSystem = getTotalMonitorCount(); + for (const check of cameraCountChecks) { + if (check.condition && monitorCountOnSystem >= check.maxCameras) { + return false; + } + } + } + return true; + } + function getTotalMonitorCount() { + let monitorCount = 0; + try{ + for (const groupKey in s.group) { + const monitorIds = Object.keys(s.group[groupKey].rawMonitorConfigurations); + monitorCount += monitorIds.length; + } + }catch(err){ + s.debugLog(err) + } + return monitorCount; + } + function sanitizeMonitorConfig(monitorConfig){ + const sanitized = {} + const errors = {} + const availableKeys = [ + {name: 'ke', type: 'string'}, + {name: 'mid', type: 'string'}, + {name: 'name', type: 'string'}, + {name: 'details', type: 'longtext'}, + {name: 'type', type: 'string', defaultTo: 'h264'}, + {name: 'ext', type: 'string', defaultTo: 'mp4'}, + {name: 'protocol', type: 'string', defaultTo: 'rtsp'}, + {name: 'host', type: 'string', missingHalt: true }, + {name: 'path', type: 'string', missingHalt: true }, + {name: 'port', type: 'integer', defaultTo: 554}, + {name: 'fps', type: 'integer', defaultTo: null}, + {name: 'mode', type: 'string', defaultTo: 'stop'}, + {name: 'width', type: 'integer', defaultTo: 640}, + {name: 'height', type: 'integer', defaultTo: 480}, + ]; + for(item of availableKeys){ + const column = item.name; + const type = item.type; + const monitorValue = monitorConfig[column] + let newValue = monitorValue; + switch(type){ + case'string': + case'longtext': + if(monitorValue instanceof String){ + + }else{ + newValue = `${monitorValue}`; + errors[column] = `corrected ${type} type : ${typeof monitorValue}`; + } + break; + case'integer': + if(!isNaN(monitorValue)){ + + }else{ + newValue = parseInt(monitorValue); + errors[column] = `corrected ${type} type : ${typeof monitorValue}`; + } + break; + } + sanitized[column] = newValue; + } + return { + sanitized, + errors + } + } + function isGroupBelowMaxMonitorCount(groupKey){ + const theGroup = s.group[groupKey]; + try{ + const initData = theGroup.init; + const maxCamerasAllowed = parseInt(initData.max_camera) || false; + return (!maxCamerasAllowed || Object.keys(theGroup.activeMonitors).length <= parseInt(maxCamerasAllowed)) + }catch(err){ + return true + } + } + return { + fetchTimeout, + canAddMoreMonitors, + getTotalMonitorCount, + sanitizeMonitorConfig, + isGroupBelowMaxMonitorCount, + } +} diff --git a/libs/config.js b/libs/config.js index c9c226c6..2ed614ff 100644 --- a/libs/config.js +++ b/libs/config.js @@ -9,6 +9,8 @@ module.exports = function(s){ try{ var config = require(s.location.config) }catch(err){ + console.log('FAILED TO OPEN CONFIGURATION FILE') + console.log('CHECK SYNTAX!') var config = {} } if(!config.productType){ diff --git a/libs/cron.js b/libs/cron.js index f728c93e..64a4d0b8 100644 --- a/libs/cron.js +++ b/libs/cron.js @@ -28,9 +28,24 @@ module.exports = (s,config,lang) => { case's.deleteVideo': s.deleteVideo(data.file) break; + case's.deleteCloudVideo': + s.deleteVideo(data.file) + break; case's.deleteFileBinEntry': s.deleteFileBinEntry(data.file) break; + case's.onCronGroupBeforeProcessed': + s.runExtensionsForArray('onCronGroupBeforeProcessed', null, data.args) + break; + case's.onCronGroupBeforeProcessedAwaited': + s.runExtensionsForArrayAwaited('onCronGroupBeforeProcessedAwaited', null, data.args) + break; + case's.onCronGroupProcessed': + s.runExtensionsForArray('onCronGroupProcessed', null, data.args) + break; + case's.onCronGroupProcessedAwaited': + s.runExtensionsForArrayAwaited('onCronGroupProcessedAwaited', null, data.args) + break; case's.setDiskUsedForGroup': function doOnMain(){ s.setDiskUsedForGroup(data.ke,data.size,data.target || undefined) diff --git a/libs/cron/worker.js b/libs/cron/worker.js index 0c011f72..4fc8b4e6 100644 --- a/libs/cron/worker.js +++ b/libs/cron/worker.js @@ -4,7 +4,7 @@ const moment = require('moment'); const exec = require('child_process').exec; const spawn = require('child_process').spawn; const { parentPort, isMainThread, workerData } = require('worker_threads'); -const config = workerData +const config = workerData; process.on('uncaughtException', function (err) { errorLog('uncaughtException',err); }); @@ -96,6 +96,18 @@ function beginProcessing(){ const deleteFileBinEntry = (x) => { postMessage({f:'s.deleteFileBinEntry',file:x}) } + const onCronGroupBeforeProcessed = (...args) => { + postMessage({f:'s.onCronGroupBeforeProcessed', args: args}) + } + const onCronGroupBeforeProcessedAwaited = (...args) => { + postMessage({f:'s.onCronGroupBeforeProcessedAwaited', args: args}) + } + const onCronGroupProcessed = (...args) => { + postMessage({f:'s.onCronGroupProcessed', args: args}) + } + const onCronGroupProcessedAwaited = (...args) => { + postMessage({f:'s.onCronGroupProcessedAwaited', args: args}) + } const setDiskUsedForGroup = (groupKey,size,target,videoRow) => { postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target, videoRow: videoRow}) } @@ -546,6 +558,9 @@ function beginProcessing(){ overlapLocks[v.ke] = true v.d = JSON.parse(v.details); try{ + debugLog('--- Running Pre Extenders') + onCronGroupBeforeProcessed(v) + onCronGroupBeforeProcessedAwaited(v) await deleteOldVideos(v) debugLog('--- deleteOldVideos Complete') await deleteOldTimelapseFrames(v) @@ -562,6 +577,9 @@ function beginProcessing(){ debugLog('--- checkFilterRules Complete') await deleteRowsWithNoVideo(v) debugLog('--- deleteRowsWithNoVideo Complete') + debugLog('--- Running Post Extenders') + onCronGroupProcessed(v) + onCronGroupProcessedAwaited(v) }catch(err){ normalLog(`Failed to Complete User : ${v.mail}`) normalLog(err) diff --git a/libs/database/utils.js b/libs/database/utils.js index ea33e257..0951a330 100644 --- a/libs/database/utils.js +++ b/libs/database/utils.js @@ -175,9 +175,9 @@ module.exports = function(s,config){ } const getDatabaseRows = function(options,callback){ //current cant handle `end` time - var whereQuery = [ + var whereQuery = options.groupKey ? [ ['ke','=',options.groupKey], - ] + ] : [] const monitorRestrictions = options.monitorRestrictions var frameLimit = options.limit const noLimit = options.noLimit === '1' @@ -482,6 +482,36 @@ module.exports = function(s,config){ } } } + const dateSubtract = function(date, interval, units){ + var ret = date + var checkRollover = function() { if(ret.getDate() != date.getDate()) ret.setDate(0);}; + switch(interval.toLowerCase()) { + case 'year' : ret.setFullYear(ret.getFullYear() - units); checkRollover(); break; + case 'quarter': ret.setMonth(ret.getMonth() - 3*units); checkRollover(); break; + case 'month' : ret.setMonth(ret.getMonth() - units); checkRollover(); break; + case 'week' : ret.setDate(ret.getDate() - 7*units); break; + case 'day' : ret.setDate(ret.getDate() - units); break; + case 'hour' : ret.setTime(ret.getTime() - units*3600000); break; + case 'minute' : ret.setTime(ret.getTime() - units*60000); break; + case 'second' :default: ret.setTime(ret.getTime() - units*1000); break; + } + return (new Date(ret)) + } + const sqlDate = function(value){ + var value = value.toLowerCase() + var splitValue = value.split(' ') + var amount = parseFloat(splitValue[0]) + var today = new Date() + var query + if(value.indexOf('min') > -1){ + query = dateSubtract(today,'minute',amount) + }else if(value.indexOf('day') > -1){ + query = dateSubtract(today,'day',amount) + }else if(value.indexOf('hour') > -1){ + query = dateSubtract(today,'hour',amount) + } + return query + } return { knexQuery: knexQuery, knexQueryPromise: knexQueryPromise, @@ -499,5 +529,7 @@ module.exports = function(s,config){ alterColumn, addColumn, isMySQL, + sqlDate, + dateSubtract, } } diff --git a/libs/events/utils.js b/libs/events/utils.js index 429b5f84..629c5c87 100644 --- a/libs/events/utils.js +++ b/libs/events/utils.js @@ -39,11 +39,12 @@ module.exports = (s,config,lang) => { async function saveImageFromEvent(options,frameBuffer){ const monitorId = options.mid || options.id const groupKey = options.ke - if(!frameBuffer || imageSaveEventLock[groupKey + monitorId])return; + //if(!frameBuffer || imageSaveEventLock[groupKey + monitorId])return; + if(!frameBuffer || frameBuffer.length === 0 || imageSaveEventLock[groupKey + monitorId]) return; const eventTime = options.time const objectsFound = options.matrices const monitorConfig = Object.assign({id: monitorId},s.group[groupKey].rawMonitorConfigurations[monitorId]) - const timelapseRecordingDirectory = s.getTimelapseFrameDirectory({mid: monitorId, ke: groupKey}) + const timelapseRecordingDirectory = s.getTimelapseFrameDirectory(monitorConfig) const currentDate = s.formattedTime(eventTime,'YYYY-MM-DD') const filename = s.formattedTime(eventTime) + '.jpg' const location = timelapseRecordingDirectory + currentDate + '/' @@ -79,9 +80,8 @@ module.exports = (s,config,lang) => { var newString = string + '' var d = Object.assign(eventData,addOps) var detailString = s.stringJSON(d.details) - var tag = detailString.matrices - && detailString.matrices[0] - && detailString.matrices[0].tag; + var firstMatrix = d.details.matrices ? d.details.matrices[0] : null; + var tag = firstMatrix ? firstMatrix.tag : ''; newString = newString .replace(/{{CONFIDENCE}}/g,d.details.confidence) .replace(/{{TIME}}/g,d.currentTimestamp) @@ -91,18 +91,16 @@ module.exports = (s,config,lang) => { .replace(/{{MONITOR_NAME}}/g,s.group[d.ke].rawMonitorConfigurations[d.id].name) .replace(/{{GROUP_KEY}}/g,d.ke) .replace(/{{DETAILS}}/g,detailString); - if(tag){ + if(firstMatrix && tag){ newString = newString.replace(/{{TAG}}/g,tag) } - if(d.details.confidence){ + if(d.details.confidence || firstMatrix){ newString = newString - .replace(/{{CONFIDENCE}}/g,d.details.confidence) + .replace(/{{CONFIDENCE}}/g,d.details.confidence || firstMatrix.confidence) } - if(newString.includes("REASON")) { - if(d.details.reason) { + if(d.details.reason && newString.includes("REASON")) { newString = newString .replace(/{{REASON}}/g, d.details.reason) - } } return newString } diff --git a/libs/extenders.js b/libs/extenders.js index 7cf3595d..0f8fc0fd 100644 --- a/libs/extenders.js +++ b/libs/extenders.js @@ -15,6 +15,34 @@ module.exports = function(s,config){ } } } + s.runExtensionsForArray = (nameOfExtension, nameOfExtensionContainer, args) => { + nameOfExtensionContainer = nameOfExtensionContainer || `${nameOfExtension}Extensions` + const theExtenders = s[nameOfExtensionContainer]; + for(extender of theExtenders){ + extender(...args) + } + } + s.runExtensionsForArrayAwaited = async (nameOfExtension, nameOfExtensionContainer, args) => { + nameOfExtensionContainer = nameOfExtensionContainer || `${nameOfExtension}Extensions` + const theExtenders = s[nameOfExtensionContainer]; + for(extender of theExtenders){ + await extender(...args) + } + } + s.runExtensionsForObject = (nameOfExtension, nameOfExtensionContainer, args) => { + nameOfExtensionContainer = nameOfExtensionContainer || `${nameOfExtension}Extensions` + const theExtenders = s[nameOfExtensionContainer]; + for(extender in theExtenders){ + extender(...args) + } + } + s.runExtensionsForObjectAwaited = async (nameOfExtension, nameOfExtensionContainer, args) => { + nameOfExtensionContainer = nameOfExtensionContainer || `${nameOfExtension}Extensions` + const theExtenders = s[nameOfExtensionContainer]; + for(extender in theExtenders){ + await extender(...args) + } + } ////// USER ////// createExtension(`onSocketAuthentication`) createExtension(`onUserLog`) @@ -59,6 +87,11 @@ module.exports = function(s,config){ createExtension(`onSubscriptionCheck`) createExtension(`onDataPortMessage`) createExtension(`onHttpRequestUpgrade`,null,true) + /////// CRON //////// + createExtension(`onCronGroupProcessed`) + createExtension(`onCronGroupProcessedAwaited`) + createExtension(`onCronGroupBeforeProcessed`) + createExtension(`onCronGroupBeforeProcessedAwaited`) /////// VIDEOS //////// createExtension(`insertCompletedVideoExtender`,`insertCompletedVideoExtensions`) createExtension(`onEventBasedRecordingComplete`) diff --git a/libs/ffmpeg/builders.js b/libs/ffmpeg/builders.js index 905b11b9..01f60fd2 100644 --- a/libs/ffmpeg/builders.js +++ b/libs/ffmpeg/builders.js @@ -180,6 +180,9 @@ module.exports = (s,config,lang) => { break; } } + if(input.hwaccel_format){ + inputFlags.push(`-hwaccel_output_format ${input.hwaccel_format}`) + } } //custom - input flags return `${getInputTypeFlags(input.type)} ${inputFlags.join(' ')} -i "${input.fulladdress}"` @@ -242,7 +245,7 @@ module.exports = (s,config,lang) => { streamFlags.push(`-c:v ${videoCodec === 'libx264' ? 'h264' : videoCodec}`) } if(!videoCodecisCopy || outputRequiresEncoding){ - if(videoWidth && videoHeight)streamFlags.push(`-s ${videoWidth}x${videoHeight}`) + if(videoWidth && videoHeight && !e.details.hwaccel_format) streamFlags.push(`-s ${videoWidth}x${videoHeight}`) if(videoFps && streamType === 'mjpeg' || streamType === 'b64'){ streamFilters.push(`fps=${videoFps}`) } @@ -362,6 +365,9 @@ module.exports = (s,config,lang) => { break; } } + if(e.details.hwaccel_format){ + inputFlags.push(`-hwaccel_output_format ${e.details.hwaccel_format}`) + } } inputFlags.push(`-loglevel ${logLevel}`) //add main input @@ -415,8 +421,13 @@ module.exports = (s,config,lang) => { streamFlags.push(`-an`) } if(videoCodec === 'h264_vaapi'){ - streamFilters.push('format=nv12,hwupload'); + if (!e.details.hwaccel_format) { + streamFilters.push('format=nv12,hwupload'); + } if(e.details.stream_scale_x && e.details.stream_scale_y){ + if (!e.details.hwaccel_format) { + streamFilters.push(',') + } streamFilters.push('scale_vaapi=w='+e.details.stream_scale_x+':h='+e.details.stream_scale_y) } } @@ -426,7 +437,7 @@ module.exports = (s,config,lang) => { if(!outputRequiresEncoding && videoCodec !== 'no'){ streamFlags.push(`-c:v ` + videoCodec) } - if(!videoCodecisCopy || outputRequiresEncoding){ + if((!videoCodecisCopy || outputRequiresEncoding) && !e.details.hwaccel_format){ if(videoWidth && videoHeight)streamFlags.push(`-s ${videoWidth}x${videoHeight}`) if(videoFps && streamType === 'mjpeg' || streamType === 'b64' || videoFps && !videoCodecisCopy){ streamFilters.push(`fps=${videoFps}`) diff --git a/libs/monitor.js b/libs/monitor.js index 233b10ae..b4eb93e1 100644 --- a/libs/monitor.js +++ b/libs/monitor.js @@ -27,8 +27,12 @@ module.exports = function(s,config,lang){ getMonitorConfiguration, copyMonitorConfiguration, checkObjectsInMonitorDetails, - isGroupBelowMaxMonitorCount, } = require('./monitor/utils.js')(s,config,lang) + const { + canAddMoreMonitors, + sanitizeMonitorConfig, + isGroupBelowMaxMonitorCount, + } = require('./checker/utils.js')(s,config,lang) s.initiateMonitorObject = function(e){ if(!s.group[e.ke]){s.group[e.ke]={}}; if(!s.group[e.ke].activeMonitors){s.group[e.ke].activeMonitors={}} @@ -196,7 +200,7 @@ module.exports = function(s,config,lang){ var iconImageFile = streamDir + 'icon.jpg' const snapRawFilters = monitor.details.cust_snap_raw if(snapRawFilters)outputOptions.push(snapRawFilters); - var ffmpegCmd = splitForFFMPEG(`-y -loglevel warning ${isDetectorStream ? '-live_start_index 2' : ''} -re ${inputOptions.join(' ')} -timeout 4000000 -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`) + var ffmpegCmd = splitForFFMPEG(`-y -loglevel warning ${isDetectorStream ? '-live_start_index 2' : ''} -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f mjpeg -an -frames:v 1 "${temporaryImageFile}"`) try{ await fs.promises.mkdir(streamDir, {recursive: true}, (err) => {s.debugLog(err)}) }catch(err){ @@ -546,10 +550,9 @@ module.exports = function(s,config,lang){ var endData = { ok: false } - if(!form.mid){ - endData.msg = lang['No Monitor ID Present in Form'] + if(!form.mid || !s.timeReady){ + endData.msg = !s.timeReady ? lang.notReadyYet : lang['No Monitor ID Present in Form'] if(callback)callback(endData); - resolve(endData) return } form.mid = form.mid.replace(/[^\w\s]/gi,'').replace(/ /g,'') @@ -563,6 +566,9 @@ module.exports = function(s,config,lang){ ] }); const monitorExists = selectResponse.rows && selectResponse.rows[0]; + const systemMax = canAddMoreMonitors(); + const groupMax = isGroupBelowMaxMonitorCount(form.ke); + const canDoTheDo = systemMax && groupMax; var affectMonitor = false var monitorQuery = {} var txData = { @@ -610,7 +616,7 @@ module.exports = function(s,config,lang){ ] }) affectMonitor = true - }else if(isGroupBelowMaxMonitorCount(form.ke)){ + }else if(canDoTheDo){ txData.new = true Object.keys(form).forEach(function(v){ if(form[v] && form[v] !== ''){ @@ -631,7 +637,7 @@ module.exports = function(s,config,lang){ }else{ txData.f = 'monitor_edit_failed' txData.ff = 'max_reached' - endData.msg = user.lang.monitorEditFailedMaxReached + endData.msg = !systemMax ? user.lang.monitorEditFailedMaxReachedUnactivated : user.lang.monitorEditFailedMaxReached } if(affectMonitor === true){ form.details = JSON.parse(form.details) @@ -650,10 +656,12 @@ module.exports = function(s,config,lang){ } s.tx(txData,'GRP_'+form.ke) if(callback)callback(!endData.ok,endData); - let monitorConfig = copyMonitorConfiguration(form.ke,form.mid) - s.onMonitorSaveExtensions.forEach(function(extender){ - extender(monitorConfig,form,endData) - }) + if(monitorExists || canDoTheDo){ + let monitorConfig = copyMonitorConfiguration(form.ke,form.mid) + s.onMonitorSaveExtensions.forEach(function(extender){ + extender(monitorConfig,form,endData) + }) + } return endData } s.camera = async (selectedMode,e,cn) => { diff --git a/libs/monitor/utils.js b/libs/monitor/utils.js index c45a5630..6f3ca3c3 100644 --- a/libs/monitor/utils.js +++ b/libs/monitor/utils.js @@ -219,7 +219,7 @@ module.exports = (s,config,lang) => { }) } const temporaryImageFile = streamDir + s.gid(5) + '.jpg' - const ffmpegCmd = splitForFFMPEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`) + const ffmpegCmd = splitForFFMPEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f mjpeg -an -frames:v 1 "${temporaryImageFile}"`) const snapProcess = spawn('ffmpeg',ffmpegCmd,{detached: true}) snapProcess.stderr.on('data',function(data){ // s.debugLog(data.toString()) @@ -674,6 +674,8 @@ module.exports = (s,config,lang) => { mid: monitorId, }); } + delete(s.group[groupKey].activeMonitors[monitorId]); + delete(s.group[groupKey].rawMonitorConfigurations[monitorId]); response.msg = `${lang.monitorDeleted} ${lang.byUser} : ${userId}` }catch(err){ response.ok = false diff --git a/libs/socketio.js b/libs/socketio.js index 6587397d..0d027c1d 100644 --- a/libs/socketio.js +++ b/libs/socketio.js @@ -568,55 +568,64 @@ module.exports = function(s,config,lang,io){ if(!d.videoEndDate&&d.endDate){ d.videoEndDate = stringToSqlTime(d.endDate) } - var getVideos = function(callback){ - var videoWhereQuery = [ - ['ke','=',cn.ke], - ] - if(d.videoStartDate || d.videoEndDate){ - if(!d.videoStartDateOperator||d.videoStartDateOperator==''){ - d.videoStartDateOperator='>=' - } - if(!d.videoEndDateOperator||d.videoEndDateOperator==''){ - d.videoEndDateOperator='<=' - } - switch(true){ - case(d.videoStartDate && d.videoStartDate !== '' && d.videoEndDate && d.videoEndDate !== ''): - videoWhereQuery.push(['time',d.videoStartDateOperator,d.videoStartDate]) - videoWhereQuery.push(['end',d.videoEndDateOperator,d.videoEndDate]) - break; - case(d.videoStartDate && d.videoStartDate !== ''): - videoWhereQuery.push(['time',d.videoStartDateOperator,d.videoStartDate]) - break; - case(d.videoEndDate && d.videoEndDate !== ''): - videoWhereQuery.push(['end',d.videoEndDateOperator,d.videoEndDate]) - break; - } - } - if(monitorRestrictions.length > 0){ - videoWhereQuery.push(monitorRestrictions) - } - s.knexQuery({ - action: "select", - columns: "*", - table: videoSet === 'cloud' ? `Cloud Videos` : "Videos", - where: videoWhereQuery, - orderBy: ['time','desc'], - limit: d.videoLimit || '100' - },(err,r) => { - if(err){ - console.error(err) - setTimeout(function(){ - callback({total:0,limit:d.videoLimit,videos:[]}) - },2000) - }else{ - s.buildVideoLinks(r,{ - videoParam : videoSet === 'cloud' ? `cloudVideos` : "videos", - auth : cn.auth - }) - callback({total:r.length,limit:d.videoLimit,videos:r}) - } - }) - } + var getVideos = function(callback) { + var videoWhereQuery = [ + ['ke','=',cn.ke], + ]; + + // Add filtering logic here (startDate, endDate, etc.) + if(d.videoStartDate || d.videoEndDate) { + if(!d.videoStartDateOperator || d.videoStartDateOperator == '') { + d.videoStartDateOperator = '>=' + } + if(!d.videoEndDateOperator || d.videoEndDateOperator == '') { + d.videoEndDateOperator = '<=' + } + switch(true) { + case(d.videoStartDate && d.videoStartDate !== '' && d.videoEndDate && d.videoEndDate !== ''): + videoWhereQuery.push(['time', d.videoStartDateOperator, d.videoStartDate]) + videoWhereQuery.push(['end', d.videoEndDateOperator, d.videoEndDate]) + break; + case(d.videoStartDate && d.videoStartDate !== ''): + videoWhereQuery.push(['time', d.videoStartDateOperator, d.videoStartDate]) + break; + case(d.videoEndDate && d.videoEndDate !== ''): + videoWhereQuery.push(['end', d.videoEndDateOperator, d.videoEndDate]) + break; + } + } + if(monitorRestrictions.length > 0) { + videoWhereQuery.push(monitorRestrictions) + } + + // Implementing pagination + var pageSize = parseInt(d.pageSize) || 10; // Default page size + var currentPage = parseInt(d.currentPage) || 1; // Default to page 1 + var offset = (currentPage - 1) * pageSize; + + s.knexQuery({ + action: "select", + columns: "*", + table: videoSet === 'cloud' ? `Cloud Videos` : "Videos", + where: videoWhereQuery, + orderBy: ['time','desc'], + limit: pageSize, // Limiting the number of rows returned + offset: offset // Skipping the previous pages' rows + },(err,r) => { + if(err) { + console.log(err) + setTimeout(function(){ + callback({total:0, limit:pageSize, videos:[]}) + },2000) + } else { + s.buildVideoLinks(r,{ + videoParam: videoSet === 'cloud' ? `cloudVideos` : "videos", + auth: cn.auth + }) + callback({total: r.length, limit: pageSize, videos: r}) + } + }) + } getVideos(function(videos){ getEvents(function(events){ tx({ diff --git a/libs/uploaders/amazonS3.js b/libs/uploaders/amazonS3.js index 95dd6497..e261c157 100644 --- a/libs/uploaders/amazonS3.js +++ b/libs/uploaders/amazonS3.js @@ -1,7 +1,7 @@ // https://us-east-1.console.aws.amazon.com/iamv2/home#/users const fs = require('fs'); -const { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3"); +const { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand, StorageClass} = require("@aws-sdk/client-s3"); module.exports = function(s,config,lang){ const genericRequest = async (groupKey,requestOptions) => { @@ -123,7 +123,8 @@ module.exports = function(s,config,lang){ Bucket: s.group[groupKey].init.aws_s3_bucket, Key: saveLocation, Body: fileStream, - ContentType: 'video/'+e.ext + ContentType: 'video/'+e.ext, + StorageClass: s.group[groupKey].init.aws_storage_class || StorageClass.STANDARD }).then((response) => { if(response.err){ s.userLog(e,{type:lang['Amazon S3 Upload Error'],msg:response.err}) @@ -433,6 +434,17 @@ module.exports = function(s,config,lang){ } ] }, + { + "hidden": true, + "name": "detail=aws_storage_class", + "field": lang['Storage Class'], + "fieldType": "select", + "form-group-class": "autosave_aws_s3_input autosave_aws_s3_1", + "description": "The storage class of the uploaded objects see https://aws.amazon.com/s3/storage-classes/", + "default": StorageClass.STANDARD, + "example": StorageClass.STANDARD, + "possible": Object.keys(StorageClass).map(k => ({name: k, value: k})), + }, { "hidden": true, "name": "detail=aws_s3_log", diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js index a0d9186a..217e83cd 100644 --- a/libs/webServerPaths.js +++ b/libs/webServerPaths.js @@ -787,14 +787,28 @@ module.exports = function(s,config,lang,app,io){ return } const cannotSeeImportantSettings = (isRestrictedApiKey && apiKeyPermissions.edit_monitors_disallowed) || userPermissions.monitor_create_disallowed; + const whereQuery = [ + ['ke','=',groupKey], + monitorRestrictions + ]; + if(!!req.query.search){ + const searchQuery = req.query.search.split(','); + const whereQuerySearch = [] + for(item of searchQuery){ + if(item){ + whereQuerySearch.push( + whereQuerySearch.length === 0 ? ['name','LIKE',`%${item.trim()}%`] : ['or', 'name','LIKE',`%${item}%`], + ['or','mid','LIKE',`%${item.trim()}%`] + ); + } + } + whereQuery.push(whereQuerySearch) + } s.knexQuery({ action: "select", columns: "*", table: "Monitors", - where: [ - ['ke','=',groupKey], - monitorRestrictions - ] + where: whereQuery },(err,r) => { r.forEach(function(v,n){ const monitorId = v.mid; diff --git a/web/assets/js/bs5.extenders.js b/web/assets/js/bs5.extenders.js index b1bdc647..3ef7f5d7 100644 --- a/web/assets/js/bs5.extenders.js +++ b/web/assets/js/bs5.extenders.js @@ -1,3 +1,18 @@ +const on = {}; +const dashboardExtensions = {}; +async function addExtender(extenderContainer){ + dashboardExtensions[extenderContainer] = []; + on[extenderContainer] = function(...extender){ + dashboardExtensions[extenderContainer].push(...extender) + }; +} +async function executeExtender(extenderContainer, args){ + console.log('Running', extenderContainer) + for(extender of dashboardExtensions[extenderContainer]){ + await extender(...args) + } +} + var accountSettings = { onLoadFieldsExtensions: [], onLoadFields: function(...extender){ diff --git a/web/assets/js/bs5.liveGrid.cycle.js b/web/assets/js/bs5.liveGrid.cycle.js index 109727c9..e69de29b 100644 --- a/web/assets/js/bs5.liveGrid.cycle.js +++ b/web/assets/js/bs5.liveGrid.cycle.js @@ -1,163 +0,0 @@ -var liveGridCycleTimer = null; -var cycleLiveOptionsBefore = null; -var cycleLiveOptions = null; -var cycleLiveMoveNext = function(){} -var cycleLiveMovePrev = function(){} -var cycleLiveFullList = null -var cycleLiveCurrentPart = null -function getListOfMonitorsToCycleLive(chosenTags,useMonitorIds){ - var monitors = [] - if(useMonitorIds){ - monitors = getMonitorsFromIds(chosenTags) - }else if(chosenTags){ - var tags = sanitizeTagList(chosenTags) - monitors = getMonitorsFromTags(tags) - }else{ - monitors = getRunningMonitors(true) - } - return monitors; -} -function getPartForCycleLive(fullList, afterMonitorId, numberOfMonitors) { - const startIndex = afterMonitorId ? fullList.findIndex(monitor => monitor.mid === afterMonitorId) : -1; - const result = []; - for (let i = 1; i <= numberOfMonitors; i++) { - const index = (startIndex + i) % fullList.length; - result.push(fullList[index]); - } - return result; -} -function displayCycleSetOnLiveGrid(monitorsList){ - cycleLiveCurrentPart = [].concat(monitorsList) - closeAllLiveGridPlayers() - monitorsWatchOnLiveGrid(monitorsList.map(monitor => monitor.mid)) -} -// rotator -function stopCycleLive(){ - clearTimeout(liveGridCycleTimer) - liveGridCycleTimer = null -} -function resumeCycleLive(fullList,partForCycle,numberOfMonitors){ - const theLocalStorage = dashboardOptions() - const cycleLiveTimerAmount = parseInt(theLocalStorage.cycleLiveTimerAmount) || 30000 - function next(){ - var afterMonitorId = partForCycle.slice(-1)[0].mid; - partForCycle = getPartForCycleLive(fullList,afterMonitorId,numberOfMonitors) - displayCycleSetOnLiveGrid(partForCycle) - reset() - } - function prev(){ - var firstInPart = partForCycle[0].mid; - var firstPartIndex = fullList.findIndex(monitor => monitor.mid === firstInPart) - var backedToIndex = (firstPartIndex - (numberOfMonitors + 1) + fullList.length) % fullList.length; - var beforeMonitorId = fullList[backedToIndex].mid - partForCycle = getPartForCycleLive(fullList,beforeMonitorId,numberOfMonitors, true) - displayCycleSetOnLiveGrid(partForCycle) - reset() - } - function reset(){ - clearTimeout(liveGridCycleTimer) - liveGridCycleTimer = setTimeout(function(){ - next() - },cycleLiveTimerAmount) - } - reset() - cycleLiveMoveNext = next - cycleLiveMovePrev = prev -} -function beginCycleLive({ - chosenTags, - useMonitorIds, - numberOfMonitors, - monitorHeight, -}){ - var fullList = getListOfMonitorsToCycleLive(chosenTags,useMonitorIds) - var partForCycle = getPartForCycleLive(fullList,null,numberOfMonitors) - cycleLiveFullList = [].concat(fullList) - displayCycleSetOnLiveGrid(partForCycle) - stopCycleLive() - resumeCycleLive(fullList,partForCycle,numberOfMonitors) -} -dashboardSwitchCallbacks.cycleLiveGrid = function(toggleState){ - if(toggleState !== 1){ - cycleLiveOptions = null - cycleLiveOptionsBefore = null - stopCycleLive() - }else{ - openTab('liveGrid') - cycleLiveOptionsBefore = cycleLiveOptions ? Object.assign({},cycleLiveOptions) : null - const theLocalStorage = dashboardOptions() - const cycleLivePerRow = parseInt(theLocalStorage.cycleLivePerRow) || 2 - const cycleLiveNumberOfMonitors = parseInt(theLocalStorage.cycleLiveNumberOfMonitors) || 4 - const cycleLiveMonitorHeight = parseInt(theLocalStorage.cycleLiveMonitorHeight) || 4 - cycleLiveOptions = { - chosenTags: null, - useMonitorIds: null, - monitorsPerRow: cycleLivePerRow, - numberOfMonitors: cycleLiveNumberOfMonitors, - monitorHeight: cycleLiveMonitorHeight, - } - beginCycleLive(cycleLiveOptions) - } -} -function keyShortcutsForCycleLive(enable) { - function cleanup(){ - document.removeEventListener('keydown', keyShortcuts['cycleLive'].keydown); - document.removeEventListener('keyup', keyShortcuts['cycleLive'].keyup); - delete(keyShortcuts['cycleLive']) - } - if(enable){ - let isKeyPressed = false; - function handleKeyboard(event){ - if (isKeyPressed) { - return; - } - event.preventDefault(); - switch(event.code){ - case 'Space': - isKeyPressed = true; - if(liveGridCycleTimer){ - stopCycleLive() - }else{ - resumeCycleLive( - cycleLiveFullList, - cycleLiveCurrentPart, - cycleLiveOptions.numberOfMonitors - ) - } - break; - case 'ArrowLeft': - isKeyPressed = true; - cycleLiveMovePrev(); - break; - case 'ArrowRight': - isKeyPressed = true; - cycleLiveMoveNext(); - break; - } - } - function handleKeyup(event) { - isKeyPressed = false; - } - keyShortcuts['cycleLive'] = { - keydown: handleKeyboard, - keyup: handleKeyup, - } - document.addEventListener('keydown', keyShortcuts['cycleLive'].keydown); - document.addEventListener('keyup', keyShortcuts['cycleLive'].keyup); - }else{ - cleanup() - } -} -addOnTabOpen('liveGrid', function () { - keyShortcutsForCycleLive(true) -}) -addOnTabReopen('liveGrid', function () { - if(cycleLiveOptions){ - beginCycleLive(cycleLiveOptions) - } - keyShortcutsForCycleLive(true) -}) -addOnTabAway('liveGrid', function () { - stopCycleLive() - keyShortcutsForCycleLive(false) -}) diff --git a/web/assets/js/bs5.liveGrid.js b/web/assets/js/bs5.liveGrid.js index d1eaca83..929bbd72 100644 --- a/web/assets/js/bs5.liveGrid.js +++ b/web/assets/js/bs5.liveGrid.js @@ -656,6 +656,11 @@ function closeLiveGridPlayer(monitorId,killElement){ console.log(err) } } +function closeLiveGridPlayers(monitors, killElement){ + $.each(monitors,function(n,v){ + monitorWatchOnLiveGrid(v.mid, killElement) + }) +} function monitorWatchOnLiveGrid(monitorId, watchOff){ return mainSocket.f({f:'monitor',ff:watchOff ? 'watch_off' : 'watch_on',id: monitorId}) } @@ -664,13 +669,20 @@ function monitorsWatchOnLiveGrid(monitorIds, watchOff){ monitorWatchOnLiveGrid(monitorId, watchOff) }) } -function callMonitorToLiveGrid(v){ +function callMonitorToLiveGrid(v, justTry){ var watchedOn = dashboardOptions().watch_on || {} - if(watchedOn[v.ke] && watchedOn[v.ke][v.mid] === 1 && loadedMonitors[v.mid] && loadedMonitors[v.mid].mode !== 'stop'){ + if(justTry || watchedOn[v.ke] && watchedOn[v.ke][v.mid] === 1 && loadedMonitors[v.mid] && loadedMonitors[v.mid].mode !== 'stop'){ mainSocket.f({f:'monitor',ff:'watch_on',id:v.mid}) if(tabTree.name !== 'monitorSettings')openLiveGrid() + console.log('loaded',v.name) } } +function callMonitorsToLiveGrid(monitors, justTry){ + $.each(monitors,function(n,v){ + console.log('loading',v.name) + callMonitorToLiveGrid(v, justTry) + }) +} function loadPreviouslyOpenedLiveGridBlocks(){ $.getJSON(getApiPrefix(`monitor`),function(data){ $.each(data,function(n,v){ @@ -1279,8 +1291,8 @@ $(document).ready(function(e){ var monitorId = d.mid || d.id var loadedMonitor = loadedMonitors[monitorId] var subStreamChannel = d.subStreamChannel - var monitorsPerRow = cycleLiveOptions ? cycleLiveOptions.monitorsPerRow : null; - var monitorHeight = cycleLiveOptions ? cycleLiveOptions.monitorHeight : null; + var monitorsPerRow = null; + var monitorHeight = null; if(!loadedMonitor.subStreamChannel && loadedMonitor.details.stream_type === 'useSubstream'){ toggleSubStream(monitorId,function(){ drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel,monitorsPerRow,monitorHeight) @@ -1428,4 +1440,9 @@ $(document).ready(function(e){ }) } dashboardSwitchCallbacks.jpegMode = toggleJpegMode + window.openLiveGrid = openLiveGrid; + window.callMonitorToLiveGrid = callMonitorToLiveGrid; + window.monitorsWatchOnLiveGrid = monitorsWatchOnLiveGrid; + window.closeAllLiveGridPlayers = closeAllLiveGridPlayers; + window.closeLiveGridPlayers = closeLiveGridPlayers; }) diff --git a/web/assets/js/bs5.liveGrid.keyboard.js b/web/assets/js/bs5.liveGrid.keyboard.js index 6c32ee6d..e02e0a6b 100644 --- a/web/assets/js/bs5.liveGrid.keyboard.js +++ b/web/assets/js/bs5.liveGrid.keyboard.js @@ -1,41 +1,41 @@ -function keyShortcutsForLiveGridUtils(enable) { - function cleanup(){ - document.removeEventListener('keydown', keyShortcuts['liveGridUtils'].keydown); - document.removeEventListener('keyup', keyShortcuts['liveGridUtils'].keyup); - delete(keyShortcuts['liveGridUtils']) - } - if(enable){ - let isKeyPressed = false; - function handleKeyboard(event){ - if (isKeyPressed) { - return; - } - event.preventDefault(); - switch(event.code){ - case 'Enter': - addMarkAsEventToAllOpenMonitors() - break; - } - } - function handleKeyup(event) { - isKeyPressed = false; - } - keyShortcuts['liveGridUtils'] = { - keydown: handleKeyboard, - keyup: handleKeyup, - } - document.addEventListener('keydown', keyShortcuts['liveGridUtils'].keydown); - document.addEventListener('keyup', keyShortcuts['liveGridUtils'].keyup); - }else{ - cleanup() - } -} -addOnTabOpen('liveGrid', function () { - keyShortcutsForLiveGridUtils(true) -}) -addOnTabReopen('liveGrid', function () { - keyShortcutsForLiveGridUtils(true) -}) -addOnTabAway('liveGrid', function () { - keyShortcutsForLiveGridUtils(false) -}) +// function keyShortcutsForLiveGridUtils(enable) { +// function cleanup(){ +// document.removeEventListener('keydown', keyShortcuts['liveGridUtils'].keydown); +// document.removeEventListener('keyup', keyShortcuts['liveGridUtils'].keyup); +// delete(keyShortcuts['liveGridUtils']) +// } +// if(enable){ +// let isKeyPressed = false; +// function handleKeyboard(event){ +// if (isKeyPressed) { +// return; +// } +// event.preventDefault(); +// switch(event.code){ +// case 'Enter': +// addMarkAsEventToAllOpenMonitors() +// break; +// } +// } +// function handleKeyup(event) { +// isKeyPressed = false; +// } +// keyShortcuts['liveGridUtils'] = { +// keydown: handleKeyboard, +// keyup: handleKeyup, +// } +// document.addEventListener('keydown', keyShortcuts['liveGridUtils'].keydown); +// document.addEventListener('keyup', keyShortcuts['liveGridUtils'].keyup); +// }else{ +// cleanup() +// } +// } +// addOnTabOpen('liveGrid', function () { +// keyShortcutsForLiveGridUtils(true) +// }) +// addOnTabReopen('liveGrid', function () { +// keyShortcutsForLiveGridUtils(true) +// }) +// addOnTabAway('liveGrid', function () { +// keyShortcutsForLiveGridUtils(false) +// }) diff --git a/web/assets/js/bs5.monitorMap.js b/web/assets/js/bs5.monitorMap.js index df857eb5..86b879c4 100644 --- a/web/assets/js/bs5.monitorMap.js +++ b/web/assets/js/bs5.monitorMap.js @@ -1,7 +1,7 @@ +var loadedMap; $(document).ready(function(){ var theBlock = $('#tab-monitorMap') var theMap = $('#monitor-map-canvas') - var loadedMap; function loadPopupVideoList(monitor){ var groupKey = monitor.ke var monitorId = monitor.mid diff --git a/web/assets/js/bs5.monitorSettings.js b/web/assets/js/bs5.monitorSettings.js index 5ec616c6..31d220d8 100644 --- a/web/assets/js/bs5.monitorSettings.js +++ b/web/assets/js/bs5.monitorSettings.js @@ -62,6 +62,7 @@ function generateDefaultMonitorSettings(){ "hwaccel": "auto", "hwaccel_vcodec": "", "hwaccel_device": "", + "hwaccel_format": "", "use_coprocessor": null, "stream_type": "hls", "stream_flv_type": "http", @@ -274,6 +275,7 @@ function generateDefaultMonitorSettings(){ "hwaccel": null, "hwaccel_vcodec": "", "hwaccel_device": "", + "hwaccel_format": "", "cust_input": "" }, "output": { @@ -412,6 +414,7 @@ var copyMonitorSettingsToSelected = function(monitorConfig){ monitor.details.hwaccel = monitorDetails.hwaccel monitor.details.hwaccel_vcodec = monitorDetails.hwaccel_vcodec monitor.details.hwaccel_device = monitorDetails.hwaccel_device + monitor.details.hwaccel_format = monitorDetails.hwaccel_format }else{ monitor = Object.assign({},loadedMonitors[id]); } diff --git a/web/assets/js/bs5.monitorSettings.monitorMap.js b/web/assets/js/bs5.monitorSettings.monitorMap.js index 3733b8dd..26fa627d 100644 --- a/web/assets/js/bs5.monitorSettings.monitorMap.js +++ b/web/assets/js/bs5.monitorSettings.monitorMap.js @@ -5,7 +5,7 @@ $(document).ready(function(e){ var monitorSettingsMapOptionsEl = $('#monitor-settings-geolocation-options') var monitorSettingsMapOptionsElOptions = monitorSettingsMapOptionsEl.find('[map-option]') var editorForm = monitorEditorWindow.find('form') - var loadedMap; + var mapInWindow; var monitorMapMarker; var monitorMapMarkerFov; function setAdditionalControls(options){ @@ -34,14 +34,14 @@ $(document).ready(function(e){ fov, range, } = getGeolocationParts(geoString || monitor.details.geolocation); - loadedMap = L.map('monitor-settings-monitor-map').setView([lat, lng], zoom); + mapInWindow = L.map('monitor-settings-monitor-map').setView([lat, lng], zoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, - }).addTo(loadedMap); + }).addTo(mapInWindow); monitorMapMarker = L.marker([lat, lng], { title: monitor ? `${monitor.name} (${monitor.host})` : null, draggable: true, - }).addTo(loadedMap); + }).addTo(mapInWindow); monitorMapMarker.on('dragend', function(){ setGeolocationFieldValue() }); @@ -49,7 +49,7 @@ $(document).ready(function(e){ var markerDetails = getMapMarkerDetails(); setMapMarkerFov(monitorMapMarkerFov,markerDetails) }); - loadedMap.on('zoomend', function(){ + mapInWindow.on('zoomend', function(){ setGeolocationFieldValue() }); setAdditionalControls({ @@ -57,7 +57,7 @@ $(document).ready(function(e){ fov, range, }) - monitorMapMarkerFov = drawMapMarkerFov(loadedMap,{ + monitorMapMarkerFov = drawMapMarkerFov(mapInWindow,{ lat, lng, direction, @@ -71,8 +71,8 @@ $(document).ready(function(e){ }) } function unloadMap(){ - loadedMap.remove(); - loadedMap = null; + mapInWindow.remove(); + mapInWindow = null; } function getMapOptions(){ var options = {} @@ -86,7 +86,7 @@ $(document).ready(function(e){ } function getMapMarkerDetails(){ var pos = monitorMapMarker.getLatLng() - var zoom = loadedMap.getZoom(); + var zoom = mapInWindow.getZoom(); var { direction, fov, diff --git a/web/assets/js/bs5.timeline.js b/web/assets/js/bs5.timeline.js index d9456fb3..9c7073bd 100644 --- a/web/assets/js/bs5.timeline.js +++ b/web/assets/js/bs5.timeline.js @@ -110,7 +110,7 @@ $(document).ready(function(){ async function loopOnGaps(monitorId){ for (let i = 0; i < gaps.length; i++) { var range = gaps[i] - videos.push(...(await getVideos({ + var videosFound = (await getVideos({ monitorId, startDate: range[0], endDate: range[1], @@ -118,7 +118,9 @@ $(document).ready(function(){ searchQuery, // archived: false, // customVideoSet: wantCloudVideo ? 'cloudVideos' : null, - },null,dontShowDetectionOnTimeline)).videos); + },null,dontShowDetectionOnTimeline)).videos; + videos.push(...videosFound); + executeExtender('timelineGetVideosByMonitor', [monitorId, videosFound]) } } if(monitorIds && monitorIds.length > 0){ @@ -148,6 +150,7 @@ $(document).ready(function(){ setLoadingMask(true) timeStripListOfQueries.push(...gaps) var videos = await getVideosInGaps(gaps,timeStripSelectedMonitors) + executeExtender('timelineGetVideos', [videos]) videos = addVideoBeforeAndAfter(videos) loadedVideosOnTimeStrip.push(...videos) if(currentVideosLength !== loadedVideosOnTimeStrip.length)addTimelineItems(loadedVideosOnTimeStrip); @@ -293,6 +296,7 @@ $(document).ready(function(){ timeStripActionWithPausePlay().then((timeChanging) => { if(!timeChanging){ resetTimeline(clickTime) + executeExtender('timelineTimeChange', [clickTime]) } }) }); @@ -313,6 +317,7 @@ $(document).ready(function(){ setTimeout(() => { timeChanging = false getAndDrawVideosToTimeline(clickTime) + executeExtender('timelineRangeChanged', [properties.start, properties.end, getTimestripDate()]) },500) },300) }) @@ -641,6 +646,7 @@ $(document).ready(function(){ addition += (msSpeed * timelineSpeed); newTime = new Date(currentDate + addition) setTickDate(newTime); + executeExtender('timelineTimeChange', [newTime]) // setTimeOfCanvasVideos(newTime) }, msSpeed) timeStripVisTickMovementIntervalSecond = setInterval(function() { @@ -911,9 +917,25 @@ $(document).ready(function(){ onSelectedMonitorChange() refreshTimeline() } + window.resetTimelineWithMonitors = function(monitorIds, start = new Date(), end = new Date(), tickTime){ + setTimeout(() => { + timeStripSelectedMonitors = monitorIds || []; + onSelectedMonitorChange() + setLoadingMask(true) + dateRangeChanging = true + setTimestripDate(start, end) + setTimeout(() => { + dateRangeChanging = false + refreshTimeline() + var newTickPosition = tickTime || getTimeBetween(start,end,50); + setTickDate(newTickPosition) + },2000) + },1000) + openTab('timeline') + } function refreshTimelineOnAgree(){ var askToLoad = isAllMonitorsSelected(50) - if(askToLoad){ + if(!window.skipTimelineAgree && askToLoad){ $.confirm.create({ title: lang.tooManyMonitorsSelected, body: lang.performanceMayBeAffected, @@ -931,6 +953,7 @@ $(document).ready(function(){ }else{ refreshTimeline() } + window.skipTimelineAgree = false; } function monitorSelectorController(){ var el = $(this) @@ -1072,4 +1095,8 @@ $(document).ready(function(){ if(currentOptions.dontShowDetectionOnTimeline === '1'){ timeStripDontShowDetectionToggle() } + addExtender('timelineTimeChange') + addExtender('timelineRangeChanged') + addExtender('timelineGetVideos') + addExtender('timelineGetVideosByMonitor') }) diff --git a/web/assets/js/bs5.videos.js b/web/assets/js/bs5.videos.js index a91a31a6..95be4765 100644 --- a/web/assets/js/bs5.videos.js +++ b/web/assets/js/bs5.videos.js @@ -79,35 +79,69 @@ function createVideoLinks(video,options){ video.details = details return video } -function applyDataListToVideos(videos,events,keyName,reverseList){ - var updatedVideos = videos.concat([]) - var currentEvents = events.concat([]) - updatedVideos.forEach(function(video){ - var videoEvents = [] - currentEvents.forEach(function(theEvent,index){ - var startTime = new Date(video.time) - var endTime = new Date(video.end) - var eventTime = new Date(theEvent.time) - if(theEvent.mid === video.mid && eventTime >= startTime && eventTime <= endTime){ - videoEvents.push(theEvent) - currentEvents.splice(index, 1) - } - }) - if(reverseList)videoEvents = videoEvents.reverse() - video[keyName || 'events'] = videoEvents - }) - return updatedVideos +function applyDataListToVideos(videos, events, keyName, reverseList) { + const eventMap = new Map(); + + // Build a map of events by monitor ID + events.forEach(event => { + if (!eventMap.has(event.mid)) { + eventMap.set(event.mid, []); + } + eventMap.get(event.mid).push(event); + }); + + // Attach events to videos + videos.forEach(video => { + const videoEvents = eventMap.get(video.mid) || []; + const matchedEvents = videoEvents.filter(event => { + const startTime = new Date(video.time); + const endTime = new Date(video.end); + const eventTime = new Date(event.time); + return eventTime >= startTime && eventTime <= endTime; + }); + + if (reverseList) matchedEvents.reverse(); + + video[keyName || 'events'] = matchedEvents; + }); + + return videos; } -function applyTimelapseFramesListToVideos(videos,events,keyName,reverseList){ - var thisApiPrefix = getApiPrefix() + '/timelapse/' + $user.ke + '/' - var newVideos = applyDataListToVideos(videos,events,keyName,reverseList) - newVideos.forEach(function(video){ - video.timelapseFrames.forEach(function(row){ - var apiURL = thisApiPrefix + row.mid - row.href = libURL + apiURL + '/' + row.filename.split('T')[0] + '/' + row.filename - }) - }) - return newVideos +function applyTimelapseFramesListToVideos(videos, events, keyName, reverseList) { + const thisApiPrefix = `${getApiPrefix()}/timelapse/${$user.ke}/`; + const eventMap = new Map(); + + // Build a map of events by monitor ID + events.forEach(event => { + if (!eventMap.has(event.mid)) { + eventMap.set(event.mid, []); + } + eventMap.get(event.mid).push(event); + }); + + // Attach timelapse frames to videos + videos.forEach(video => { + const videoEvents = eventMap.get(video.mid) || []; + const matchedEvents = videoEvents.filter(event => { + const startTime = new Date(video.time); + const endTime = new Date(video.end); + const eventTime = new Date(event.time); + return eventTime >= startTime && eventTime <= endTime; + }); + + if (reverseList) matchedEvents.reverse(); + + // Assigning matched events to video + video[keyName || 'timelapseFrames'] = matchedEvents.map(row => { + const apiURL = `${thisApiPrefix}${row.mid}`; + return { + ...row, + href: `${libURL}${apiURL}/${row.filename.split('T')[0]}/${row.filename}` + }; + }); + }); + + return videos; } function getFrameOnVideoRow(percentageInward, video) { var startTime = video.time; diff --git a/web/assets/js/bs5.videosTable.js b/web/assets/js/bs5.videosTable.js index c803b936..112cb21b 100644 --- a/web/assets/js/bs5.videosTable.js +++ b/web/assets/js/bs5.videosTable.js @@ -26,16 +26,20 @@ $(document).ready(function(e){ return href } } - function loadFramesForVideosInView(){ - videosTableDrawArea.find('.video-thumbnail').each(async (n,imgEl) => { - const el = $(imgEl) - const monitorId = el.attr('data-mid') - const startDate = el.attr('data-time') - const endDate = el.attr('data-end') - const imgBlock = el.find('.video-thumbnail-img-block') - const href = await getSnapshotFromVideoTimeFrame(monitorId,startDate,endDate) - imgBlock.find('img').attr('src',href) - }) + //Lazy Load Thumbnails + function loadFramesForVideosInView() { + videosTableDrawArea.find('.video-thumbnail').each(async (n, imgEl) => { + const el = $(imgEl); + const monitorId = el.attr('data-mid'); + const startDate = el.attr('data-time'); + const endDate = el.attr('data-end'); + const imgBlock = el.find('.video-thumbnail-img-block'); + + if (el.is(':visible')) { // Only load if visible + const href = await getSnapshotFromVideoTimeFrame(monitorId, startDate, endDate); + imgBlock.find('img').attr('src', href); + } + }); } window.openVideosTableView = function(monitorId){ drawMonitorListToSelector(monitorsList,null,null,true) @@ -45,143 +49,169 @@ $(document).ready(function(e){ } loadDateRangePicker(dateSelector,{ onChange: function(start, end, label) { + videosTableDrawArea.bootstrapTable('destroy'); drawVideosTableViewElements() } }) - monitorsList.change(function(){ - drawVideosTableViewElements() - }) - objectTagSearchField.change(function(){ - drawVideosTableViewElements() - }) - cloudVideoCheckSwitch.change(function(){ - drawVideosTableViewElements() - }) - async function drawVideosTableViewElements(usePreloadedData){ - var dateRange = getSelectedTime(dateSelector) - var searchQuery = objectTagSearchField.val() || null - var startDate = dateRange.startDate - var endDate = dateRange.endDate - var monitorId = monitorsList.val() - var wantsArchivedVideo = getVideoSetSelected() === 'archive' - var wantCloudVideo = wantCloudVideos() - var frameIconsHtml = '' - if(!usePreloadedData){ - loadedVideosTable = (await getVideos({ - monitorId, - startDate, - endDate, - searchQuery, - archived: wantsArchivedVideo, - customVideoSet: wantCloudVideo ? 'cloudVideos' : null, - })).videos; - $.each(loadedVideosTable,function(n,v){ - loadedVideosInMemory[`${monitorId}${v.time}${v.type}`] - }) - } - // for (let i = 0; i < loadedVideosTable.length; i++) { - // const file = loadedVideosTable[i] - // const frameUrl = await getSnapshotFromVideoTimeFrame(file.mid,file.time,file.end); - // file.frameUrl = frameUrl - // } - videosTableDrawArea.bootstrapTable('destroy') - videosTableDrawArea.bootstrapTable({ - onPostBody: loadFramesForVideosInView, - onPageChange: () => { - setTimeout(() => { - loadFramesForVideosInView() - },500) + function debounce(func, wait) { + let timeout; + return function(...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; + } + + monitorsList.change(debounce(function(){ + videosTableDrawArea.bootstrapTable('destroy'); + drawVideosTableViewElements(); + }, 300)); + + objectTagSearchField.change(debounce(function(){ + videosTableDrawArea.bootstrapTable('destroy'); + drawVideosTableViewElements(); + }, 300)); + + cloudVideoCheckSwitch.change(debounce(function(){ + videosTableDrawArea.bootstrapTable('destroy'); + drawVideosTableViewElements(); + }, 300)); + + // Event listener for the Refresh link styled as a button + $('.refresh-data').click(function(e) { + e.preventDefault(); + videosTableDrawArea.bootstrapTable('destroy'); + drawVideosTableViewElements(); + }); + + async function drawVideosTableViewElements(pageNumber, pageSize, usePreloadedData) { + // Set default values if pageNumber and pageSize are not provided + pageNumber = pageNumber || 1; + pageSize = pageSize || 10; + + var dateRange = getSelectedTime(dateSelector); + var searchQuery = objectTagSearchField.val() || null; + var startDate = dateRange.startDate; + var endDate = dateRange.endDate; + var monitorId = monitorsList.val(); + var wantsArchivedVideo = getVideoSetSelected() === 'archive'; + var wantCloudVideo = wantCloudVideos(); + + if (!usePreloadedData) { + var result = await getVideos({ + monitorId, + startDate, + endDate, + searchQuery, + archived: wantsArchivedVideo, + customVideoSet: wantCloudVideo ? 'cloudVideos' : null, + pageSize: pageSize, // Pass pageSize to server + currentPage: pageNumber // Pass currentPage to server + }); + loadedVideosTable = result.videos; + } + + //videosTableDrawArea.bootstrapTable('destroy'); + videosTableDrawArea.bootstrapTable({ + pagination: true, + search: true, + pageList: [10, 25, 50, 100, 1000, 2000], + pageSize: pageSize, // Ensure the current page size is maintained + pageNumber: pageNumber, // Ensure the current page number is maintained + totalRows: result.total, // Reflect total number of videos + onPostBody: loadFramesForVideosInView, + onPageChange: (newPageNumber, newPageSize) => { + drawVideosTableViewElements(newPageNumber, newPageSize); + setTimeout(() => { + loadFramesForVideosInView(); + }, 500); + }, + columns: [ + { + field: 'mid', + title: '', + checkbox: true, + formatter: () => { + return { + checked: false + }; + }, }, - pagination: true, - search: true, - pageList: [10, 25, 50, 100, 1000, 2000], - columns: [ - { - field: 'mid', - title: '', - checkbox: true, - formatter: () => { - return { - checked: false - } - }, - }, - { - field: 'image', - title: '', - }, - { - field: 'Monitor', - title: '', - }, - { - field: 'time', - title: lang['Time'], - }, - { - field: 'objects', - title: lang['Objects Found'] - }, - { - field: 'tags', - title: '' - }, - { - field: 'size', - title: '' - }, - { - field: 'buttons', - title: '' - } - ], - data: loadedVideosTable.map((file) => { - var isLocalVideo = !wantCloudVideo - var href = file.href + `${!isLocalVideo ? `?type=${file.type}` : ''}` - var loadedMonitor = loadedMonitors[file.mid] - return { - image: `
-
- -
-
- - - - - - -
-
`, - Monitor: loadedMonitor && loadedMonitor.name ? loadedMonitor.name : file.mid, - mid: file.mid, - time: ` -
${timeAgo(file.time)}
-
${lang.Start} : ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}
-
${lang.End} : ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}
`, - objects: file.objects, - tags: ` - ${file.ext ? `${file.ext}` : ''} - ${!isLocalVideo ? `${file.type}` : ''} - `, - size: convertKbToHumanSize(file.size), - buttons: ` -
- - ${isLocalVideo && permissionCheck('video_delete',file.mid) ? `` : ''} - + { + field: 'image', + title: '', + }, + { + field: 'Monitor', + title: '', + }, + { + field: 'time', + title: lang['Time'], + }, + { + field: 'objects', + title: lang['Objects Found'] + }, + { + field: 'tags', + title: '' + }, + { + field: 'size', + title: '' + }, + { + field: 'buttons', + title: '' + } + ], + data: loadedVideosTable.map((file) => { + var isLocalVideo = !wantCloudVideo; + var href = file.href + `${!isLocalVideo ? `?type=${file.type}` : ''}`; + var loadedMonitor = loadedMonitors[file.mid]; + return { + image: `
+
+ +
+ +
`, + Monitor: loadedMonitor && loadedMonitor.name ? loadedMonitor.name : file.mid, + mid: file.mid, + time: ` +
${timeAgo(file.time)}
+
${lang.Start} : ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}
+
${lang.End} : ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}
`, + objects: file.objects, + tags: ` + ${file.ext ? `${file.ext}` : ''} + ${!isLocalVideo ? `${file.type}` : ''} + `, + size: convertKbToHumanSize(file.size), + buttons: ` +
+ + ${isLocalVideo && permissionCheck('video_delete',file.mid) ? `` : ''} + - `, - } - }) +
+ `, + } }) + }) } function drawPreviewVideo(href){ videosTablePreviewArea.html(``) @@ -389,6 +419,7 @@ $(document).ready(function(e){ delete(loadedVideosInMemory[`${data.mid}${data.time}${data.type}`]) clearTimeout(redrawTimeout) redrawTimeout = setTimeout(function(){ + videosTableDrawArea.bootstrapTable('destroy'); drawVideosTableViewElements(true) },2000) } diff --git a/web/assets/vendor/leaflet/markerRotator.js b/web/assets/vendor/leaflet/markerRotator.js new file mode 100644 index 00000000..cbc37666 --- /dev/null +++ b/web/assets/vendor/leaflet/markerRotator.js @@ -0,0 +1,57 @@ +(function() { + // save these original methods before they are overwritten + var proto_initIcon = L.Marker.prototype._initIcon; + var proto_setPos = L.Marker.prototype._setPos; + + var oldIE = (L.DomUtil.TRANSFORM === 'msTransform'); + + L.Marker.addInitHook(function () { + var iconOptions = this.options.icon && this.options.icon.options; + var iconAnchor = iconOptions && this.options.icon.options.iconAnchor; + if (iconAnchor) { + iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px'); + } + this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom' ; + this.options.rotationAngle = this.options.rotationAngle || 0; + + // Ensure marker keeps rotated during dragging + this.on('drag', function(e) { e.target._applyRotation(); }); + }); + + L.Marker.include({ + _initIcon: function() { + proto_initIcon.call(this); + }, + + _setPos: function (pos) { + proto_setPos.call(this, pos); + this._applyRotation(); + }, + + _applyRotation: function () { + if(this.options.rotationAngle) { + this._icon.style[L.DomUtil.TRANSFORM+'Origin'] = this.options.rotationOrigin; + + if(oldIE) { + // for IE 9, use the 2D rotation + this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)'; + } else { + // for modern browsers, prefer the 3D accelerated version + this._icon.style[L.DomUtil.TRANSFORM] += ' rotateZ(' + this.options.rotationAngle + 'deg)'; + } + } + }, + + setRotationAngle: function(angle) { + this.options.rotationAngle = angle; + this.update(); + return this; + }, + + setRotationOrigin: function(origin) { + this.options.rotationOrigin = origin; + this.update(); + return this; + } + }); +})(); diff --git a/web/pages/blocks/footer.ejs b/web/pages/blocks/footer.ejs index 98611724..a0be9898 100644 --- a/web/pages/blocks/footer.ejs +++ b/web/pages/blocks/footer.ejs @@ -1,5 +1,6 @@ +