From f41fe9ecf674ad3b054b854bfccfc4121a11cc9e Mon Sep 17 00:00:00 2001
From: Moe <github@m03.ca>
Date: Thu, 22 Aug 2024 18:29:10 +0000
Subject: [PATCH] Bug Fix Bandit

---
 definitions/base.js                           |  35 +-
 languages/de.json                             |   1 +
 languages/en_CA.json                          |   4 +
 languages/fr.json                             |   1 +
 languages/it.json                             |   1 +
 languages/ja.json                             |   1 +
 libs/auth.js                                  |   9 +-
 libs/checker/utils.js                         | 106 ++++++
 libs/config.js                                |   2 +
 libs/cron.js                                  |  15 +
 libs/cron/worker.js                           |  20 +-
 libs/database/utils.js                        |  36 +-
 libs/events/utils.js                          |  20 +-
 libs/extenders.js                             |  33 ++
 libs/ffmpeg/builders.js                       |  17 +-
 libs/monitor.js                               |  30 +-
 libs/monitor/utils.js                         |   4 +-
 libs/socketio.js                              | 107 +++---
 libs/uploaders/amazonS3.js                    |  16 +-
 libs/webServerPaths.js                        |  22 +-
 web/assets/js/bs5.extenders.js                |  15 +
 web/assets/js/bs5.liveGrid.cycle.js           | 163 ---------
 web/assets/js/bs5.liveGrid.js                 |  25 +-
 web/assets/js/bs5.liveGrid.keyboard.js        |  82 ++---
 web/assets/js/bs5.monitorMap.js               |   2 +-
 web/assets/js/bs5.monitorSettings.js          |   3 +
 .../js/bs5.monitorSettings.monitorMap.js      |  18 +-
 web/assets/js/bs5.timeline.js                 |  33 +-
 web/assets/js/bs5.videos.js                   |  90 +++--
 web/assets/js/bs5.videosTable.js              | 313 ++++++++++--------
 web/assets/vendor/leaflet/markerRotator.js    |  57 ++++
 web/pages/blocks/footer.ejs                   |   1 +
 32 files changed, 781 insertions(+), 501 deletions(-)
 create mode 100644 libs/checker/utils.js
 create mode 100644 web/assets/vendor/leaflet/markerRotator.js

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: `<div class="video-thumbnail" data-mid="${file.mid}" data-ke="${file.ke}" data-time="${file.time}" data-end="${file.end}" data-filename="${file.filename}">
-                        <div class="video-thumbnail-img-block">
-                            <img class="pop-image cursor-pointer" style="min-width: 100px;min-height: 75px;">
-                        </div>
-                        <div class="video-thumbnail-buttons d-flex">
-                            <a class="video-thumbnail-button-cell open-snapshot p-3">
-                                <i class="fa fa-camera"></i>
-                            </a>
-                            <a class="video-thumbnail-button-cell preview-video p-3" href="${href}" title="${lang.Play}">
-                                <i class="fa fa-play"></i>
-                            </a>
-                        </div>
-                    </div>`,
-                    Monitor: loadedMonitor && loadedMonitor.name ? loadedMonitor.name : file.mid,
-                    mid: file.mid,
-                    time: `
-                           <div>${timeAgo(file.time)}</div>
-                           <div><small><b>${lang.Start} :</b> ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>
-                           <div><small><b>${lang.End} :</b> ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>`,
-                    objects: file.objects,
-                    tags: `
-                        ${file.ext ? `<span class="badge badge-${file.ext ==='webm' ? `primary` : 'danger'}">${file.ext}</span>` : ''}
-                        ${!isLocalVideo ? `<span class="badge badge-success">${file.type}</span>` : ''}
-                    `,
-                    size: convertKbToHumanSize(file.size),
-                    buttons: `
-                    <div class="row-info btn-group" data-mid="${file.mid}" data-ke="${file.ke}" data-time="${file.time}" data-filename="${file.filename}" data-status="${file.status}" data-type="${file.type}">
-                        <a class="btn btn-sm btn-default btn-monitor-status-color open-video" href="${href}" title="${lang.Play}"><i class="fa fa-play"></i></a>
-                        ${isLocalVideo && permissionCheck('video_delete',file.mid) ? `<a class="btn btn-sm btn-${file.archive === 1 ? `success status-archived` : `default`} archive-video" title="${lang.Archive}"><i class="fa fa-${file.archive === 1 ? `lock` : `unlock-alt`}"></i></a>` : ''}
-                        <div class="dropdown d-inline-block">
-                            <a class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" data-bs-reference="parent">
-                              <i class="fa fa-ellipsis-v" aria-hidden="true"></i>
-                            </a>
-                            <ul class="dropdown-menu ${definitions.Theme.isDark ? 'dropdown-menu-dark bg-dark' : ''} shadow-lg">
-                                ${buildDefaultVideoMenuItems(file)}
-                            </ul>
-                        </div>
+            {
+                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: `<div class="video-thumbnail" data-mid="${file.mid}" data-ke="${file.ke}" data-time="${file.time}" data-end="${file.end}" data-filename="${file.filename}">
+                            <div class="video-thumbnail-img-block">
+                                <img class="pop-image cursor-pointer" style="min-width: 100px;min-height: 75px;">
+                            </div>
+                            <div class="video-thumbnail-buttons d-flex">
+                                <a class="video-thumbnail-button-cell open-snapshot p-3">
+                                    <i class="fa fa-camera"></i>
+                                </a>
+                                <a class="video-thumbnail-button-cell preview-video p-3" href="${href}" title="${lang.Play}">
+                                    <i class="fa fa-play"></i>
+                                </a>
+                            </div>
+                        </div>`,
+                Monitor: loadedMonitor && loadedMonitor.name ? loadedMonitor.name : file.mid,
+                mid: file.mid,
+                time: `
+                       <div>${timeAgo(file.time)}</div>
+                       <div><small><b>${lang.Start} :</b> ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>
+                       <div><small><b>${lang.End} :</b> ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>`,
+                objects: file.objects,
+                tags: `
+                    ${file.ext ? `<span class="badge badge-${file.ext ==='webm' ? `primary` : 'danger'}">${file.ext}</span>` : ''}
+                    ${!isLocalVideo ? `<span class="badge badge-success">${file.type}</span>` : ''}
+                `,
+                size: convertKbToHumanSize(file.size),
+                buttons: `
+                <div class="row-info btn-group" data-mid="${file.mid}" data-ke="${file.ke}" data-time="${file.time}" data-filename="${file.filename}" data-status="${file.status}" data-type="${file.type}">
+                    <a class="btn btn-sm btn-default btn-monitor-status-color open-video" href="${href}" title="${lang.Play}"><i class="fa fa-play"></i></a>
+                    ${isLocalVideo && permissionCheck('video_delete',file.mid) ? `<a class="btn btn-sm btn-${file.archive === 1 ? `success status-archived` : `default`} archive-video" title="${lang.Archive}"><i class="fa fa-${file.archive === 1 ? `lock` : `unlock-alt`}"></i></a>` : ''}
+                    <div class="dropdown d-inline-block">
+                        <a class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" data-bs-reference="parent">
+                          <i class="fa fa-ellipsis-v" aria-hidden="true"></i>
+                        </a>
+                        <ul class="dropdown-menu ${definitions.Theme.isDark ? 'dropdown-menu-dark bg-dark' : ''} shadow-lg">
+                            ${buildDefaultVideoMenuItems(file)}
+                        </ul>
                     </div>
-                    `,
-                }
-            })
+                </div>
+                `,
+            }
         })
+    })
     }
     function drawPreviewVideo(href){
         videosTablePreviewArea.html(`<video class="video_video" style="width:100%" autoplay controls preload loop src="${href}"></video>`)
@@ -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 @@
 <!--   Core JS Files   -->
 <script src="<%-window.libURL%>assets/vendor/leaflet/leaflet.js"></script>
+<script src="<%-window.libURL%>assets/vendor/leaflet/markerRotator.js"></script>
 <script src="<%-window.libURL%>assets/vendor/js/jquery-ui.min.js"></script>
 <script src="<%-window.libURL%>assets/vendor/js/pnotify.custom.min.js"></script>
 <script src="<%-window.libURL%>assets/vendor/js/socket.io.min.js"></script>