From aecd277edda07fbca98d3f50a81959807cf6c351 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:22:40 +0000 Subject: [PATCH 01/10] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0149f48c3..98d6982ab 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml @@ -68,7 +68,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl- @@ -83,4 +83,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 83cf67071262eecdc98ce23a7a0c2ce35913b93d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 18:31:03 +0000 Subject: [PATCH 02/10] Bump actions/dependency-review-action from 3 to 4 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 3 to 4. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/depsreview.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/depsreview.yaml b/.github/workflows/depsreview.yaml index b9945082d..b9d6d20ff 100644 --- a/.github/workflows/depsreview.yaml +++ b/.github/workflows/depsreview.yaml @@ -11,4 +11,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@v4 - name: 'Dependency Review' - uses: actions/dependency-review-action@v3 + uses: actions/dependency-review-action@v4 From 7ae337927500b77fc4a78e79e5cd7a5a00a7f57d Mon Sep 17 00:00:00 2001 From: Andrew Bauer Date: Wed, 4 Jun 2025 07:56:58 -0500 Subject: [PATCH 03/10] Update zoneminder.spec - build against pcre2 The original pcre library is no longer supported in rhel, moving forward. Use pcre2 instead. --- distros/redhat/zoneminder.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index 40b45f1cf..c89701cdd 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -46,7 +46,7 @@ BuildRequires: polkit-devel BuildRequires: cmake BuildRequires: gnutls-devel BuildRequires: bzip2-devel -BuildRequires: pcre-devel +BuildRequires: pcre2-devel BuildRequires: libjpeg-turbo-devel BuildRequires: findutils BuildRequires: coreutils From 568fe410aede107d84e1612ce32463e022965afb Mon Sep 17 00:00:00 2001 From: Andrew Bauer Date: Wed, 4 Jun 2025 09:30:30 -0500 Subject: [PATCH 04/10] zoneminder.spec - add empty %check, to keep rpmlint happy --- distros/redhat/zoneminder.spec | 3 +++ 1 file changed, 3 insertions(+) diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index c89701cdd..2865f0936 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -246,6 +246,9 @@ ln -s ../../../../../../../..%{_sysconfdir}/pki/tls/certs/ca-bundle.crt %{buildr # Handle the polkit file differently for web server agnostic support (see post) rm -f %{buildroot}%{_datadir}/polkit-1/rules.d/com.zoneminder.systemctl.rules +%check +# Nothing to do. No tests exist. + %post common # Initial installation if [ $1 -eq 1 ] ; then From 5406c6b72ee2fd3935983fd1a1b0aed2d91ee8f9 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 5 Jun 2025 14:42:19 -0400 Subject: [PATCH 05/10] add iproute2 to depends to get 'ip' command in build system --- distros/ubuntu2004/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distros/ubuntu2004/control b/distros/ubuntu2004/control index 12eb558f8..43d75ac28 100644 --- a/distros/ubuntu2004/control +++ b/distros/ubuntu2004/control @@ -78,7 +78,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,policykit-1|pkexec ,rsyslog | system-log-daemon ,zip - ,arp-scan + ,arp-scan, iproute2 ,libcrypt-eksblowfish-perl ,libdata-entropy-perl ,libvncclient1|libvncclient0 From 9d0f2c25bba2371ae5128e52c570c3fed392b685 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 11 Jun 2025 14:51:43 -0400 Subject: [PATCH 06/10] Small debug cleanup --- src/zm_eventstream.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index 787d9181d..80b8dc440 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -306,7 +306,6 @@ bool EventStream::loadEventData(uint64_t event_id) { last_timestamp = event_data->start_time; event_data->frame_count ++; } else { - Debug(1, "EIther no endtime or no duration, frame_count %d, last_id %d", event_data->frame_count, last_id); delta = std::chrono::duration_cast((event_data->end_time - last_timestamp)/(event_data->frame_count-last_id)); Debug(1, "Setting delta from endtime %f - %f / %d - %d", FPSeconds(event_data->end_time.time_since_epoch()).count(), From d97b37f83fc9c5dadfe92dc405af9dce352dc332 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 11 Jun 2025 14:57:54 -0400 Subject: [PATCH 07/10] Don't do pagination in events.This may break users of API but pagination should only happen if asked for --- web/api/app/Controller/EventsController.php | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/web/api/app/Controller/EventsController.php b/web/api/app/Controller/EventsController.php index 9a1181253..935c7d6d8 100644 --- a/web/api/app/Controller/EventsController.php +++ b/web/api/app/Controller/EventsController.php @@ -99,17 +99,8 @@ class EventsController extends AppController { } $settings['conditions'] = array($conditions, $mon_options); - // How many events to return - $this->loadModel('Config'); - $limit = $this->Config->find('list', array( - 'conditions' => array('Name' => 'ZM_WEB_EVENTS_PER_PAGE'), - 'fields' => array('Name', 'Value') - )); - $this->Paginator->settings = $settings; - $events = $this->Paginator->paginate('Event'); - - // For each event, get the frameID which has the largest score - // also add FS path + $events = $this->Event->find('all', $settings); + // For each event, get the frameID which has the largest score also add FS path foreach ( $events as $key => $value ) { $EventObj = new ZM\Event($value['Event']); From 1997dfcc1c4f78c309dbcca816caa38d95bd3c5e Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 11 Jun 2025 15:03:05 -0400 Subject: [PATCH 08/10] Update montagereview from another dev branch. The main gist is that we no longer pre-populate events in the js code included with html. We load by ajax now from the API. This reduces the amount of ram used by php. The rest is basically code and white space cleanups. --- web/skins/classic/views/js/montagereview.js | 518 +++++++++++------- .../classic/views/js/montagereview.js.php | 43 +- 2 files changed, 362 insertions(+), 199 deletions(-) diff --git a/web/skins/classic/views/js/montagereview.js b/web/skins/classic/views/js/montagereview.js index 7c7641086..d9b33a077 100644 --- a/web/skins/classic/views/js/montagereview.js +++ b/web/skins/classic/views/js/montagereview.js @@ -1,3 +1,12 @@ +"use strict"; + + +var LOADING = 1; + +var ajax = null; +var wait_for_events_interval = null; + + function evaluateLoadTimes() { if (liveMode != 1 && currentSpeed == 0) return; // don't evaluate when we are not moving as we can do nothing really fast. @@ -45,26 +54,33 @@ function evaluateLoadTimes() { $j('#fps').text("Display refresh rate is " + (1000 / currentDisplayInterval).toFixed(1) + " per second, avgFrac=" + avgFrac.toFixed(3) + "."); } // end evaluateLoadTimes() -function findEventByTime(arr, time, debug) { +function findEventByTime(arr, time, debug=false) { let start = 0; let end = arr.length-1; // -1 because 0 based indexing - //console.log("looking for "+time+" Start: " + arr[start].StartTimeSecs + ' End: ' + arr[end].EndTimeSecs); + if (debug) { + if ( arr.length ) { + console.log("looking for "+time+" Start: " + arr[start].StartTimeSecs + ' End: ' + arr[end].EndTimeSecs); + } else { + console.log("looking for "+time+" but nothing in arr"); + } + } // Iterate while start not meets end while ((start <= end) && (arr[start].StartTimeSecs <= time) && (!arr[end].EndTimeSecs || (arr[end].EndTimeSecs >= time))) { - //console.log("looking for "+time+" Start: " + arr[start].StartTimeSecs + ' End: ' + arr[end].EndTimeSecs); + if (debug) + console.log("looking for "+time+" Start: " + arr[start].StartTimeSecs + ' End: ' + arr[end].EndTimeSecs); // Find the middle index const middle = Math.floor((start + end)/2); const zm_event = arr[middle]; // If element is present at mid, return True - //console.log(middle, zm_event, time); + if (debug) console.log(middle, zm_event, time); if ((zm_event.StartTimeSecs <= time) && (!zm_event.EndTimeSecs || (zm_event.EndTimeSecs >= time))) { - //console.log("Found it at ", zm_event); + if (debug) console.log("Found it at ", zm_event); return zm_event; } - //console.log("Didn't find it looking for "+time+" Start: " + zm_event.StartTimeSecs + ' End: ' + zm_event.EndTimeSecs); + if (debug) console.log("Didn't find it looking for "+time+" Start: " + zm_event.StartTimeSecs + ' End: ' + zm_event.EndTimeSecs); // Else look in left or right half accordingly if (zm_event.StartTimeSecs < time) { start = middle + 1; @@ -77,7 +93,7 @@ function findEventByTime(arr, time, debug) { return false; } // end function findEventByTime -function findFrameByTime(arr, time, debug) { +function findFrameByTime(arr, time, debug=false) { if (!arr) { console.log("No array in findFrameByTime"); return false; @@ -149,7 +165,7 @@ function findFrameByTime(arr, time, debug) { break; } } // end while - if (debug) console.log("Didn't find it"); + if (debug) console.log("Didn't find frame it"); return false; } // end function findFrameByTime(arr, time, debug=false) @@ -180,7 +196,6 @@ function getFrame(monId, time, last_Frame) { let Event = findEventByTime(events_for_monitor[monId], time, false); if (Event === false) { - // This might be better with a binary search for (let i=0, len=events_for_monitor[monId].length; i 100) { scale = 100; } else { - scale = 10 * parseInt(scale/10); + scale = 10 * parseInt(scale/10); // Round to nearest 10 + // May need to limit how small we can go to maintain fidelity } - // Storage[0] is guaranteed to exist as we make sure it is there in montagereview.js.php const storage = Storage[e.StorageId] ? Storage[e.StorageId] : Storage[0]; // monitorServerId may be 0, which gives us the default Server entry const server = storage.ServerId ? Servers[storage.ServerId] : Servers[monitorServerId[monId]]; - return server.PathToZMS + '?mode=jpeg&frames=1&event=' + Frame.EventId + '&frame='+frame_id + + return server.PathToZMS + '?mode=jpeg&event=' + Frame.EventId + '&frame='+frame_id + //"&width=" + monitorCanvasObj[monId].width + //"&height=" + monitorCanvasObj[monId].height + "&scale=" + scale + "&frames=1" + "&rate=" + 100*speeds[speedIndex] + '&' + auth_relay; - - return server.PathToIndex + - '?view=image&eid=' + Frame.EventId + '&fid='+frame_id + - "&width=" + monitorCanvasObj[monId].width + - "&height=" + monitorCanvasObj[monId].height; } // end found Frame return ''; } // end function getImageSource // callback when loading an image. Will load itself to the canvas, or draw no data function imagedone( obj, monId, success ) { - if ( success ) { + if (success) { const canvasCtx = monitorCanvasCtx[monId]; const canvasObj = monitorCanvasObj[monId]; @@ -369,42 +388,25 @@ function imagedone( obj, monId, success ) { return; } -function loadNoData( monId ) { - if ( monId ) { - var canvasCtx = monitorCanvasCtx[monId]; - var canvasObj = monitorCanvasObj[monId]; - canvasCtx.fillStyle="white"; - canvasCtx.fillRect(0, 0, canvasObj.width, canvasObj.height); - var textSize=canvasObj.width * 0.15; - var text="No Event"; - canvasCtx.font = "600 " + textSize.toString() + "px Arial"; - canvasCtx.fillStyle="black"; - var textWidth = canvasCtx.measureText(text).width; - canvasCtx.fillText(text, canvasObj.width/2 - textWidth/2, canvasObj.height/2); - } else { - console.log("No monId in loadNoData"); - } -} - -function writeText( monId, text ) { - if ( monId ) { - var canvasCtx = monitorCanvasCtx[monId]; - var canvasObj = monitorCanvasObj[monId]; +function writeText(monId, text) { + if (monId) { + const canvasCtx = monitorCanvasCtx[monId]; + const canvasObj = monitorCanvasObj[monId]; //canvasCtx.fillStyle="white"; //canvasCtx.fillRect(0, 0, canvasObj.width, canvasObj.height); - var textSize=canvasObj.width * 0.15; - canvasCtx.font = "600 " + textSize.toString() + "px Arial"; - canvasCtx.fillStyle="white"; + var textSize = canvasObj.width * 0.15; + canvasCtx.font = '600 ' + textSize.toString() + "px Arial"; + canvasCtx.fillStyle = 'white'; var textWidth = canvasCtx.measureText(text).width; canvasCtx.fillText(text, canvasObj.width/2 - textWidth/2, canvasObj.height/2); } else { - console.log("No monId in loadNoData"); + console.log('No monId in writeText'); } } // Either draws the -function loadImage2Monitor( monId, url ) { - if ( monitorLoading[monId] && monitorImageObject[monId].src != url ) { +function loadImage2Monitor(monId, url) { + if ( monitorLoading[monId] && (monitorImageObject[monId].src != url) ) { // never queue the same image twice (if it's loading it has to be defined, right? monitorLoadingStageURL[monId] = url; // we don't care if we are overriting, it means it didn't change fast enough } else { @@ -430,91 +432,94 @@ function timerFire() { console.log("Turn off interrupts timerInterfave" + timerInterval); } - if ( (currentSpeed > 0 || liveMode != 0) && ! timerObj ) { - timerObj = setInterval(timerFire, timerInterval); // don't fire out of live mode if speed is zero - } - if (liveMode) { outputUpdate(currentTimeSecs); // In live mode we basically do nothing but redisplay } else if (currentTimeSecs + playSecsPerInterval >= maxTimeSecs) { // beyond the end just stop console.log("Current time " + currentTimeSecs + " + " + playSecsPerInterval + " >= " + maxTimeSecs + " so stopping"); - if (speedIndex) setSpeed(0); + setSpeed(0); outputUpdate(currentTimeSecs); - } else { - //console.log("Current time " + currentTimeSecs + " + " + playSecsPerInterval); + } else if (playSecsPerInterval || (currentTimeSecs==minTimeSecs)) { + console.log("Current time " + currentTimeSecs + " + " + playSecsPerInterval + " + " + timerInterval); outputUpdate(playSecsPerInterval + currentTimeSecs); + } else { + console.log("Current time " + currentTimeSecs + " + " + playSecsPerInterval); + } + + if ((currentSpeed > 0 || liveMode != 0) && !timerObj) { + timerObj = setInterval(timerFire, timerInterval); // don't fire out of live mode if speed is zero + } else { + console.log("CurrentSpeed", currentSpeed, "liveMode", liveMode, timerObj); } return; -} +} // end function timerFire() // val is seconds? function drawSliderOnGraph(val) { - var sliderWidth=10; - var sliderLineWidth=1; - var sliderHeight=cHeight; + if (numMonitors <= 0) { + return; + } if ( liveMode == 1 ) { val = Math.floor( Date.now() / 1000); } - // Set some sizes + var sliderWidth=10; + var sliderLineWidth=1; + var sliderHeight=cHeight; + // Set some sizes var labelpx = Math.max( 6, Math.min( 20, parseInt(cHeight * timeLabelsFractOfRow / (numMonitors+1)) ) ); var labbottom = parseInt(cHeight * 0.2 / (numMonitors+1)).toString() + "px"; // This is positioning same as row labels below, but from bottom so 1-position var labfont = labelpx + "px"; // set this like below row labels - if ( numMonitors > 0 ) { - // if we have no data to display don't do the slider itself - var sliderX = parseInt((val - minTimeSecs) / rangeTimeSecs * cWidth - sliderWidth/2); // position left side of slider - if ( sliderX < 0 ) sliderX = 0; - if ( sliderX + sliderWidth > cWidth ) { - sliderX = cWidth-sliderWidth-1; - } + // if we have no data to display don't do the slider itself + let sliderX = parseInt((val - minTimeSecs) / rangeTimeSecs * cWidth - sliderWidth/2); // position left side of slider + if ( sliderX < 0 ) sliderX = 0; + if ( sliderX + sliderWidth > cWidth ) sliderX = cWidth-sliderWidth-1; - // If we have data already saved first restore it from LAST time + // If we have data already saved first restore it from LAST time - if ( typeof underSlider !== 'undefined' ) { - ctx.putImageData(underSlider, underSliderX, 0, 0, 0, sliderWidth, sliderHeight); - underSlider = undefined; - } - if ( liveMode == 0 ) { - // we get rid of the slider if we switch to live (since it may not be in the "right" place) - // Now save where we are putting it THIS time - underSlider = ctx.getImageData(sliderX, 0, sliderWidth, sliderHeight); - // And add in the slider' - ctx.lineWidth = sliderLineWidth; - ctx.strokeStyle = 'yellow'; - // looks like strokes are on the outside (or could be) so shrink it by the line width so we replace all the pixels - ctx.strokeRect(sliderX+sliderLineWidth, sliderLineWidth, sliderWidth - 2*sliderLineWidth, sliderHeight - 2*sliderLineWidth); - underSliderX = sliderX; - } - var o = document.getElementById('scruboutput'); - if ( liveMode == 1 ) { - o.innerHTML = "Live Feed @ " + (1000 / currentDisplayInterval).toFixed(1) + " fps"; - o.style.color = "red"; - } else { - o.innerHTML = secs2dbstr(val); - o.style.color = 'white'; - } - o.style.position = "absolute"; - o.style.bottom = labbottom; - o.style.font = labfont; - // try to get length and then when we get too close to the right switch to the left - var len = o.offsetWidth; - var x; - if ( sliderX > cWidth/2 ) { - x = sliderX - len - 10; - } else { - x = sliderX + 10; - } - o.style.left = x.toString() + "px"; + if ( typeof underSlider !== 'undefined' ) { + ctx.putImageData(underSlider, underSliderX, 0, 0, 0, sliderWidth, sliderHeight); + underSlider = undefined; } + if ( liveMode == 0 ) { + // we get rid of the slider if we switch to live (since it may not be in the "right" place) + // Now save where we are putting it THIS time + underSlider = ctx.getImageData(sliderX, 0, sliderWidth, sliderHeight); + // And add in the slider' + ctx.lineWidth = sliderLineWidth; + ctx.strokeStyle = 'yellow'; + // looks like strokes are on the outside (or could be) so shrink it by the line width so we replace all the pixels + ctx.strokeRect(sliderX+sliderLineWidth, sliderLineWidth, sliderWidth - 2*sliderLineWidth, sliderHeight - 2*sliderLineWidth); + underSliderX = sliderX; + } + var o = document.getElementById('scruboutput'); + if ( liveMode == 1 ) { + o.innerHTML = 'Live Feed @ ' + (1000 / currentDisplayInterval).toFixed(1) + ' fps'; + o.style.color = 'red'; + } else { + o.innerHTML = secs2dbstr(val); + o.style.color = 'white'; + } + o.style.position = 'absolute'; + o.style.bottom = labbottom; + o.style.font = labfont; + // try to get length and then when we get too close to the right switch to the left + var len = o.offsetWidth; + var x; + if ( sliderX > cWidth/2 ) { + x = sliderX - len - 10; + } else { + x = sliderX + 10; + } + o.style.left = x.toString() + "px"; // This displays (or not) the left/right limits depending on how close the slider is. // Because these change widths if the slider is too close, use the slider width as an estimate for the left/right label length (i.e. don't recalculate len from above) // If this starts to collide increase some of the extra space - var o = document.getElementById('scrubleft'); + o = document.getElementById('scrubleft'); o.innerHTML = secs2dbstr(minTimeSecs); o.style.position = "absolute"; o.style.bottom = labbottom; @@ -532,7 +537,7 @@ function drawSliderOnGraph(val) { o.style.display = "inline-flex"; // safari won't take this but will just ignore } - var o = document.getElementById('scrubright'); + o = document.getElementById('scrubright'); o.innerHTML = secs2dbstr(maxTimeSecs); o.style.position = "absolute"; o.style.bottom = labbottom; @@ -547,6 +552,40 @@ function drawSliderOnGraph(val) { } } +function drawFrameOnGraph(frame) { + if (!frame.Score) return; + // Now put in scored frames (if any) + var x1 = parseInt( (frame.TimeStampSecs - minTimeSecs) / rangeTimeSecs * cWidth); // round low end down + var x2 = parseInt( (frame.TimeStampSecs - minTimeSecs) / rangeTimeSecs * cWidth + 0.5 ); // round up + if (x2-x1 < 2) x2=x1+2; // So it is visible make them all at least this number of seconds wide + ctx.fillStyle=monitorColour[Event.MonitorId]; + //ctx.fillStyle = '#ff0000'; + ctx.globalAlpha = 0.4 + 0.6 * (1 - frame.Score/maxScore); // Background is scaled but even lowest is twice as dark as the background + const MonitorId = events[frame.EventId].MonitorId; + ctx.fillRect(x1, monitorIndex[MonitorId]*rowHeight, x2-x1, rowHeight-2); + //console.log("Drew frame from ", x1, MonitorId, monitorIndex[MonitorId]*rowHeight, x2-x1, rowHeight); +} + +function drawEventOnGraph(Event) { + // round low end down + const x1 = parseInt((Event.StartTimeSecs - minTimeSecs) / rangeTimeSecs * cWidth); + if (!Event.EndTimeSecs) Event.EndTimeSecs = maxTimeSecs; + // round high end up to be sure consecutive ones connect + const x2 = parseInt((Event.EndTimeSecs - minTimeSecs) / rangeTimeSecs * cWidth + 0.5 ); + if (!monitorColour[Event.MonitorId]) { + console.log("No colour for ", Event.MonitorId, monitorColour); + ctx.fillStyle = '#43bcf2'; + } else { + ctx.fillStyle = monitorColour[Event.MonitorId]; + } + ctx.globalAlpha = 0.2; // light color for background + // Erase any overlap so it doesn't look artificially darker + ctx.clearRect(x1, monitorIndex[Event.MonitorId]*rowHeight, x2-x1, rowHeight); + ctx.fillRect(x1, monitorIndex[Event.MonitorId]*rowHeight, x2-x1, rowHeight-2); + //outputUpdate(currentTimeSecs); + console.log("Drew event from ", x1, monitorIndex[Event.MonitorId]*rowHeight, x2-x1, rowHeight); +} + function drawGraph() { var divWidth = document.getElementById('timelinediv').clientWidth; canvas.width = cWidth = divWidth; // Let it float and determine width (it should be sized a bit smaller percentage of window) @@ -567,38 +606,24 @@ function drawGraph() { underSlider = undefined; return; } - var rowHeight = parseInt(cHeight / (numMonitors + 1) ); // Leave room for a scale of some sort + rowHeight = parseInt(cHeight / (numMonitors + 1) ); // Leave room for a scale of some sort // first fill in the bars for the events (not alarms) - for ( var event_id in events ) { - var Event = events[event_id]; - - // round low end down - var x1 = parseInt((Event.StartTimeSecs - minTimeSecs) / rangeTimeSecs * cWidth); - var x2 = parseInt((Event.EndTimeSecs - minTimeSecs) / rangeTimeSecs * cWidth + 0.5 ); // round high end up to be sure consecutive ones connect - ctx.fillStyle = monitorColour[Event.MonitorId]; - ctx.globalAlpha = 0.2; // light color for background - ctx.clearRect(x1, monitorIndex[Event.MonitorId]*rowHeight, x2-x1, rowHeight); // Erase any overlap so it doesn't look artificially darker - ctx.fillRect(x1, monitorIndex[Event.MonitorId]*rowHeight, x2-x1, rowHeight); - - for ( var frame_id in Event.FramesById ) { - var Frame = Event.FramesById[frame_id]; - if ( ! Frame.Score ) { - continue; - } - - // Now put in scored frames (if any) - var x1=parseInt( (Frame.TimeStampSecs - minTimeSecs) / rangeTimeSecs * cWidth); // round low end down - var x2=parseInt( (Frame.TimeStampSecs - minTimeSecs) / rangeTimeSecs * cWidth + 0.5 ); // round up - if (x2-x1 < 2) x2=x1+2; // So it is visible make them all at least this number of seconds wide - //ctx.fillStyle=monitorColour[Event.MonitorId]; - ctx.globalAlpha = 0.4 + 0.6 * (1 - Frame.Score/maxScore); // Background is scaled but even lowest is twice as dark as the background - ctx.fillRect(x1, monitorIndex[Event.MonitorId]*rowHeight, x2-x1, rowHeight); - } // end foreach frame + console.log(events); + for (const event_id in events) { + const Event = events[event_id]; + drawEventOnGraph(Event); + if (Event.FramesById) { + for (const frame_id in Event.FramesById ) { + const Frame = Event.FramesById[frame_id]; + if (!Frame.Score) continue; + drawFrameOnGraph(Frame); + } // end foreach frame + } } // end foreach Event - for ( var i=0; i < numMonitors; i++ ) { + for (let i=0; i < numMonitors; i++) { // Note that this may be a sparse array ctx.font = parseInt(rowHeight * timeLabelsFractOfRow).toString() + "px Georgia"; ctx.fillStyle = "white"; @@ -624,7 +649,7 @@ function redrawScreen() { var scaleDiv = $j('#ScaleDiv'); var fit = $j('#fit'); - if ( liveMode == 1 ) { + if (liveMode == 1) { // if we are not in live view switch to history -- this has to come before fit in case we re-establish the timeline dateTimeDiv.hide(); speedDiv.hide(); @@ -646,12 +671,11 @@ function redrawScreen() { panLeft.show(); panRight.show(); downloadVideo.show(); - drawGraph(); } var monitors = $j('#monitors'); - if ( fitMode == 1 ) { + if (fitMode == 1) { var fps = $j('#fps'); var vh = window.innerHeight; var mh = (vh - monitors.position().top - fps.outerHeight()); @@ -680,13 +704,14 @@ function redrawScreen() { } // end function redrawScreen function outputUpdate(time) { - drawSliderOnGraph(time); - for ( var i=0; i < numMonitors; i++ ) { - var src = getImageSource(monitorPtr[i], time); - //console.log("New image src: " + src); - loadImage2Monitor(monitorPtr[i], src); + if (Object.keys(events).length !== 0) { + for ( let i=0; i < numMonitors; i++ ) { + const src = getImageSource(monitorPtr[i], time); + loadImage2Monitor(monitorPtr[i], src); + } } currentTimeSecs = time; + drawSliderOnGraph(time); } // Found this here: http://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-element @@ -728,8 +753,9 @@ function tmove(event) { function mmove(event) { if ( mouseisdown ) { // only do anything if the mouse is depressed while on the sheet - var sec = Math.floor(minTimeSecs + rangeTimeSecs / event.target.width * event.target.relMouseCoords(event).x); - outputUpdate(sec); + const relx = event.target.relMouseCoords(event).x; + const sec = Math.floor(minTimeSecs + rangeTimeSecs / event.target.width * relx); + if (parseInt(sec)) outputUpdate(sec); } } @@ -748,7 +774,7 @@ function secs2inputstr(s) { } function secs2dbstr(s) { - if ( ! parseInt(s) ) { + if (!parseInt(s)) { console.log("Invalid value for " + s + " seconds"); return ''; } @@ -761,7 +787,7 @@ function secs2dbstr(s) { } function setFit(value) { - fitMode=value; + fitMode = value; redrawScreen(); } @@ -896,6 +922,7 @@ function click_zoomout() { function click_panleft() { minTimeSecs = parseInt(minTimeSecs - rangeTimeSecs/2); maxTimeSecs = minTimeSecs + rangeTimeSecs - 1; + currentTimeSecs -= rangeTimeSecs/2; clicknav(minTimeSecs, maxTimeSecs, 0); } function click_panright() { @@ -941,7 +968,7 @@ function allnon() { clicknav(0, 0, 0); } -// >>>>>>>>>>>>>>>> Handles individual monitor clicks and navigation to the standard event/watch display +// Handles individual monitor clicks and navigation to the standard event/watch display function showOneMonitor(monId, event) { // link out to the normal view of one event's data @@ -1019,19 +1046,134 @@ function changeDateTime(e) { // Reloading can take a while, so stop interrupts to reduce load clearInterval(timerObj); timerObj = null; - const form = $j('#montagereview_form'); - console.log(form.serialize()); - var uri = "?" + form.serialize() + zoomStr + "&scale=" + $j("#scaleslider")[0].value + "&speed=" + speeds[$j("#speedslider")[0].value]; + drawGraph(); + + console.log("ChangeDateTime"); + loadEventData(); + console.log("timerFire from changeDateTime"); + wait_for_events(); + + //const form = $j('#montagereview_form'); + //console.log(form.serialize()); + + //var uri = "?" + form.serialize() + zoomStr + "&scale=" + $j("#scaleslider")[0].value + "&speed=" + speeds[$j("#speedslider")[0].value]; //var uri = "?view=" + currentView + fitStr + minStr + maxStr + liveStr + zoomStr + "&scale=" + $j("#scaleslider")[0].value + "&speed=" + speeds[$j("#speedslider")[0].value]; - window.location = uri; + //window.location = uri; } -// >>>>>>>>> Initialization that runs on window load by being at the bottom +function loadEventData(e) { + LOADING = true; + + var monitors = monitorData; + var data = {}; + var mon_ids = []; + for (let monitor_i=0, monitors_len=monitors.length; monitor_i < monitors_len; monitor_i++) { + const monitor = monitors[monitor_i]; + monitorLoading[monitor.Id] = false; + mon_ids[mon_ids.length] = monitor.Id; + } + + var url = Servers[serverId].urlToApi()+'/events/index'; + $j('#fieldsTable input,#fieldsTable select').each(function(index) { + const el = $j(this); + const val = el.val(); + if (val && (!Array.isArray(val) || val.length)) { + const name = el.attr('name'); + + if (name) { + const found = name.match(/filter\[Query\]\[terms\]\[(\d)+\]\[val\]/); + if (found) { + const attr_name = 'filter[Query][terms]['+found[1]+'][attr]'; + const attr = this.form.elements[attr_name]; + const op_name = 'filter[Query][terms]['+found[1]+'][op]'; + const op = this.form.elements[op_name]; + if (attr) { + url += '/'+attr.value+' '+op.value+':'+encodeURIComponent(val); + } else { + console.log('No attr for '+attr_name); + } + //} else { + //console.log("No match for " + name); + } + data[name] = val; + const cookie = el.attr('data-cookie'); + if (cookie) setCookie(cookie, val, 3600); + } // end if name + } // end if val + }); + + function receive_events(data) { + if (data.result == 'Error') { + alert(data.message); + return; + } + if (!data.events) { + console.log(data); + return; + } + console.log("Event data ", data); + + if (data.events.length) { + const event_list = {}; + for (let i=0, len = data.events.length; ievent + } + events_by_monitor_id[ev.MonitorId].push(ev.Id); + events_for_monitor[ev.MonitorId].push(ev); + drawEventOnGraph(ev); + event_list[ev.Id] = ev; + events[ev.id] = ev; + } + loadFrames(event_list); + } + } // end function receive_events + + if (ajax) ajax.abort(); + LOADING = false; + + if (mon_ids.length) { + for (let i=0; i < mon_ids.length; i++) { + ajax = $j.ajax({ + url: url+ '/MonitorId:'+mon_ids[i]+ '.json'+'?'+auth_relay, + method: 'GET', + //url: thisUrl + '?view=request&request=events&task=query&sort=Id&order=ASC', + //data: data, + timeout: 0, + success: receive_events, + error: function(jqXHR) { + ajax = null; + console.log("error", jqXHR); + //logAjaxFail(jqXHR); + //$j('#eventTable').bootstrapTable('refresh'); + } + }); + } // end foreach monitor + } else { + ajax = $j.ajax({ + url: url+'.json'+'?'+auth_relay, + method: 'GET', + //url: thisUrl + '?view=request&request=events&task=query&sort=Id&order=ASC', + //data: data, + timeout: 0, + success: receive_events, + error: function(jqXHR) { + ajax = null; + console.log("error", jqXHR); + //logAjaxFail(jqXHR); + //$j('#eventTable').bootstrapTable('refresh'); + } + }); + } + return; +} // end function loadEventData function initPage() { if (!liveMode) { - load_Frames(events); canvas = document.getElementById('timeline'); canvas.addEventListener('mousemove', mmove, false); @@ -1041,59 +1183,38 @@ function initPage() { canvas.addEventListener('mouseout', mout, false); ctx = canvas.getContext('2d', {willReadFrequently: true}); + + // draw an empty timeline drawGraph(); } - for ( let i = 0, len = monitorPtr.length; i < len; i += 1 ) { + for (let i = 0, len = monitorPtr.length; i < len; i += 1) { const monId = monitorPtr[i]; if (!monId) continue; monitorCanvasObj[monId] = document.getElementById('Monitor'+monId); if ( !monitorCanvasObj[monId] ) { alert("Couldn't find DOM element for Monitor" + monId + "monitorPtr.length=" + len); - } else { - monitorCanvasCtx[monId] = monitorCanvasObj[monId].getContext('2d'); - const imageObject = monitorImageObject[monId] = new Image(); - imageObject.monId = monId; - imageObject.onload = function() { - imagedone(this, this.monId, true); - }; - imageObject.onerror = function() { - imagedone(this, this.monId, false); - }; - loadImage2Monitor(monId, monitorImageURL[monId]); - monitorCanvasObj[monId].addEventListener('click', clickMonitor, false); + continue; } + monitorCanvasCtx[monId] = monitorCanvasObj[monId].getContext('2d'); + const imageObject = monitorImageObject[monId] = new Image(); + imageObject.monId = monId; + imageObject.onload = function() { + imagedone(this, this.monId, true); + }; + imageObject.onerror = function() { + imagedone(this, this.monId, false); + }; + if (liveMode) loadImage2Monitor(monId, monitorImageURL[monId]); + monitorCanvasObj[monId].addEventListener('click', clickMonitor, false); } // end foreach monitor setSpeed(speedIndex); //setFit(fitMode); // will redraw //setLive(liveMode); // will redraw + loadEventData(); redrawScreen(); - /* - $j('#minTime').datetimepicker({ - timeFormat: "HH:mm:ss", - dateFormat: "yy-mm-dd", - maxDate: +0, - constrainInput: false, - onClose: function(newDate, oldData) { - if (newDate !== oldData.lastVal) { - changeDateTime(); - } - } - }); - $j('#maxTime').datetimepicker({ - timeFormat: "HH:mm:ss", - dateFormat: "yy-mm-dd", - minDate: minTime, - maxDate: +0, - constrainInput: false, - onClose: function(newDate, oldData) { - if ( newDate !== oldData.lastVal ) { - changeDateTime(); - } - } - }); - */ + $j('#scaleslider').bind('change', function() { setScale(this.value); }); @@ -1126,6 +1247,19 @@ function initPage() { el.on('change', changeDateTime); } }); + + wait_for_events(); +} + +function wait_for_events() { + if (Object.keys(events).length === 0) { + if (!wait_for_events_interval) + wait_for_events_interval = setInterval(wait_for_events, 1000); + } else { + clearInterval(wait_for_events_interval); + wait_for_events_interval = null; + timerFire(); + } } function takeSnapshot() { @@ -1156,7 +1290,7 @@ window.addEventListener("resize", redrawScreen, {passive: true}); window.addEventListener('DOMContentLoaded', initPage); /* Expects and Object, not an array, of EventId=>Event mappings. */ -function load_Frames(zm_events) { +function loadFrames(zm_events) { console.log("Loading frames", zm_events); return new Promise(function(resolve, reject) { const url = Servers[serverId].urlToApi()+'/frames/index'; @@ -1219,4 +1353,4 @@ function load_Frames(zm_events) { } // end while zm_events.legtnh } // end Promise ); -} // end function load_Frames(Event) +} // end function loadFrames(Event) diff --git a/web/skins/classic/views/js/montagereview.js.php b/web/skins/classic/views/js/montagereview.js.php index d96330ed0..fb827874e 100644 --- a/web/skins/classic/views/js/montagereview.js.php +++ b/web/skins/classic/views/js/montagereview.js.php @@ -31,6 +31,7 @@ var fitMode=; // slider scale, which is only for replay and relative to real time var currentSpeed=; var speedIndex=; +var lastSpeedIndex=0; // will be set based on performance, this is the display interval in milliseconds // for history, and fps for live, and dynamically determined (in ms) @@ -58,6 +59,7 @@ if (!$liveMode) { echo "const events = {\n"; $EventsById = array(); +if (0) { $result = dbQuery($eventsSql); if ($result) { while ( $event = $result->fetch(PDO::FETCH_ASSOC) ) { @@ -67,14 +69,15 @@ if (!$liveMode) { $events_by_monitor_id = array(); + $eventMaxSecs = 0; foreach ($EventsById as $event_id=>$event) { - $StartTimeSecs = $event['StartTimeSecs']; $EndTimeSecs = $event['EndTimeSecs']; # It isn't neccessary to do this for each event. We should be able to just look at the first and last if ( !$minTimeSecs or $minTimeSecs > $StartTimeSecs ) $minTimeSecs = $StartTimeSecs; if ( !$maxTimeSecs or $maxTimeSecs < $EndTimeSecs ) $maxTimeSecs = $EndTimeSecs; + if ($StartTimeSecs > $eventMaxSecs) $eventMaxSecs = $StartTimeSecs; $event_json = json_encode($event, JSON_PRETTY_PRINT|JSON_NUMERIC_CHECK); echo " $event_id : $event_json,\n"; @@ -89,10 +92,12 @@ if (!$liveMode) { $events_by_monitor_id[$event['MonitorId']] = array(); array_push($events_by_monitor_id[$event['MonitorId']], $event_id); } # end foreach Event + if ($eventMaxSecs < $maxTimeSecs) $maxTimeSecs = $eventMaxSecs; +} echo ' }; - const events_for_monitor = []; - const events_by_monitor_id = '.json_encode($events_by_monitor_id, JSON_NUMERIC_CHECK).PHP_EOL; + const events_for_monitor = {}; + const events_by_monitor_id = {};'; #.json_encode($events_by_monitor_id, JSON_NUMERIC_CHECK).PHP_EOL; // if there is no data set the min/max to the passed in values if ( $index == 0 ) { @@ -129,6 +134,28 @@ if ( !$have_storage_zero ) { $Storage = new ZM\Storage(); echo 'Storage[0] = ' . $Storage->to_json(). ";\n"; } +echo "\nconst monitorData = [];\n"; +foreach ( $monitors as $monitor ) { + if ($monitor->Deleted() or !$monitor->canView()) continue; +?> + +monitorData[monitorData.length] = { + 'Id': Id() ?>, + 'Name': 'Name() ?>', + 'connKey': 'connKey() ?>', + 'Width': ViewWidth() ?>, + 'Height':ViewHeight() ?>, + 'JanusEnabled':JanusEnabled() ?>, + 'Url': 'UrlToIndex( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>', + 'UrlToZms': 'UrlToZMS( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>', + 'onclick': function(){window.location.assign( '?view=watch&mid=Id() ?>' );}, + 'Type': 'Type() ?>', + 'Refresh': 'Refresh() ?>', + 'Janus_Pin': 'Janus_Pin() ?>', + 'WebColour': 'WebColour() ?>' +}; + Date: Wed, 11 Jun 2025 15:05:45 -0400 Subject: [PATCH 09/10] Remove some debug --- web/skins/classic/views/js/montagereview.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/skins/classic/views/js/montagereview.js b/web/skins/classic/views/js/montagereview.js index d9b33a077..805c044a0 100644 --- a/web/skins/classic/views/js/montagereview.js +++ b/web/skins/classic/views/js/montagereview.js @@ -436,14 +436,10 @@ function timerFire() { outputUpdate(currentTimeSecs); // In live mode we basically do nothing but redisplay } else if (currentTimeSecs + playSecsPerInterval >= maxTimeSecs) { // beyond the end just stop - console.log("Current time " + currentTimeSecs + " + " + playSecsPerInterval + " >= " + maxTimeSecs + " so stopping"); setSpeed(0); outputUpdate(currentTimeSecs); } else if (playSecsPerInterval || (currentTimeSecs==minTimeSecs)) { - console.log("Current time " + currentTimeSecs + " + " + playSecsPerInterval + " + " + timerInterval); outputUpdate(playSecsPerInterval + currentTimeSecs); - } else { - console.log("Current time " + currentTimeSecs + " + " + playSecsPerInterval); } if ((currentSpeed > 0 || liveMode != 0) && !timerObj) { From ba46bafcbfe5e4fe3f135ab587abf8423ce620c9 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 12 Jun 2025 09:39:08 -0400 Subject: [PATCH 10/10] Add SystemTimePoint StringToSystemTimePoint(const std::string ×tamp) --- src/zm_time.cpp | 9 +++++++++ src/zm_time.h | 1 + 2 files changed, 10 insertions(+) diff --git a/src/zm_time.cpp b/src/zm_time.cpp index fd8c69d67..a962f34e4 100644 --- a/src/zm_time.cpp +++ b/src/zm_time.cpp @@ -20,6 +20,7 @@ #include "zm_time.h" #include +#include std::string SystemTimePointToString(SystemTimePoint tp) { time_t tp_sec = std::chrono::system_clock::to_time_t(tp); @@ -51,3 +52,11 @@ std::string TimePointToString(TimePoint tp) { snprintf(timePtr, timeString.capacity() - (timePtr - timeString.data()), ".%06" PRIi64, static_cast(now_frac.count())); return timeString; } + +SystemTimePoint StringToSystemTimePoint(const std::string ×tamp) { + std::tm t{}; + strptime(timestamp.c_str(), "%Y-%m-%d %H:%M:%S", &t); + time_t time_t_val = mktime(&t); + SystemTimePoint stp = std::chrono::system_clock::from_time_t(time_t_val); + return stp; +} diff --git a/src/zm_time.h b/src/zm_time.h index 0ee10ab1c..eec8a0987 100644 --- a/src/zm_time.h +++ b/src/zm_time.h @@ -123,5 +123,6 @@ class TimeSegmentAdder { std::string SystemTimePointToString(SystemTimePoint tp); std::string TimePointToString(TimePoint tp); +SystemTimePoint StringToSystemTimePoint(const std::string &stp); #endif // ZM_TIME_H