From b7c01c9f0fc618bb9ded450c12255b2bb2eb1f6a Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 13 Feb 2026 21:49:11 +0300 Subject: [PATCH 01/13] Moving "monitorsSetScale" function from montage.js to skin.js (montage.js) --- web/skins/classic/views/js/montage.js | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/web/skins/classic/views/js/montage.js b/web/skins/classic/views/js/montage.js index d8f6f45b2..482609b22 100644 --- a/web/skins/classic/views/js/montage.js +++ b/web/skins/classic/views/js/montage.js @@ -860,33 +860,6 @@ function changeStreamQuality() { monitorsSetScale(); } -function monitorsSetScale(id=null) { - // This function will probably need to be moved to the main JS file, because now used on Watch & Montage pages - id = parseInt(id); - if (id || typeof monitorStream !== 'undefined') { - //monitorStream used on Watch page. - if (typeof monitorStream !== 'undefined') { - var currentMonitor = monitorStream; - } else { - var currentMonitor = monitors.find((o) => { - return parseInt(o["id"]) === id; - }); - } - const el = document.getElementById('liveStream'+id); - const panZoomScale = (panZoomEnabled && zmPanZoom.panZoom[id] ) ? zmPanZoom.panZoom[id].getScale() : 1; - console.log("monitorsSetsCale", id, 'clientWidth', el.clientWidth, 'clientHeight', el.clientHeight, 'panzoomscale', panZoomScale); - currentMonitor.setScale(0, el.clientWidth * panZoomScale + 'px', el.clientHeight * panZoomScale + 'px', {resizeImg: false, streamQuality: $j('#streamQuality').val()}); - } else { - for ( let i = 0, length = monitors.length; i < length; i++ ) { - const id = monitors[i].id; - const el = document.getElementById('liveStream'+id); - const panZoomScale = panZoomEnabled ? zmPanZoom.panZoom[id].getScale() : 1; - monitors[i].setScale(0, parseInt(el.clientWidth * panZoomScale) + 'px', parseInt(el.clientHeight * panZoomScale) + 'px', {resizeImg: false, streamQuality: $j('#streamQuality').val()}); - } - } - setButtonSizeOnStream(); -} - function changeMonitorRate() { const rate = $j('#changeRate').val(); monitorsSetRate(rate); From a5828debb9a474470a45cca05821abc36916fe34 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 13 Feb 2026 22:12:39 +0300 Subject: [PATCH 02/13] Wrap "#dvrControls" and "#extButton" in a single DIV "#bottomBlock" for correct scaling in scale=Auto mode (watch.php) --- web/skins/classic/views/watch.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/views/watch.php b/web/skins/classic/views/watch.php index 7e075519e..6433fb332 100644 --- a/web/skins/classic/views/watch.php +++ b/web/skins/classic/views/watch.php @@ -411,7 +411,7 @@ echo htmlSelect('cyclePeriod', $cyclePeriodOptions, $period, array('id'=>'cycleP echo $monitor->getStreamHTML($options); ?> -
+
@@ -461,7 +461,7 @@ ZM\Debug("Muted $muted");
-
+
From 6d5ebc131bce7523a42d9fed3974343e3c3ffb80 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 13 Feb 2026 22:20:06 +0300 Subject: [PATCH 03/13] Moving "monitorsSetScale" function from to skin.js (watch.js) And Remove Event listener when restarting the stream --- web/skins/classic/views/js/watch.js | 133 ++-------------------------- 1 file changed, 9 insertions(+), 124 deletions(-) diff --git a/web/skins/classic/views/js/watch.js b/web/skins/classic/views/js/watch.js index d23237e7b..8981437c1 100644 --- a/web/skins/classic/views/js/watch.js +++ b/web/skins/classic/views/js/watch.js @@ -834,7 +834,7 @@ function streamStart(monitor = null) { monitorStream = new MonitorStream(monitor ? monitor : monitorData[monIdx]); monitorStream.setPlayer($j('#player').val()); - monitorStream.setBottomElement(document.getElementById('dvrControls')); + monitorStream.setBottomElement(document.getElementById('bottomBlock')); monitorStream.controlMute(getCookie('zmWatchMute') || 'on'); // default to muted monitorStream.manageAvailablePlayers(); setChannelStream(); @@ -917,6 +917,13 @@ function streamReStart(oldId, newId) { zmPanZoom.init(); zmPanZoom.init({objString: '.imageFeed', disablePan: true, contain: 'inside', additional: true}); //document.getElementById('monitor').classList.remove('hidden-shift'); + + // Remove Event listener for double click + const elStream = document.querySelectorAll('[id = "wrapperMonitor"]'); + Array.prototype.forEach.call(elStream, (el) => { + el.removeEventListener('touchstart', doubleTouch); + el.removeEventListener('dblclick', doubleClickOnStream); + }); } function setChannelStream() { @@ -1105,7 +1112,7 @@ function initPage() { }); // Registering an observer on an element - $j('[id ^= "liveStream"]').each(function() { + $j('[id ^= "liveStream"], .monitorStatus').each(function() { observer.observe(this); }); @@ -1368,128 +1375,6 @@ function changePlayer() { }, 300);*/ } -function monitorsSetScale(id=null) { - //This function will probably need to be moved to the main JS file, because now used on Watch & Montage pages - if (id || typeof monitorStream !== 'undefined') { - if (monitorStream !== false) { - //monitorStream used on Watch page. - var currentMonitor = monitorStream; - } else if (typeof monitors !== 'undefined') { - //used on Montage, Watch & Event page. - var currentMonitor = monitors.find((o) => { - return parseInt(o["id"]) === id; - }); - } else { - //Stream is missing - return; - } - var panZoomScale = (panZoomEnabled && zmPanZoom.panZoom[id]) ? zmPanZoom.panZoom[id].getScale() : 1; - - let resize = false; - let width = 'auto'; - let maxWidth = ''; - let height = 'auto'; - let overrideHW = false; - let defScale = 0; - const landscape = currentMonitor.width / currentMonitor.height > 1 ? true : false; //Image orientation. - - const scale = $j('#scale').val(); - if (scale == '0') { - //Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen. - resize = true; - } else if (scale == '100') { - //Actual, 100% of original size - width = currentMonitor.width + 'px'; - height = currentMonitor.height + 'px'; - } else if (scale == 'fit_to_width') { - //Fit to screen width - width = parseInt(window.innerWidth * panZoomScale) + 'px'; - } else if (scale.indexOf("px") > -1) { - if (landscape) { - maxWidth = scale; - defScale = parseInt(Math.min(stringToNumber(scale), window.innerWidth) / currentMonitor.width * panZoomScale * 100); - } else { - defScale = parseInt(Math.min(stringToNumber(scale), window.innerHeight) / currentMonitor.height * panZoomScale * 100); - height = scale; - } - resize = true; - overrideHW = true; - } - - const liveStream = document.getElementById('liveStream'+id); - const monitor_div = document.getElementById('monitor'+id); - if (!monitor_div) console.log("No monitor div for ", id); - if (resize) { - if (scale == '0') { - monitor_div.style.width = 'max-content'; //Required when switching from resize=false to resize=true - } - monitor_div.style.maxWidth = maxWidth; - if (!landscape) { //PORTRAIT - monitor_div.style.width = 'max-content'; - liveStream.style.height = height; - } - } else { - liveStream.style.height = ''; - monitor_div.style.width = width; - monitor_div.style.maxWidth = ''; - if (scale == 'fit_to_width') { - monitor_div.style.width = ''; - } else if (scale == '100') { - liveStream.style.width = width; - } - } - //currentMonitor.setScale(0, maxWidth ? maxWidth : width, height, {resizeImg: resize, scaleImg: panZoomScale}); - currentMonitor.setScale(defScale, width, height, {resizeImg: resize, scaleImg: panZoomScale, streamQuality: $j('#streamQuality').val()}); - if (overrideHW) { - if (!landscape) { //PORTRAIT - monitor_div.style.width = 'max-content'; - } else { - liveStream.style.height = 'auto'; - monitor_div.style.width = 'auto'; - } - } - } else { // Not a specific stream, but all streams. - for ( let i = 0, length = monitors.length; i < length; i++ ) { - const id = monitors[i].id; - var panZoomScale = panZoomEnabled ? panZoom[id].getScale() : 1; - - let resize = false; - let width = 'auto'; - let height = 'auto'; - - const scale = $j('#scale').val(); - if (scale == '0') { - //Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen. - resize = true; - } else if (scale == '100') { - //Actual, 100% of original size - width = monitors[i].width + 'px'; - height = monitors[i].height + 'px'; - } else if (scale == 'fit_to_width') { - //Fit to screen width - width = parseInt(window.innerWidth * panZoomScale) + 'px'; - } - - if (resize) { - monitor_div.style.width = 'max-content'; //Required when switching from resize=false to resize=true - } - //monitors[i].setScale(0, parseInt(el.clientWidth * panZoomScale) + 'px', parseInt(el.clientHeight * panZoomScale) + 'px', {resizeImg:true, scaleImg:panZoomScale}); - monitors[i].setScale(0, width, height, {resizeImg: resize, scaleImg: panZoomScale}); - if (!resize) { - livestream = document.getElementById('liveStream'+id); - livestream.style.height = ''; - if (scale == 'fit_to_width') { - monitor_div.style.width = ''; - } else if (scale == '100') { - monitor_div.style.width = 'max-content'; - liveStream.style.width = width; - } - } - } // end foreach monitor - } - setButtonSizeOnStream(); -} - // Kick everything off $j( window ).on("load", initPage); From 97929d1c9a042a579ff03d9eea438dfc4196d8ed Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 13 Feb 2026 22:27:52 +0300 Subject: [PATCH 04/13] Merging and moving two identical functions monitorsSetScale from montage.js and watch.js to skin.js And take into account "scrollTop" when performing scaleToFit --- web/skins/classic/js/skin.js | 131 ++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index ad54c81e9..53e124059 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -796,6 +796,8 @@ function scaleToFit(baseWidth, baseHeight, scaleEl, bottomEl, container, panZoom const viewPort = $j(window); // jquery does not provide a bottom offset, and offset does not include margins. outerHeight true minus false gives total vertical margins. var bottomLoc = 0; + const content = $j("#content"); + const scrollTop = (content.length > 0) ? content.scrollTop() : 0; // Watch page may have Scroll when Stream changes if (bottomEl !== false) { if (!bottomEl || !bottomEl.length) { if (container[0] && container[0].lastElementChild) { @@ -805,7 +807,7 @@ function scaleToFit(baseWidth, baseHeight, scaleEl, bottomEl, container, panZoom } } if (bottomEl && bottomEl.length) { - bottomLoc = bottomEl.offset().top + (bottomEl.outerHeight(true) - bottomEl.outerHeight()) + bottomEl.outerHeight(true); + bottomLoc = bottomEl.offset().top + (bottomEl.outerHeight(true) - bottomEl.outerHeight()) + bottomEl.outerHeight(true) + scrollTop; console.log("bottomLoc: " + bottomEl.offset().top + " + (" + bottomEl.outerHeight(true) + ' - ' + bottomEl.outerHeight() +') + '+bottomEl.outerHeight(true) + '='+bottomLoc); } } @@ -2627,4 +2629,131 @@ function replaceDOMElement(fromEl, toTypeEl) { return newEl; }; +function monitorsSetScale(id=null) { + id = stringToNumber(id); + if (!isNaN(id)) { + let currentMonitor; + if (typeof monitorStream !== 'undefined' && monitorStream !== false) { + //monitorStream used on Watch page. + currentMonitor = monitorStream; + } else if (typeof monitors !== 'undefined') { + //used on Montage, Watch & Event page. + currentMonitor = monitors.find((o) => { + return parseInt(o["id"]) === id; + }); + } else { + console.log("Stream is missing."); + return; + } + _setScale(currentMonitor) + } else { // Not a specific stream, but all streams. + for ( let i = 0, length = monitors.length; i < length; i++ ) { + _setScale(monitors[i]) + } // end foreach monitor + } + + function _setScale(currentMonitor) { + const id = currentMonitor.id; + let panZoomScale = (panZoomEnabled && typeof zmPanZoom !== 'undefined') ? zmPanZoom.panZoom[id].getScale() : 1; + let resize = false; + let width = 'auto'; + let height = 'auto'; + let overrideHW = false; + let defScale = 0; + const ratio = currentMonitor.width / currentMonitor.height; + const landscape = ratio > 1 ? true : false; //Image orientation. + const liveStream = currentMonitor.getElement(); + const monitor_div = document.getElementById('monitor'+id); + if (!monitor_div) { + console.log("No monitor div for ", id); + return; + } + const monitorStatus = $j('#monitorStatus'+stringToNumber(liveStream.id)); + const scale = $j('#scale').val(); + if (scale) { + if (scale == '0') { + //Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen. +// overrideHW = true; + } else if (scale == '100') { + //Actual, 100% of original size + width = currentMonitor.width + 'px'; + height = currentMonitor.height + 'px'; + overrideHW = true; + } else if (scale == 'fit_to_width') { + //Fit to screen width + width = parseInt(monitor_div.clientWidth * panZoomScale) + 'px'; + defScale = parseInt(monitor_div.clientWidth / currentMonitor.width * panZoomScale * 100); + overrideHW = true; + } else if (scale.indexOf("px") > -1) { + // If the aspect ratio specified in the monitor settings does not correspond to the actual aspect ratio of the video, then: + // - For MJPEG player, the video will be displayed according to the specified aspect ratio. + // - For other players, taking into account the actual video aspect ratio. + if (landscape) { + width = scale; + //height = parseInt(scale) / ratio + 'px'; + defScale = parseInt(Math.min(stringToNumber(scale), window.innerWidth) / currentMonitor.width * panZoomScale * 100); + } else { + width = parseInt(parseInt(scale) * ratio) + 'px'; + //height = scale; + defScale = parseInt(Math.min(stringToNumber(scale), window.innerHeight) / currentMonitor.height * panZoomScale * 100); + } + overrideHW = true; + } + resize = true; + } else { // Montage page does not have "scale" + width = parseInt(liveStream.clientWidth * panZoomScale) + 'px'; + height = parseInt(liveStream.clientHeight * panZoomScale) + 'px'; + } + + currentMonitor.setScale(defScale, width, height, {resizeImg: resize, scaleImg: panZoomScale, streamQuality: $j('#streamQuality').val()}); + + if (overrideHW) { + if (scale == 'fit_to_width') { + liveStream.style.width = width; + liveStream.style.height = ''; + liveStream.style.maxWidth = ''; + monitor_div.style.width = 'auto'; + monitor_div.style.height = ''; + monitor_div.style.maxWidth = ''; + } else if (scale.indexOf("px") > -1) { + liveStream.style.width = ''; + liveStream.style.height = height; + liveStream.style.maxWidth = '100%'; + monitor_div.style.width = width; + monitor_div.style.height = ''; + monitor_div.style.maxWidth = '100%'; + } else if (scale == '100') { + liveStream.style.width = width; + liveStream.style.height = height; + liveStream.style.maxWidth = ''; + monitor_div.style.width = 'fit-content'; + monitor_div.style.height = ''; + monitor_div.style.maxWidth = ''; + } else { + liveStream.style.width = width; + liveStream.style.height = height; + liveStream.style.maxWidth = ''; + monitor_div.style.width = '100%'; + monitor_div.style.height = ''; + monitor_div.style.maxWidth = ''; + } + } + + if (scale == '0'){ + const fillVideo = true; //true = In AUTO mode it will stretch to the full width, but will spoil the proportions if they do not correspond to the actual proportions in the monitor settings!!! + const tagVideo = (liveStream.tagName == 'VIDEO') ? liveStream : liveStream.querySelector('video'); + if (tagVideo) { + if (fillVideo) { + tagVideo.style.width = ''; + tagVideo.objectFit = 'fill'; + } else { + tagVideo.style.width = 'auto'; + tagVideo.objectFit = ''; + } + } + } + } // End function _setScale + setButtonSizeOnStream(); +} // End function monitorsSetScale + $j( window ).on("load", initPageGeneral); From 0d712deb4c9b6c171c4743ba6a33aa7c2d45bd80 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 13 Feb 2026 23:00:58 +0300 Subject: [PATCH 05/13] Added styles for id=liveStream* (skin.css) --- web/skins/classic/css/base/skin.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/skins/classic/css/base/skin.css b/web/skins/classic/css/base/skin.css index b01c10447..7656dcc4c 100644 --- a/web/skins/classic/css/base/skin.css +++ b/web/skins/classic/css/base/skin.css @@ -1595,6 +1595,17 @@ video-stream { overflow: hidden; } +img[id^='liveStream'], video[id^='liveStream'], video-stream[id^='liveStream'] { + margin: 0 auto; + object-fit: fill; +} + +video-stream[id^='liveStream'] video{ + max-height: inherit; + max-width: inherit; + object-fit: fill; +} + /* +++ This block should always be located at the end! */ .hidden { display: none; From 327dd9999db6c904abee19502a75b11ad1052864 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 13 Feb 2026 23:07:03 +0300 Subject: [PATCH 06/13] Changed the style for ".monitor" a bit (watch.css) --- web/skins/classic/css/base/views/watch.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/skins/classic/css/base/views/watch.css b/web/skins/classic/css/base/views/watch.css index 9f29730fb..40f259966 100644 --- a/web/skins/classic/css/base/views/watch.css +++ b/web/skins/classic/css/base/views/watch.css @@ -40,11 +40,15 @@ } .monitor { + display: flex; + flex-direction: column; margin: 0 auto; } .monitor .imageFeed { border: 2px solid #999999; + padding: 0px; + margin: 0px; } .monitor .imageFeed img, @@ -52,7 +56,8 @@ .monitor .imageFeed video-stream, .monitor .imageFeed svg { border: none; - display: inline-block; + /*display: inline-block;*/ + display: block; width: 100%; } From a754967c28c94170a5b6225528d192fc831422f4 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 13 Feb 2026 23:12:10 +0300 Subject: [PATCH 07/13] Removed styles for GridStack, since in version 12 styles are now generated dynamically (montage.css) --- web/skins/classic/css/base/views/montage.css | 428 ------------------- 1 file changed, 428 deletions(-) diff --git a/web/skins/classic/css/base/views/montage.css b/web/skins/classic/css/base/views/montage.css index 3557b985f..14193ef0a 100644 --- a/web/skins/classic/css/base/views/montage.css +++ b/web/skins/classic/css/base/views/montage.css @@ -105,431 +105,3 @@ .grid-stack-item-content.modeEditingMonitor { cursor: crosshair; } - -.gs-24 > .grid-stack-item { - width: 4.167%; -} -.gs-24 > .grid-stack-item[gs-x="1"] { - left: 4.167%; -} -.gs-24 > .grid-stack-item[gs-w="2"] { - width: 8.333%; -} -.gs-24 > .grid-stack-item[gs-x="2"] { - left: 8.333%; -} -.gs-24 > .grid-stack-item[gs-w="3"] { - width: 12.5%; -} -.gs-24 > .grid-stack-item[gs-x="3"] { - left: 12.5%; -} -.gs-24 > .grid-stack-item[gs-w="4"] { - width: 16.667%; -} -.gs-24 > .grid-stack-item[gs-x="4"] { - left: 16.667%; -} -.gs-24 > .grid-stack-item[gs-w="5"] { - width: 20.833%; -} -.gs-24 > .grid-stack-item[gs-x="5"] { - left: 20.833%; -} -.gs-24 > .grid-stack-item[gs-w="6"] { - width: 25%; -} -.gs-24 > .grid-stack-item[gs-x="6"] { - left: 25%; -} -.gs-24 > .grid-stack-item[gs-w="7"] { - width: 29.167%; -} -.gs-24 > .grid-stack-item[gs-x="7"] { - left: 29.167%; -} -.gs-24 > .grid-stack-item[gs-w="8"] { - width: 33.333%; -} -.gs-24 > .grid-stack-item[gs-x="8"] { - left: 33.333%; -} -.gs-24 > .grid-stack-item[gs-w="9"] { - width: 37.5%; -} -.gs-24 > .grid-stack-item[gs-x="9"] { - left: 37.5%; -} -.gs-24 > .grid-stack-item[gs-w="10"] { - width: 41.667%; -} -.gs-24 > .grid-stack-item[gs-x="10"] { - left: 41.667%; -} -.gs-24 > .grid-stack-item[gs-w="11"] { - width: 45.833%; -} -.gs-24 > .grid-stack-item[gs-x="11"] { - left: 45.833%; -} -.gs-24 > .grid-stack-item[gs-w="12"] { - width: 50%; -} -.gs-24 > .grid-stack-item[gs-x="12"] { - left: 50%; -} -.gs-24 > .grid-stack-item[gs-w="13"] { - width: 54.167%; -} -.gs-24 > .grid-stack-item[gs-x="13"] { - left: 54.167%; -} -.gs-24 > .grid-stack-item[gs-w="14"] { - width: 58.333%; -} -.gs-24 > .grid-stack-item[gs-x="14"] { - left: 58.333%; -} -.gs-24 > .grid-stack-item[gs-w="15"] { - width: 62.5%; -} -.gs-24 > .grid-stack-item[gs-x="15"] { - left: 62.5%; -} -.gs-24 > .grid-stack-item[gs-w="16"] { - width: 66.667%; -} -.gs-24 > .grid-stack-item[gs-x="16"] { - left: 66.667%; -} -.gs-24 > .grid-stack-item[gs-w="17"] { - width: 70.833%; -} -.gs-24 > .grid-stack-item[gs-x="17"] { - left: 70.833%; -} -.gs-24 > .grid-stack-item[gs-w="18"] { - width: 75%; -} -.gs-24 > .grid-stack-item[gs-x="18"] { - left: 75%; -} -.gs-24 > .grid-stack-item[gs-w="19"] { - width: 79.167%; -} -.gs-24 > .grid-stack-item[gs-x="19"] { - left: 79.167%; -} -.gs-24 > .grid-stack-item[gs-w="20"] { - width: 83.333%; -} -.gs-24 > .grid-stack-item[gs-x="20"] { - left: 83.333%; -} -.gs-24 > .grid-stack-item[gs-w="21"] { - width: 87.5%; -} -.gs-24 > .grid-stack-item[gs-x="21"] { - left: 87.5%; -} -.gs-24 > .grid-stack-item[gs-w="22"] { - width: 91.667%; -} -.gs-24 > .grid-stack-item[gs-x="22"] { - left: 91.667%; -} -.gs-24 > .grid-stack-item[gs-w="23"] { - width: 95.833%; -} -.gs-24 > .grid-stack-item[gs-x="23"] { - left: 95.833%; -} -.gs-24 > .grid-stack-item[gs-w="24"] { - width: 100%; -} - -.gs-48 > .grid-stack-item { - width: 2.083%; -} -.gs-48 > .grid-stack-item[gs-x="1"] { - left: 2.083%; -} -.gs-48 > .grid-stack-item[gs-w="2"] { - width: 4.167%; -} -.gs-48 > .grid-stack-item[gs-x="2"] { - left: 4.167%; -} -.gs-48 > .grid-stack-item[gs-w="3"] { - width: 6.25%; -} -.gs-48 > .grid-stack-item[gs-x="3"] { - left: 6.25%; -} -.gs-48 > .grid-stack-item[gs-w="4"] { - width: 8.333%; -} -.gs-48 > .grid-stack-item[gs-x="4"] { - left: 8.333%; -} -.gs-48 > .grid-stack-item[gs-w="5"] { - width: 10.417%; -} -.gs-48 > .grid-stack-item[gs-x="5"] { - left: 10.417%; -} -.gs-48 > .grid-stack-item[gs-w="6"] { - width: 12.5%; -} -.gs-48 > .grid-stack-item[gs-x="6"] { - left: 12.5%; -} -.gs-48 > .grid-stack-item[gs-w="7"] { - width: 14.583%; -} -.gs-48 > .grid-stack-item[gs-x="7"] { - left: 14.583%; -} -.gs-48 > .grid-stack-item[gs-w="8"] { - width: 16.667%; -} -.gs-48 > .grid-stack-item[gs-x="8"] { - left: 16.667%; -} -.gs-48 > .grid-stack-item[gs-w="9"] { - width: 18.75%; -} -.gs-48 > .grid-stack-item[gs-x="9"] { - left: 18.75%; -} -.gs-48 > .grid-stack-item[gs-w="10"] { - width: 20.833%; -} -.gs-48 > .grid-stack-item[gs-x="10"] { - left: 20.833%; -} -.gs-48 > .grid-stack-item[gs-w="11"] { - width: 22.917%; -} -.gs-48 > .grid-stack-item[gs-x="11"] { - left: 22.917%; -} -.gs-48 > .grid-stack-item[gs-w="12"] { - width: 25%; -} -.gs-48 > .grid-stack-item[gs-x="12"] { - left: 25%; -} -.gs-48 > .grid-stack-item[gs-w="13"] { - width: 27.083%; -} -.gs-48 > .grid-stack-item[gs-x="13"] { - left: 27.083%; -} -.gs-48 > .grid-stack-item[gs-w="14"] { - width: 29.167%; -} -.gs-48 > .grid-stack-item[gs-x="14"] { - left: 29.167%; -} -.gs-48 > .grid-stack-item[gs-w="15"] { - width: 31.25%; -} -.gs-48 > .grid-stack-item[gs-x="15"] { - left: 31.25%; -} -.gs-48 > .grid-stack-item[gs-w="16"] { - width: 33.333%; -} -.gs-48 > .grid-stack-item[gs-x="16"] { - left: 33.333%; -} -.gs-48 > .grid-stack-item[gs-w="17"] { - width: 35.417%; -} -.gs-48 > .grid-stack-item[gs-x="17"] { - left: 35.417%; -} -.gs-48 > .grid-stack-item[gs-w="18"] { - width: 37.5%; -} -.gs-48 > .grid-stack-item[gs-x="18"] { - left: 37.5%; -} -.gs-48 > .grid-stack-item[gs-w="19"] { - width: 39.583%; -} -.gs-48 > .grid-stack-item[gs-x="19"] { - left: 39.583%; -} -.gs-48 > .grid-stack-item[gs-w="20"] { - width: 41.667%; -} -.gs-48 > .grid-stack-item[gs-x="20"] { - left: 41.667%; -} -.gs-48 > .grid-stack-item[gs-w="21"] { - width: 43.75%; -} -.gs-48 > .grid-stack-item[gs-x="21"] { - left: 43.75%; -} -.gs-48 > .grid-stack-item[gs-w="22"] { - width: 45.833%; -} -.gs-48 > .grid-stack-item[gs-x="22"] { - left: 45.833%; -} -.gs-48 > .grid-stack-item[gs-w="23"] { - width: 47.917%; -} -.gs-48 > .grid-stack-item[gs-x="23"] { - left: 47.917%; -} -.gs-48 > .grid-stack-item[gs-w="24"] { - width: 50%; -} -.gs-48 > .grid-stack-item[gs-x="24"] { - left: 50%; -} -.gs-48 > .grid-stack-item[gs-w="25"] { - width: 52.083%; -} -.gs-48 > .grid-stack-item[gs-x="25"] { - left: 52.083%; -} -.gs-48 > .grid-stack-item[gs-w="26"] { - width: 54.167%; -} -.gs-48 > .grid-stack-item[gs-x="26"] { - left: 54.167%; -} -.gs-48 > .grid-stack-item[gs-w="27"] { - width: 56.25%; -} -.gs-48 > .grid-stack-item[gs-x="27"] { - left: 56.25%; -} -.gs-48 > .grid-stack-item[gs-w="28"] { - width: 58.333%; -} -.gs-48 > .grid-stack-item[gs-x="28"] { - left: 58.333%; -} -.gs-48 > .grid-stack-item[gs-w="29"] { - width: 60.417%; -} -.gs-48 > .grid-stack-item[gs-x="29"] { - left: 60.417%; -} -.gs-48 > .grid-stack-item[gs-w="30"] { - width: 62.5%; -} -.gs-48 > .grid-stack-item[gs-x="30"] { - left: 62.5%; -} -.gs-48 > .grid-stack-item[gs-w="31"] { - width: 64.583%; -} -.gs-48 > .grid-stack-item[gs-x="31"] { - left: 64.583%; -} -.gs-48 > .grid-stack-item[gs-w="32"] { - width: 66.667%; -} -.gs-48 > .grid-stack-item[gs-x="32"] { - left: 66.667%; -} -.gs-48 > .grid-stack-item[gs-w="33"] { - width: 68.75%; -} -.gs-48 > .grid-stack-item[gs-x="33"] { - left: 68.75%; -} -.gs-48 > .grid-stack-item[gs-w="34"] { - width: 70.833%; -} -.gs-48 > .grid-stack-item[gs-x="34"] { - left: 70.833%; -} -.gs-48 > .grid-stack-item[gs-w="35"] { - width: 72.917%; -} -.gs-48 > .grid-stack-item[gs-x="35"] { - left: 72.917%; -} -.gs-48 > .grid-stack-item[gs-w="36"] { - width: 75%; -} -.gs-48 > .grid-stack-item[gs-x="36"] { - left: 75%; -} -.gs-48 > .grid-stack-item[gs-w="37"] { - width: 77.083%; -} -.gs-48 > .grid-stack-item[gs-x="37"] { - left: 77.083%; -} -.gs-48 > .grid-stack-item[gs-w="38"] { - width: 79.167%; -} -.gs-48 > .grid-stack-item[gs-x="38"] { - left: 79.167%; -} -.gs-48 > .grid-stack-item[gs-w="39"] { - width: 81.25%; -} -.gs-48 > .grid-stack-item[gs-x="39"] { - left: 81.25%; -} -.gs-48 > .grid-stack-item[gs-w="40"] { - width: 83.333%; -} -.gs-48 > .grid-stack-item[gs-x="40"] { - left: 83.333%; -} -.gs-48 > .grid-stack-item[gs-w="41"] { - width: 85.417%; -} -.gs-48 > .grid-stack-item[gs-x="41"] { - left: 85.417%; -} -.gs-48 > .grid-stack-item[gs-w="42"] { - width: 87.5%; -} -.gs-48 > .grid-stack-item[gs-x="42"] { - left: 87.5%; -} -.gs-48 > .grid-stack-item[gs-w="43"] { - width: 89.583%; -} -.gs-48 > .grid-stack-item[gs-x="43"] { - left: 89.583%; -} -.gs-48 > .grid-stack-item[gs-w="44"] { - width: 91.667%; -} -.gs-48 > .grid-stack-item[gs-x="44"] { - left: 91.667%; -} -.gs-48 > .grid-stack-item[gs-w="45"] { - width: 93.75%; -} -.gs-48 > .grid-stack-item[gs-x="45"] { - left: 93.75%; -} -.gs-48 > .grid-stack-item[gs-w="46"] { - width: 95.833%; -} -.gs-48 > .grid-stack-item[gs-x="46"] { - left: 95.833%; -} -.gs-48 > .grid-stack-item[gs-w="47"] { - width: 97.917%; -} -.gs-48 > .grid-stack-item[gs-x="47"] { - left: 97.917%; -} -.gs-48 > .grid-stack-item[gs-w="48"] { - width: 100%; -} From 3f78562de8afb5e33dc1db5486637ab457ee6c4f Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 13 Feb 2026 23:27:42 +0300 Subject: [PATCH 08/13] Fix: Eslint (skin.js) --- web/skins/classic/js/skin.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index 53e124059..c56bf261a 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -2645,16 +2645,16 @@ function monitorsSetScale(id=null) { console.log("Stream is missing."); return; } - _setScale(currentMonitor) + _setScale(currentMonitor); } else { // Not a specific stream, but all streams. for ( let i = 0, length = monitors.length; i < length; i++ ) { - _setScale(monitors[i]) + _setScale(monitors[i]); } // end foreach monitor } function _setScale(currentMonitor) { const id = currentMonitor.id; - let panZoomScale = (panZoomEnabled && typeof zmPanZoom !== 'undefined') ? zmPanZoom.panZoom[id].getScale() : 1; + const panZoomScale = (panZoomEnabled && typeof zmPanZoom !== 'undefined') ? zmPanZoom.panZoom[id].getScale() : 1; let resize = false; let width = 'auto'; let height = 'auto'; @@ -2668,12 +2668,10 @@ function monitorsSetScale(id=null) { console.log("No monitor div for ", id); return; } - const monitorStatus = $j('#monitorStatus'+stringToNumber(liveStream.id)); const scale = $j('#scale').val(); if (scale) { if (scale == '0') { //Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen. -// overrideHW = true; } else if (scale == '100') { //Actual, 100% of original size width = currentMonitor.width + 'px'; @@ -2739,7 +2737,7 @@ function monitorsSetScale(id=null) { } } - if (scale == '0'){ + if (scale == '0') { const fillVideo = true; //true = In AUTO mode it will stretch to the full width, but will spoil the proportions if they do not correspond to the actual proportions in the monitor settings!!! const tagVideo = (liveStream.tagName == 'VIDEO') ? liveStream : liveStream.querySelector('video'); if (tagVideo) { From 931ac2da08ac1db47be74e7dfd2f31d2624beb41 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Sat, 14 Feb 2026 00:03:57 +0300 Subject: [PATCH 09/13] I created some slightly rounded corners for the streams, tables, and navigation block: .imageFeed, .nav, .fixed-table-body, id='videoFeedStream*' (skin.css) --- web/skins/classic/css/base/skin.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/skins/classic/css/base/skin.css b/web/skins/classic/css/base/skin.css index b01c10447..58bfc839e 100644 --- a/web/skins/classic/css/base/skin.css +++ b/web/skins/classic/css/base/skin.css @@ -1595,6 +1595,14 @@ video-stream { overflow: hidden; } +.imageFeed, +.nav, +.fixed-table-body, +*[id^='videoFeedStream'] + { + border-radius: 4px; +} + /* +++ This block should always be located at the end! */ .hidden { display: none; From ce9f6f4a853694117b84f33cd69023a89e3dda40 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 15 Feb 2026 12:45:52 -0500 Subject: [PATCH 10/13] Add 4px border-radius to thumbnail img --- web/skins/classic/css/base/skin.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/skins/classic/css/base/skin.css b/web/skins/classic/css/base/skin.css index b01c10447..ad43304de 100644 --- a/web/skins/classic/css/base/skin.css +++ b/web/skins/classic/css/base/skin.css @@ -1044,6 +1044,9 @@ a.flip { right: 0; background: none; } +.colThumbnail img { + border-radius: 4px; +} .monitor { position: relative; } From a2949b7f2b0daed69fb7582db475fa2400797a2b Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 15 Feb 2026 12:47:24 -0500 Subject: [PATCH 11/13] fix: update heartbeat during camera close and reconnect avformat_close_input() can block for 75-90s on TCP retransmit timeout when an RTSP camera becomes unresponsive, and the connect() retry loop also lacks heartbeat updates. This causes zmwatch to kill zmc with a stale heartbeat even though the process is actively reconnecting. Add SetHeartbeatTime() calls before/after Close() and in the connect() retry loop so zmwatch knows zmc is still alive during reconnection. Co-Authored-By: Claude Opus 4.6 --- src/zmc.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/zmc.cpp b/src/zmc.cpp index 8bd0184f7..92b0ef212 100644 --- a/src/zmc.cpp +++ b/src/zmc.cpp @@ -248,6 +248,7 @@ int main(int argc, char *argv[]) { while (!monitor->connect() and !zm_terminate) { Warning("Couldn't connect to monitor %d", monitor->Id()); + monitor->SetHeartbeatTime(std::chrono::system_clock::now()); sleep(1); } if (zm_terminate) break; @@ -381,7 +382,9 @@ int main(int argc, char *argv[]) { } // end while ! zm_terminate and connected for (std::shared_ptr & monitor : monitors) { + monitor->SetHeartbeatTime(std::chrono::system_clock::now()); monitor->Close(); + monitor->SetHeartbeatTime(std::chrono::system_clock::now()); monitor->disconnect(); } From 58aa6c9a44994b50d5bfcc319ff5771818ecb2af Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 13 Feb 2026 12:17:39 -0500 Subject: [PATCH 12/13] feat: update Controls entries to use unified ONVIF.pm module Add new 'ONVIF' Controls entry with corrected brightness/contrast ranges (0-100). Update legacy entries (ONVIF Camera, Netcat ONVIF, Reolink RLC-423/411/420) to use Protocol 'ONVIF' instead of the old per-vendor module names (onvif, Netcat, Reolink). Co-Authored-By: Claude Opus 4.6 --- db/zm_create.sql.in | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index cd0a5a4d0..d44def94e 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -1121,21 +1121,21 @@ INSERT INTO `Controls` VALUES (NULL,'Loftek Sentinel','Remote','LoftekSentinel', INSERT INTO `Controls` VALUES (NULL,'Toshiba IK-WB11A','Remote','Toshiba_IK_WB11A',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,10,0,1,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'WanscamPT','Remote','Wanscam',1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,16,0,0,0,0,0,1,16,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'3S Domo N5071', 'Remote', '3S', 0, 0, 1, 0,1, 0, 1, 1, 0, 0, 9999, 0, 9999, 0, 0, 0, 1, 1, 1, 1, 0, 0, 9999, 20, 9999, 0, 0, 0, 1, 1, 1, 1, 0, 0, 9999, 1, 9999, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 64, 1, 0, 1, 1, 0, 0, 0, 0, 1, -180, 180, 40, 100, 1, 40, 100, 0, 0, 1, -180, 180, 40, 100, 1, 40, 100, 0, 0, 0, 0); -INSERT INTO `Controls` VALUES (NULL,'ONVIF Camera','Ffmpeg','onvif',0,0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,255,16,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,6,1,1,0,0,0,1,10,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); +INSERT INTO `Controls` VALUES (NULL,'ONVIF Camera','Ffmpeg','ONVIF',0,0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,255,16,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,6,1,1,0,0,0,1,10,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Foscam 9831W','Ffmpeg','FI9831W',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,16,1,1,1,1,0,0,0,1,1,0,360,0,360,1,0,4,0,0,1,0,90,0,90,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Foscam FI8918W','Ffmpeg','FI8918W',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,8,0,1,1,1,0,0,0,1,1,0,360,0,360,1,0,4,0,0,1,0,90,0,90,1,0,4,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'SunEyes SP-P1802SWPTZ','Libvlc','SPP1802SWPTZ',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,8,0,1,1,0,0,0,0,1,1,0,0,0,0,1,0,64,0,0,1,0,0,0,0,1,0,64,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Wanscam HW0025','Libvlc','WanscamHW0025', 1, 1, 1, 0,1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 16, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 350, 0, 0, 1, 0, 10, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 0, 0, 0, 0); INSERT INTO `Controls` VALUES (NULL,'IPCC 7210W','Remote','IPCC7210W', 1, 1, 1, 0,1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 16, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); INSERT INTO `Controls` VALUES (NULL,'Vivotek ePTZ','Remote','Vivotek_ePTZ',0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,1,0,5,0,0,1,0,0,0,0,1,0,5,0,0,0,0); -INSERT INTO `Controls` VALUES (NULL,'Netcat ONVIF','Ffmpeg','Netcat',0,0,1,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,100,5,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,100,5,5,0,0,0,1,255,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); +INSERT INTO `Controls` VALUES (NULL,'Netcat ONVIF','Ffmpeg','ONVIF',0,0,1,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,100,5,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,100,5,5,0,0,0,1,255,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Keekoon','Remote','Keekoon', 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 6, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); INSERT INTO `Controls` VALUES (NULL,'HikVision','Local','HikVision',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,1,1,1,1,0,0,0,1,1,0,0,0,0,1,1,100,0,0,1,0,0,0,0,1,1,100,1,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Maginon Supra IPC','cURL','MaginonIPC',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,1,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Floureon 1080P','Ffmpeg','Floureon',0,0,0,0,1,0,0,0,1,1,18,1,1,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,0,1,1,1,0,0,0,1,1,0,0,0,0,1,1,8,0,0,1,0,0,0,0,1,1,8,0,0,0,0); -INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-423','Ffmpeg','Reolink',0,0,1,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,64,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); -INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-411','Ffmpeg','Reolink',0,0,1,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); -INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-420','Ffmpeg','Reolink',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); +INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-423','Ffmpeg','ONVIF',0,0,1,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,64,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); +INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-411','Ffmpeg','ONVIF',0,0,1,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); +INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-420','Ffmpeg','ONVIF',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'D-LINK DCS-3415','Remote','DCS3415',0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'D-Link DCS-5020L','Remote','DCS5020L',1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,24,1,0,1,1,1,0,1,0,1,0,0,1,30,0,0,0,0,0,1,0,0,1,30,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'IOS Camera','Ffmpeg','IPCAMIOS',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); @@ -1145,6 +1145,7 @@ INSERT INTO `Controls` VALUES (NULL,'PSIA','Remote','PSIA',0,0,0,0,1,0,0,1,0,0,0 INSERT INTO `Controls` VALUES (NULL,'Dahua','Ffmpeg','Dahua',0,0,1,1,1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,1,1,1,1,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'FOSCAMR2C','Libvlc','FOSCAMR2C',1,1,1,0,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,1,12,0,1,1,1,0,0,0,1,1,NULL,NULL,NULL,NULL,1,0,4,0,NULL,1,NULL,NULL,NULL,NULL,1,0,4,0,NULL,0,0); INSERT INTO `Controls` VALUES (NULL,'Amcrest HTTP API','Ffmpeg','Amcrest_HTTP',0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,5,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,5); +INSERT INTO `Controls` VALUES (NULL,'ONVIF','Ffmpeg','ONVIF',0,0,1,1,1,0,0,0,1,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,1,0,1,0,0,0,100,1,1,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,1,0,1,0,0,0,100,1,1,0,NULL,NULL,1,20,1,1,1,1,1,0,1,1,1,NULL,NULL,NULL,NULL,0,NULL,NULL,0,NULL,1,NULL,NULL,NULL,NULL,0,NULL,NULL,0,NULL,0,0); -- -- Add some monitor preset values From 113f1435f6ffa158e9737133e23ea9100639ec02 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 13 Feb 2026 12:33:46 -0500 Subject: [PATCH 13/13] fix: restore filter Name and UserId after save/reload The post-save redirect included filter query params in the URL, which caused the view to populate from request data instead of loading from the database. Since the querystring omitted Name and UserId, both fields appeared empty/wrong after reload. Remove the querystring from the redirect so the view loads the saved filter from the database. Co-Authored-By: Claude Opus 4.6 --- web/includes/actions/filter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/includes/actions/filter.php b/web/includes/actions/filter.php index b6159d7c5..15e047651 100644 --- a/web/includes/actions/filter.php +++ b/web/includes/actions/filter.php @@ -112,7 +112,7 @@ if (isset($_REQUEST['object']) and ($_REQUEST['object'] == 'filter')) { $filter->control('start'); } global $redirect; - $redirect = '?view=filter&Id='.$_REQUEST['Id'].$filter->querystring('filter', '&'); + $redirect = '?view=filter&Id='.$_REQUEST['Id']; } else if ($action == 'control') { if ( $_REQUEST['command'] == 'start'