Merge branch 'master' into add_janus_rtsp_user

pull/3587/head
Isaac Connor 2022-09-14 18:34:07 -04:00
commit d1cd7d3f91
25 changed files with 1121 additions and 63 deletions

View File

@ -1,18 +1,25 @@
--
-- Update Monitors table to have a MQTT_Enabled Column
-- This adds the Reports Table
--
SELECT 'Checking for `Janus_RTSP_User` in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'Janus_RTSP_User'
) > 0,
"SELECT 'Column Janus_RTSP_User already exists in Monitors'",
"ALTER TABLE Monitors ADD COLUMN `Janus_RTSP_User` INT(10) AFTER `Janus_Use_RTSP_Restream`"
));
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = 'Reports'
AND table_schema = DATABASE()
) > 0,
"SELECT 'Reports table exists'",
"
CREATE TABLE Reports (
Id INT(10) UNSIGNED auto_increment,
Name varchar(30),
FilterId int(10) UNSIGNED,
`StartDateTime` datetime default NULL,
`EndDateTime` datetime default NULL,
`Interval` INT(10) UNSIGNED,
PRIMARY KEY(Id)
) ENGINE=InnoDB;"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

18
db/zm_update-1.37.23.sql Normal file
View File

@ -0,0 +1,18 @@
--
-- Update Monitors table to have a Janus_RTSP_User Column
--
SELECT 'Checking for `Janus_RTSP_User` in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'Janus_RTSP_User'
) > 0,
"SELECT 'Column Janus_RTSP_User already exists in Monitors'",
"ALTER TABLE Monitors ADD COLUMN `Janus_RTSP_User` INT(10) AFTER `Janus_Use_RTSP_Restream`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

View File

@ -275,7 +275,7 @@ sub zmMemVerify {
}
$valid = zmMemRead($monitor, 'shared_data:valid', 1);
if (!$valid) {
Error("Shared data not valid for monitor $$monitor{Id}");
Debug(1, "Shared data not valid for monitor $$monitor{Id}");
return undef;
}
} else {

View File

@ -87,7 +87,7 @@ sub zmMemAttach {
my $mmap_file = $Config{ZM_PATH_MAP}.'/zm.mmap.'.$monitor->{Id};
if ( ! -e $mmap_file ) {
Error("Memory map file '$mmap_file' does not exist in zmMemAttach. zmc might not be running.");
Debug(1, "Memory map file '$mmap_file' does not exist in zmMemAttach. zmc might not be running.");
return undef;
}
my $mmap_file_size = -s $mmap_file;

View File

@ -1070,6 +1070,7 @@ void EventStream::runStream() {
);
} else {
Debug(1, "invalid curr_frame_id %d !< %d", curr_frame_id, event_data->frame_count);
curr_frame_id = event_data->frame_count;
} // end if not at end of event
} else {
// Paused

View File

@ -345,7 +345,6 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
++it;
} // end while
Debug(1, "Resulting it pointing at latest packet? %d, next front points to begin? %d, Keyframe interval %d",
( *it == add_packet ),
( next_front == pktQueue.begin() ),

View File

@ -1 +1 @@
1.37.21
1.37.22

View File

@ -19,7 +19,7 @@
//
global $CLANG;
?>
<div id="modalLogout" class="modal" tabindex="-1" role="dialog">
<div id="modalLogout" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">

209
web/ajax/reports.php Normal file
View File

@ -0,0 +1,209 @@
<?php
ini_set('display_errors', '');
$message = '';
$data = array();
//
// INITIALIZE AND CHECK SANITY
//
if (!canView('Events'))
$message = 'Insufficient permissions for user '.$user['Username'].'<br/>';
if (empty($_REQUEST['task'])) {
$message = 'Must specify a task<br/>';
} else {
$task = $_REQUEST['task'];
}
if (empty($_REQUEST['ids'])) {
if (isset($_REQUEST['task']) && $_REQUEST['task'] != 'query')
$message = 'No id(s) supplied<br/>';
} else {
$ids = $_REQUEST['ids'];
}
if ($message) {
ajaxError($message);
return;
}
require_once('includes/Filter.php');
require_once('includes/Report.php');
// Search contains a user entered string to search on
$search = isset($_REQUEST['search']) ? $_REQUEST['search'] : '';
// Advanced search contains an array of "column name" => "search text" pairs
// Bootstrap table sends json_ecoded array, which we must decode
$advsearch = isset($_REQUEST['advsearch']) ? json_decode($_REQUEST['advsearch'], JSON_OBJECT_AS_ARRAY) : array();
// Order specifies the sort direction, either asc or desc
if (isset($_REQUEST['order'])) {
if (strtolower($_REQUEST['order']) == 'asc') {
$order = 'ASC';
} else if (strtolower($_REQUEST['order']) == 'desc') {
$order = 'DESC';
} else {
Warning('Invalid value for order ' . $_REQUEST['order']);
}
}
// Sort specifies the name of the column to sort on
$sort = (isset($_REQUEST['sort'])) ? $_REQUEST['sort'] : '';
// Offset specifies the starting row to return, used for pagination
$offset = 0;
if (isset($_REQUEST['offset'])) {
if ((!is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']))) {
ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']);
} else {
$offset = $_REQUEST['offset'];
}
}
// Limit specifies the number of rows to return
// Set the default to 0 for reports view, to prevent an issue with ALL pagination
$limit = 0;
if (isset($_REQUEST['limit'])) {
if ((!is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']))) {
ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']);
} else {
$limit = $_REQUEST['limit'];
}
}
//
// MAIN LOOP
//
switch ($task) {
case 'delete' :
if (!canEdit('Events')) {
ajaxError('Insufficient permissions for user '.$user['Username']);
return;
}
foreach ($ids as $id) {
$message = deleteRequest($id);
if (count($message)) {
$data[] = $message;
}
}
break;
case 'query' :
$data = queryRequest($search, $advsearch, $sort, $offset, $order, $limit);
break;
default :
ZM\Fatal("Unrecognised task '$task'");
} // end switch task
ajaxResponse($data);
//
// FUNCTION DEFINITIONS
//
function deleteRequest($id) {
$message = array();
$report = new ZM\Report($id);
if ( !$report->Id() ) {
$message[] = array($id=>'Report not found.');
} else if (!$report->canEdit()) {
$message[] = array($id=>'You do not have permission to delete report '.$report->Id());
} else {
$report->delete();
}
return $message;
}
function queryRequest($search, $advsearch, $sort, $offset, $order, $limit) {
global $dateTimeFormatter;
$data = array(
'total' => 0,
'totalNotFiltered' => 0,
'rows' => array(),
'updated' => $dateTimeFormatter->format(time())
);
// Put server pagination code here
// The table we want our data from
$table = 'Reports';
// The names of the dB columns in the reports table we are interested in
$columns = array('Id', 'Name', 'FilterId', 'StartDateTime', 'EndDateTime', 'Interval');
if ($sort != '') {
if (!in_array($sort, $columns)) {
ZM\Error('Invalid sort field: ' . $sort);
$sort = '';
} else if ($sort == 'EndDateTime') {
if ($order == 'ASC') {
$sort = 'EndDateTime IS NULL, E.EndDateTime';
} else {
$sort = 'EndDateTime IS NOT NULL, E.EndDateTime';
}
}
}
$values = array();
$likes = array();
$where = '';
$col_str = '*';
$sql = 'SELECT ' .$col_str. ' FROM `Reports` '.$where.($sort?' ORDER BY '.$sort.' '.$order:'');
if ($limit) $sql .= ' LIMIT '.$limit;
$unfiltered_rows = array();
$ids = array();
ZM\Debug('Calling the following sql query: ' .$sql);
$query = dbQuery($sql, $values);
if (!$query) {
ajaxError(dbError($sql));
return;
}
while ($row = dbFetchNext($query)) {
$request = new ZM\Report($row);
$request->remove_from_cache();
$ids[] = $request->Id();
$unfiltered_rows[] = $row;
} # end foreach row
# Filter limits come before pagination limits.
if ($limit and ($limit > count($unfiltered_rows))) {
ZM\Debug('Filtering rows due to filter->limit '.count($unfiltered_rows).' limit: '.$limit);
$unfiltered_rows = array_slice($unfiltered_rows, 0, $limit);
}
ZM\Debug('Have ' . count($unfiltered_rows) . ' reports matching base filter.');
$filtered_rows = $unfiltered_rows;
if ($limit) {
ZM\Debug("Filtering rows due to limit " . count($filtered_rows)." offset: $offset limit: $limit");
$filtered_rows = array_slice($filtered_rows, $offset, $limit);
}
$returned_rows = array();
foreach ($filtered_rows as $row) {
$report = new ZM\Report($row);
$row['Name'] = validHtmlStr($row['Name']);
$row['StartDateTime'] = $dateTimeFormatter->format(strtotime($row['StartDateTime']));
$row['EndDateTime'] = $row['EndDateTime'] ? $dateTimeFormatter->format(strtotime($row['EndDateTime'])) : null;
$returned_rows[] = $row;
} # end foreach row matching search
$data['rows'] = $returned_rows;
# totalNotFiltered must equal total, except when either search bar has been used
$data['totalNotFiltered'] = count($unfiltered_rows);
if ( $search != '' || count($advsearch) ) {
$data['total'] = count($filtered_rows);
} else {
$data['total'] = $data['totalNotFiltered'];
}
return $data;
}
?>

26
web/includes/Report.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace ZM;
require_once('database.php');
require_once('Object.php');
class Report extends ZM_Object {
protected static $table = 'Reports';
protected $defaults = array(
'Id' => null,
'Name' => '',
'FilterId' => null,
'StartDateTime' => null,
'EndDateTime' => null,
'Interval' => '86400',
);
public static function find( $parameters = array(), $options = array() ) {
return ZM_Object::_find(get_class(), $parameters, $options);
}
public static function find_one( $parameters = array(), $options = array() ) {
return ZM_Object::_find_one(get_class(), $parameters, $options);
}
} # end class Report
?>

View File

@ -0,0 +1,52 @@
<?php
//
// ZoneMinder web action file
// Copyright (C) 2019 ZoneMinder LLC
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// System edit actions
//if (!canEdit('System') ) {
//ZM\Warning('Need System permissions to add servers');
//return;
//}
require_once('includes/Report.php');
if (!empty($_REQUEST['id'])) {
$report = new ZM\Report($_REQUEST['id']);
} else {
$report = new ZM\Report();
}
global $redirect;
global $error_message;
if ($action == 'save') {
$changes = $report->changes($_REQUEST['Report']);
if (count($changes)) {
if (!$report->save($changes)) {
$error_message .= "Error saving report: " . $report->get_last_error().'</br>';
} else {
$redirect = '?view=report&id='.$report->Id();
}
}
} else if ($action == 'delete') {
$report->delete();
$redirect = '?view=reports';
} else {
ZM\Error("Unknown action $action in saving Report");
}
?>

View File

@ -196,7 +196,7 @@ function getAuthUser($auth) {
if (isset($_SESSION['username'])) {
# In a multi-server case, we might be logged in as another user and so the auth hash didn't work
if (ZM_CASE_INSENSITIVE_USERNAMES) {
$sql = 'SELECT * FROM Users WHERE Enabled = 1 AND LOWER(Username) != LOWER(?)';
$sql = 'SELECT * FROM Users WHERE Enabled = 1 AND LOWER(Username) != LOWER(?)';
} else {
$sql = 'SELECT * FROM Users WHERE Enabled = 1 AND Username != ?';
}
@ -216,7 +216,7 @@ function getAuthUser($auth) {
} // end if
} // end if using auth hash
ZM\Error("Unable to authenticate user from auth hash '$auth'");
ZM\Info("Unable to authenticate user from auth hash '$auth'");
return null;
} // end getAuthUser($auth)

View File

@ -75,8 +75,6 @@ function expr_to_ui(expr, container) {
let brackets = 0;
const used_monitorlinks = [];
if (!tokens.length) return;
// Every monitorlink should have possible parenthesis on either side of it
if (tokens.length > 3) {
if (tokens[0].type != '(') {
@ -160,9 +158,8 @@ function expr_to_ui(expr, container) {
select.append('<option value="">Add MonitorLink</option>');
for (monitor_id in monitors) {
const monitor = monitors[monitor_id];
if (!array_search(monitor.Id, used_monitorlinks)) {
select.append('<option value="' + monitor.Id + '">' + monitor.Name + ' : All Zones</option>');
}
//if (!array_search(monitor.Id, used_monitorlinks))
select.append('<option value="' + monitor.Id + '">' + monitor.Name + ' : All Zones</option>');
for ( zone_id in zones ) {
const zone = zones[zone_id];
if ( monitor.Id == zone.MonitorId ) {
@ -184,8 +181,10 @@ function array_search(needle, haystack) {
}
function add_to_expr() {
$j('[name="newMonitor[LinkedMonitors]"]').val($j('[name="newMonitor[LinkedMonitors]"]').val() + '|' + $j('#monitorLinks').val());
expr_to_ui($j('[name="newMonitor[LinkedMonitors]"]').val(), $j('#LinkedMonitorsUI'));
const expr = $j('[name="newMonitor[LinkedMonitors]"]');
const oldval = expr.val();
expr.val(oldval == '' ? $j('#monitorLinks').val() : oldval + '|' + $j('#monitorLinks').val());
expr_to_ui(expr.val(), $j('#LinkedMonitorsUI'));
}
function update_expr(ev) {

View File

@ -10,23 +10,42 @@
display: inline-block;
}
#alarmCues,
.alarmCue {
position: absolute;
background: none;
/*
background-color: #222222;
height: 1.25em;
*/
height: 16px;
text-align: left;
margin: 0 auto 0 auto;
border-radius: 0 0 .3em .3em;
z-index: 10;
border: none;
border-right: 1px solid black;
}
.alarmCue span {
background-color:red;
height: 100%;
display: inline-block;
border-radius: 0;
#alarmCues span {
border-left: 1px solid black;
top: 0;
position: absolute;
height: 100%;
display: inline-block;
border-radius: 0;
z-index: 11;
font-size: 8px;
}
span.alarmCue {
background-color:red;
z-index: 9;
opacity: 0.33;
}
span.noneCue {
background: none;
z-index: 9;
}
#header {
@ -150,16 +169,27 @@ height: 100%;
#progressBar {
position: relative;
/*
top: -1.25em;
*/
height: 1.25em;
/*
margin: 0 auto -1.25em auto;
*/
margin: 0;
z-index: 5;
}
#progressBar .progressBox {
transition: width .1s;
height: 100%;
position: absolute;
top: 0;
background: rgba(170, 170, 170, .7);
/*
border-radius: 0 0 .3em .3em;
*/
z-index: 5;
}
#eventStills {
@ -288,3 +318,9 @@ svg.zones {
#toggleZonesButton span.material-icons {
font-size: 18px;
}
#indicator {
height: 2.75em;
position: absolute;
border-left: 1px solid blue;
margin-top: -1.25em;
}

View File

@ -0,0 +1,39 @@
#reportsTable.major .colTime {
white-space: nowrap;
}
#reportsTable [data-field="Id"],
#reportsTable [data-field="Interval"] {
text-align: center;
}
#reportsTable [data-field="Name"] {
text-align: left;
}
#header {
display: flex;
justify-content: space-between;
}
#header h2, #header a {
line-height: 1.1;
margin:5px 0 0 0;
}
#header #info, #header #pagination, #header #controls {
display: flex;
flex-direction: column;
}
#header #controls {
align-items: flex-end;
}
#header #pagination {
align-items: center;
}
/* Dirty hack to fix up/down buttons on pagination number input */
input[type="number"].form-control {
padding: 5px;
}

View File

@ -120,7 +120,7 @@ echo output_link_if_exists(array(
'css/base/views/'.$basename.'.css',
'js/dateTimePicker/jquery-ui-timepicker-addon.css',
'js/jquery-ui-1.12.1/jquery-ui.structure.min.css',
));
), true);
if ( $css != 'base' )
echo output_link_if_exists(array(
'css/'.$css.'/skin.css',
@ -234,6 +234,7 @@ function getNormalNavBarHTML($running, $user, $bandwidth_options, $view, $skin)
echo getMontageHTML($view);
echo getMontageReviewHTML($view);
echo getSnapshotsHTML($view);
echo getReportsHTML($view);
echo getRprtEvntAuditHTML($view);
echo getHeaderFlipHTML();
echo '</ul>';
@ -368,6 +369,7 @@ function getCollapsedNavBarHTML($running, $user, $bandwidth_options, $view, $ski
echo getMontageHTML($view);
echo getMontageReviewHTML($view);
echo getSnapshotsHTML($view);
echo getReportsHTML($view);
echo getRprtEvntAuditHTML($view);
echo '</ul>';
}
@ -771,6 +773,17 @@ function getSnapshotsHTML($view) {
return $result;
}
function getReportsHTML($view) {
$result = '';
if (canView('Events')) {
$class = ($view == 'reports' or $view == 'report') ? ' selected' : '';
$result .= '<li id="getReportsHTML" class="nav-item dropdown"><a class="nav-link'.$class.'" href="?view=reports">'.translate('Reports').'</a></li>'.PHP_EOL;
}
return $result;
}
// Returns the html representing the Audit Events Report menu item
function getRprtEvntAuditHTML($view) {
$result = '';

14
web/skins/classic/js/Chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -334,7 +334,7 @@ if ( currentView != 'none' && currentView != 'login' ) {
function insertModalHtml(name, html) {
var modal = $j('#' + name);
if ( modal.length ) {
if (modal.length) {
modal.replaceWith(html);
} else {
$j("body").append(html);
@ -360,13 +360,13 @@ if ( currentView != 'none' && currentView != 'login' ) {
if (error == 'Unauthorized') {
window.location.reload(true);
}
if ( ! jqxhr.responseText ) {
if (!jqxhr.responseText) {
console.log("No responseText in jqxhr");
console.log(jqxhr);
return;
}
console.log("Response Text: " + jqxhr.responseText.replace(/(<([^>]+)>)/gi, ''));
if ( textStatus != "timeout" ) {
if (textStatus != "timeout") {
// The idea is that this should only fail due to auth, so reload the page
// which should go to login if it can't stay logged in.
window.location.reload(true);
@ -375,29 +375,30 @@ if ( currentView != 'none' && currentView != 'login' ) {
}
function setNavBar(data) {
if ( !data ) {
if (!data) {
console.error("No data in setNavBar");
return;
}
if ( data.auth ) {
if ( data.auth != auth_hash ) {
if (data.auth) {
if (data.auth != auth_hash) {
console.log("Update auth_hash to "+data.auth);
// Update authentication token.
auth_hash = data.auth;
}
}
if ( data.auth_relay ) {
if (data.auth_relay) {
auth_relay = data.auth_relay;
}
// iterate through all the keys then update each element id with the same name
for (var key of Object.keys(data)) {
if ( key == "auth" ) continue;
if ( key == "auth_relay" ) continue;
if ( $j('#'+key).hasClass("show") ) continue; // don't update if the user has the dropdown open
if ( $j('#'+key).length ) $j('#'+key).replaceWith(data[key]);
if ( key == 'getBandwidthHTML' ) bwClickFunction();
}
}
}
} // end if ( currentView != 'none' && currentView != 'login' )
//Shows a message if there is an error in the streamObj or the stream doesn't exist. Returns true if error, false otherwise.
function checkStreamForErrors(funcName, streamObj) {
@ -441,7 +442,7 @@ function secsToTime( seconds ) {
}
timeString = timeHours+":"+timeMins+":"+timeSecs;
}
return ( timeString );
return timeString;
}
function submitTab(evt) {
@ -712,11 +713,13 @@ function getLogoutModal() {
.fail(logAjaxFail);
}
function clickLogout() {
if ( ! $j('#modalLogout').length ) {
const modalLogout = $j('#modalLogout');
if (!modalLogout.length) {
getLogoutModal();
return;
}
$j('#modalLogout').modal('show');
modalLogout.modal('show');
}
function getStateModal() {

View File

@ -245,9 +245,10 @@ if ( (ZM_WEB_STREAM_METHOD == 'mpeg') && ZM_MPEG_LIVE_FORMAT ) {
}
} // end if stream method
?>
<div id="alarmCue" class="alarmCue"></div>
<div id="progressBar" style="width: 100%;">
<div id="alarmCues" style="width: 100%;"></div>
<div class="progressBox" id="progressBox" title="" style="width: 0%;"></div>
<div id="indicator" style="display: none;"></div>
</div><!--progressBar-->
<?php
} /*end if !DefaultVideo*/

View File

@ -105,18 +105,51 @@ function setAlarmCues(data) {
} else {
cueFrames = data.frames;
alarmSpans = renderAlarmCues(vid ? $j("#videoobj") : $j("#evtStream"));//use videojs width or zms width
$j(".alarmCue").html(alarmSpans);
$j('#alarmCues').html(alarmSpans);
}
}
function renderAlarmCues(containerEl) {
if ( !( cueFrames && cueFrames.length ) ) {
let html = '';
/*
grid_size = 25;
const canvas = document.getElementById('alarmCues');
canvas_width = canvas.width = containerEl.width();
pixPerSegment = canvas_width / eventData.Length
console.log(pixPerSegment);
console.log(canvas);
const ctx = canvas.getContext('2d');
for (let i=0; i <= pixPerSegment; i++) {
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "#000000";
ctx.moveTo(grid_size*i, 0);
ctx.lineTo(grid_size*i, canvas.height);
ctx.stroke();
}
*/
cues_div = document.getElementById('alarmCues');
const event_length = (eventData.Length > cueFrames[cueFrames.length - 1].Delta) ? eventData.Length : cueFrames[cueFrames.length - 1].Delta;
let span_count = 10;
let span_seconds = parseInt(event_length / span_count);
let span_width = parseInt(containerEl.width() / span_count);
console.log(span_width, containerEl.width(), span_count);
//let span_width =
const date = new Date(eventData.StartDateTime);
for (let i=0; i < span_count; i += 1) {
html += '<span style="left:'+(i*span_width)+'px; width: '+span_width+'px;">'+date.toLocaleTimeString()+'</span>';
date.setTime(date.getTime() + span_seconds*1000);
}
if (!(cueFrames && cueFrames.length)) {
console.log('No cue frames for event');
return;
return html;
}
// This uses the Delta of the last frame to get the length of the event. I can't help but wonder though
// if we shouldn't just use the event length endtime-starttime
var cueRatio = containerEl.width() / (cueFrames[cueFrames.length - 1].Delta * 100);
var cueRatio = containerEl.width() / (event_length * 100);
var minAlarm = Math.ceil(1/cueRatio);
var spanTimeStart = 0;
var spanTimeEnd = 0;
@ -125,22 +158,28 @@ function renderAlarmCues(containerEl) {
var pixSkew = 0;
var skip = 0;
var num_cueFrames = cueFrames.length;
for ( var i = 0; i < num_cueFrames; i++ ) {
let left = 0;
for (let i=0; i < num_cueFrames; i++) {
skip = 0;
frame = cueFrames[i];
if ( (frame.Type == 'Alarm') && (alarmed == 0) ) { //From nothing to alarm. End nothing and start alarm.
if ((frame.Type == 'Alarm') && (alarmed == 0)) { //From nothing to alarm. End nothing and start alarm.
alarmed = 1;
if (frame.Delta == 0) continue; //If event starts with an alarm or too few for a nonespan
spanTimeEnd = frame.Delta * 100;
spanTime = spanTimeEnd - spanTimeStart;
var pix = cueRatio * spanTime;
let pix = cueRatio * spanTime;
pixSkew += pix - Math.round(pix);//average out the rounding errors.
pix = Math.round(pix);
if ((pixSkew > 1 || pixSkew < -1) && pix + Math.round(pixSkew) > 0) { //add skew if it's a pixel and won't zero out span.
pix += Math.round(pixSkew);
pixSkew = pixSkew - Math.round(pixSkew);
}
alarmHtml += '<span class="alarmCue noneCue" style="width: ' + pix + 'px;"></span>';
alarmHtml += '<span class="alarmCue noneCue" style="left: '+left+'px; width: ' + pix + 'px;"></span>';
left = parseInt((frame.Delta / event_length) * containerEl.width());
console.log(left, frame.Delta, event_length, containerEl.width());
spanTimeStart = spanTimeEnd;
} else if ( (frame.Type !== 'Alarm') && (alarmed == 1) ) { //from alarm to nothing. End alarm and start nothing.
futNone = 0;
@ -170,7 +209,8 @@ function renderAlarmCues(containerEl) {
pix += Math.round(pixSkew);
pixSkew = pixSkew - Math.round(pixSkew);
}
alarmHtml += '<span class="alarmCue" style="width: ' + pix + 'px;"></span>';
alarmHtml += '<span class="alarmCue" style="left: '+left+'px; width: ' + pix + 'px;"></span>';
left = parseInt((frame.Delta / event_length) * containerEl.width());
spanTimeStart = spanTimeEnd;
} else if ( (frame.Type == 'Alarm') && (alarmed == 1) && (i + 1 >= cueFrames.length) ) { //event ends on an alarm
spanTimeEnd = frame.Delta * 100;
@ -178,10 +218,11 @@ function renderAlarmCues(containerEl) {
alarmed = 0;
pix = Math.round(cueRatio * spanTime);
if (pixSkew >= .5 || pixSkew <= -.5) pix += Math.round(pixSkew);
alarmHtml += '<span class="alarmCue" style="width: ' + pix + 'px;"></span>';
alarmHtml += '<span class="alarmCue" style="left: '+left+'px; width: ' + pix + 'px;"></span>';
}
}
return alarmHtml;
return html + alarmHtml;
}
function changeCodec() {
@ -193,8 +234,9 @@ function changeScale() {
var newWidth;
var newHeight;
var autoScale;
var eventViewer = $j(vid ? '#videoobj' : '#videoFeed');
var alarmCue = $j('div.alarmCue');
const eventViewer = $j(vid ? '#videoobj' : '#evtStream');
var alarmCue = $j('#alarmCues');
var bottomEl = $j('#replayStatus');
if (scale == '0') {
@ -766,17 +808,57 @@ function updateProgressBar() {
if (!(eventData && streamStatus)) {
return;
} // end if ! eventData && streamStatus
var curWidth = (streamStatus.progress / parseFloat(eventData.Length)) * 100;
$j("#progressBox").css('width', curWidth + '%');
const curWidth = (streamStatus.progress / parseFloat(eventData.Length)) * 100;
const progressDate = new Date(eventData.StartDateTime);
progressDate.setTime(progressDate.getTime() + (streamStatus.progress*1000));
const progressBox = $j("#progressBox");
progressBox.css('width', curWidth + '%');
progressBox.attr('title', progressDate.toLocaleTimeString());
} // end function updateProgressBar()
// Handles seeking when clicking on the progress bar.
function progressBarNav() {
$j('#progressBar').click(function(e) {
var x = e.pageX - $j(this).offset().left;
var seekTime = (x / $j('#progressBar').width()) * parseFloat(eventData.Length);
let x = e.pageX - $j(this).offset().left;
if (x<0) x=0;
const seekTime = (x / $j('#progressBar').width()) * parseFloat(eventData.Length);
console.log("clicked at ", x, seekTime);
streamSeek(seekTime);
});
$j('#progressBar').mouseover(function(e) {
let x = e.pageX - $j(this).offset().left;
if (x<0) x=0;
console.log(x);
const seekTime = (x / $j('#progressBar').width()) * parseFloat(eventData.Length);
const indicator = document.getElementById('indicator');
indicator.style.display = 'block';
indicator.style.left = x + 'px';
indicator.setAttribute('title', seekTime);
});
$j('#progressBar').mouseout(function(e) {
const indicator = document.getElementById('indicator');
indicator.style.display = 'none';
});
$j('#progressBar').mousemove(function(e) {
const bar = $j(this);
let x = e.pageX - bar.offset().left;
if (x<0) x=0;
if (x > bar.width()) x = bar.width();
let seekTime = (x / bar.width()) * parseFloat(eventData.Length);
const indicator = document.getElementById('indicator');
const date = new Date(eventData.StartDateTime);
date.setTime(date.getTime() + (seekTime*1000));
indicator.innerHTML = date.toLocaleTimeString();
indicator.style.left = x+'px';
indicator.setAttribute('title', seekTime);
});
}
function handleClick(event) {
@ -932,7 +1014,7 @@ function initPage() {
if ($j('#videoobj').length) {
vid = videojs('videoobj');
addVideoTimingTrack(vid, LabelFormat, eventData.MonitorName, eventData.Length, eventData.StartDateTime);
$j('.vjs-progress-control').append('<div class="alarmCue"></div>');//add a place for videojs only on first load
$j('.vjs-progress-control').append('<div id="alarmCues" class="alarmCues"></div>');//add a place for videojs only on first load
vid.on('ended', vjsReplay);
vid.on('play', vjsPlay);
vid.on('pause', pauseClicked);

View File

@ -0,0 +1,92 @@
var backBtn = $j('#backBtn');
var deleteBtn = $j('#deleteBtn');
// Load the Delete Confirmation Modal HTML via Ajax call
function getDelConfirmModal() {
$j.getJSON(thisUrl + '?request=modal&modal=delconfirm')
.done(function(data) {
insertModalHtml('deleteConfirm', data.html);
manageDelConfirmModalBtns();
})
.fail(logAjaxFail);
}
// Manage the DELETE CONFIRMATION modal button
function manageDelConfirmModalBtns() {
document.getElementById("delConfirmBtn").addEventListener('click', function onDelConfirmClick(evt) {
if ( ! canEdit.Events ) {
enoperm();
return;
}
evt.preventDefault();
const selections = getIdSelections();
if (!selections.length) {
alert('Please select reports to delete.');
} else {
deleteReports(selections);
}
});
// Manage the CANCEL modal button
document.getElementById("delCancelBtn").addEventListener('click', function onDelCancelClick(evt) {
$j('#deleteConfirm').modal('hide');
});
}
function deleteReports(ids) {
const ticker = document.getElementById('deleteProgressTicker');
const chunk = ids.splice(0, 10);
console.log("Deleting " + chunk.length + " selections. " + ids.length);
$j.getJSON(thisUrl + '?request=reports&task=delete&ids[]='+chunk.join('&ids[]='))
.done( function(data) {
if (!ids.length) {
$j('#reportsTable').bootstrapTable('refresh');
$j('#deleteConfirm').modal('hide');
} else {
if (ticker.innerHTML.length < 1 || ticker.innerHTML.length > 10) {
ticker.innerHTML = '.';
} else {
ticker.innerHTML = ticker.innerHTML + '.';
}
deleteReports(ids);
}
})
.fail( function(jqxhr) {
logAjaxFail(jqxhr);
$j('#reportsTable').bootstrapTable('refresh');
$j('#deleteConfirm').modal('hide');
});
}
function initPage() {
// Load the delete confirmation modal into the DOM
getDelConfirmModal();
deleteBtn.prop('disabled', canEdit.Events);
// Don't enable the back button if there is no previous zm page to go back to
backBtn.prop('disabled', !document.referrer.length);
// Manage the BACK button
document.getElementById("backBtn").addEventListener('click', function onBackClick(evt) {
evt.preventDefault();
window.history.back();
});
// Manage the DELETE button
document.getElementById("deleteBtn").addEventListener('click', function onDeleteClick(evt) {
if (!canEdit.Events) {
enoperm();
return;
}
evt.preventDefault();
$j('#deleteConfirm').modal('show');
});
}
$j(document).ready(function() {
initPage();
});

View File

@ -0,0 +1,188 @@
var backBtn = $j('#backBtn');
var viewBtn = $j('#viewBtn');
var editBtn = $j('#editBtn');
var deleteBtn = $j('#deleteBtn');
var table = $j('#reportsTable');
/*
This is the format of the json object sent by bootstrap-table
var params =
{
"type":"get",
"data":
{
"search":"some search text",
"sort":"StartDateTime",
"order":"asc",
"offset":0,
"limit":25
"filter":
{
"Name":"some advanced search text"
"StartDateTime":"some more advanced search text"
}
},
"cache":true,
"contentType":"application/json",
"dataType":"json"
};
*/
// Called by bootstrap-table to retrieve zm event data
function ajaxRequest(params) {
if (params.data && params.data.filter) {
params.data.advsearch = params.data.filter;
delete params.data.filter;
}
$j.getJSON(thisUrl + '?view=request&request=reports&task=query', params.data)
.done(function(data) {
if (data.result == 'Error') {
alert(data.message);
return;
}
var rows = processRows(data.rows);
// rearrange the result into what bootstrap-table expects
params.success({total: data.total, totalNotFiltered: data.totalNotFiltered, rows: rows});
})
.fail(function(jqXHR) {
logAjaxFail(jqXHR);
$j('#reportsTable').bootstrapTable('refresh');
});
}
function processRows(rows) {
$j.each(rows, function(ndx, row) {
const id = row.Id;
row.Id = '<a href="?view=report&amp;id=' + id + '&amp;page=1">' + id + '</a>';
row.Name = '<a href="?view=report&amp;id=' + id + '&amp;page=1">' + row.Name + '</a>';
});
return rows;
}
// Returns the event id's of the selected rows
function getIdSelections() {
var table = $j('#reportsTable');
return $j.map(table.bootstrapTable('getSelections'), function(row) {
return row.Id.replace(/(<([^>]+)>)/gi, ''); // strip the html from the element before sending
});
}
// Load the Delete Confirmation Modal HTML via Ajax call
function getDelConfirmModal() {
$j.getJSON(thisUrl + '?request=modal&modal=delconfirm')
.done(function(data) {
insertModalHtml('deleteConfirm', data.html);
manageDelConfirmModalBtns();
})
.fail(logAjaxFail);
}
// Manage the DELETE CONFIRMATION modal button
function manageDelConfirmModalBtns() {
document.getElementById("delConfirmBtn").addEventListener('click', function onDelConfirmClick(evt) {
if ( ! canEdit.Events ) {
enoperm();
return;
}
evt.preventDefault();
const selections = getIdSelections();
if (!selections.length) {
alert('Please select reports to delete.');
} else {
deleteReports(selections);
}
});
// Manage the CANCEL modal button
document.getElementById("delCancelBtn").addEventListener('click', function onDelCancelClick(evt) {
$j('#deleteConfirm').modal('hide');
});
}
function deleteReports(ids) {
const ticker = document.getElementById('deleteProgressTicker');
const chunk = ids.splice(0, 10);
console.log("Deleting " + chunk.length + " selections. " + ids.length);
$j.getJSON(thisUrl + '?request=reports&task=delete&ids[]='+chunk.join('&ids[]='))
.done( function(data) {
if (!ids.length) {
$j('#reportsTable').bootstrapTable('refresh');
$j('#deleteConfirm').modal('hide');
} else {
if (ticker.innerHTML.length < 1 || ticker.innerHTML.length > 10) {
ticker.innerHTML = '.';
} else {
ticker.innerHTML = ticker.innerHTML + '.';
}
deleteReports(ids);
}
})
.fail( function(jqxhr) {
logAjaxFail(jqxhr);
$j('#reportsTable').bootstrapTable('refresh');
$j('#deleteConfirm').modal('hide');
});
}
function initPage() {
// Load the delete confirmation modal into the DOM
getDelConfirmModal();
// Init the bootstrap-table
table.bootstrapTable({icons: icons});
// enable or disable buttons based on current selection and user rights
table.on('check.bs.table uncheck.bs.table ' +
'check-all.bs.table uncheck-all.bs.table',
function() {
selections = table.bootstrapTable('getSelections');
viewBtn.prop('disabled', !(selections.length && canView.Events));
deleteBtn.prop('disabled', !(selections.length && canEdit.Events));
});
// Don't enable the back button if there is no previous zm page to go back to
backBtn.prop('disabled', !document.referrer.length);
// Manage the BACK button
document.getElementById("backBtn").addEventListener('click', function onBackClick(evt) {
evt.preventDefault();
window.history.back();
});
// Manage the REFRESH Button
document.getElementById("refreshBtn").addEventListener('click', function onRefreshClick(evt) {
evt.preventDefault();
window.location.reload(true);
});
document.getElementById("newBtn").addEventListener('click', function (evt) {
evt.preventDefault();
window.location = '?view=report';
});
// Manage the DELETE button
document.getElementById("deleteBtn").addEventListener('click', function onDeleteClick(evt) {
if (!canEdit.Events) {
enoperm();
return;
}
evt.preventDefault();
$j('#deleteConfirm').modal('show');
});
table.bootstrapTable('resetSearch');
// The table is initially given a hidden style, so now that we are done rendering, show it
table.show();
}
$j(document).ready(function() {
initPage();
});

View File

@ -344,6 +344,7 @@ function probeNetwork() {
$macBases = array(
'00:0f:7c' => array('type'=>'ACTi','probeFunc'=>'probeACTi'),
#'9c:8e:cd' => array('type'=>'Amcrest', 'probeFunc'=>'probeAmcrest'),
'00:40:8c' => array('type'=>'Axis', 'probeFunc'=>'probeAxis'),
'2c:a5:9c' => array('type'=>'Hikvision', 'probeFunc'=>'probeHikvision'),
'00:80:f0' => array('type'=>'Panasonic','probeFunc'=>'probePana'),

View File

@ -0,0 +1,183 @@
<?php
//
// ZoneMinder web reports view file
// Copyright (C) 2022 Isaac Connor
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
#if (!canView('Reports')) {
#$view = 'error';
#return;
#} else if (!ZM_FEATURES_SNAPSHOTS) {
#$view = 'console';
#return;
#}
#
require_once('includes/Event.php');
require_once('includes/Filter.php');
require_once('includes/Report.php');
$report_id = isset($_REQUEST['id']) ? validInt($_REQUEST['id']) : '';
$report = new ZM\Report($report_id);
xhtmlHeaders(__FILE__, translate('Reports'));
getBodyTopHTML();
echo getNavBarHTML();
?>
<div id="page" class="container-fluid p-3">
<div class="Edit">
<form name="report" id="reportForm" method="post" action="?view=report&id=<?php echo $report_id ?>">
<!-- Toolbar button placement and styling handled by bootstrap-tables -->
<div id="toolbar">
<button id="backBtn" type="button" class="btn btn-normal" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Back') ?>" disabled><i class="fa fa-arrow-left"></i></button>
<!--<button id="filterBtn" class="btn btn-normal" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Filter') ?>"><i class="fa fa-filter"></i></button>-->
<!--<button id="exportBtn" class="btn btn-normal" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Export') ?>" disabled><i class="fa fa-external-link"></i></button>-->
<button id="saveBtn" name="action" value="save" type="submit" class="btn btn-normal" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Save') ?>"><i class="fa fa-save"></i></button>
<button id="deleteBtn" name="action" value="delete" type="submit" class="btn btn-danger" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Delete') ?>" disabled><i class="fa fa-trash"></i></button>
</div>
<table class="major table table-sm">
<tbody>
<tr>
<th class="text-right" scope="row"><?php echo translate('Name') ?></th>
<td><input type="text" name="Report[Name]" value="<?php echo $report->Name() ?>"/></td>
</tr>
<tr>
<th class="text-right " scope="row"><?php echo translate('Filter') ?></th>
<td>
<?php
$FilterById = array();
foreach (ZM\Filter::find() as $F) {
$FiltersById[$F->Id()] = $F;
}
echo htmlSelect('Report[FilterId]', array(''=>translate('select')) + $FiltersById, $report->FilterId())
?></td>
</tr>
<!--
<tr>
<th class="text-right" scope="row"><?php echo translate('Starting') ?></th>
<td><input type="text" name="Report[StartDateTime]" value="<?php echo $report->StartDateTime() ?>"/></td>
</tr>
<tr>
<th class="text-right" scope="row"><?php echo translate('Ending') ?></th>
<td><input type="text" name="Report[EndDateTime]" value="<?php echo $report->EndDateTime() ?>"/></td>
</tr>
<tr>
<th class="text-right" scope="row"><?php echo translate('Interval') ?></th>
<td><input type="text" name="Report[Interval]" value="<?php echo $report->Interval() ?>"/></td>
</tr>
-->
</tbody>
</table>
</form>
</div>
<canvas id="bar-chart" width=300" height="150"></canvas>
<script src="/skins/classic/js/Chart.min.js"></script>
<script nonce="<?php echo $cspNonce; ?>">
var events = Array();
<?php
require_once('includes/Filter.php');
if (!$report->FilterId()) return;
$filter = new ZM\Filter($report->FilterId());
if ($user['MonitorIds']) {
$filter = $filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=>'IN', 'val'=>$user['MonitorIds']));
}
$events = $filter->Events();
foreach ($events as $event) {
echo 'events[events.length] = '.$event->to_json().PHP_EOL;
}
?>
time_labels = Array();
datasets = Array();
dataset_indexes = {}; // Associative array from a date String like July 20 to an index into the datasets.
for (i=0; i < 24; i++) {
time_labels[time_labels.length] = `${i}:00`;
}
months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
for (event_index=0; event_index < events.length; event_index++) {
const event = events[event_index];
const event_start = new Date(event.StartDateTime);
const day = event_start.getDate();
const date_key = months[event_start.getMonth()] + ' ' + day;
if (! (date_key in dataset_indexes)) {
dataset_indexes[date_key] = datasets.length;
}
const dataset_index = dataset_indexes[date_key];
if (!(dataset_index in datasets)) {
datasets[dataset_index] = {
label: date_key,
fill: false,
borderColor: 'rgb('+parseInt(255*Math.random())+', '+parseInt(255*Math.random())+', '+parseInt(255*Math.random())+')',
tension: 0.1,
data: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
};
}
datasets[dataset_index].data[event_start.getHours()] += parseFloat(event.Length);
}
/*
for (i=0; i < datasets.length; i++) {
if (!datasets[i]) {
datasets[i] = {
label: '',
fill: false,
borderColor: 'rgb(192, 192, 192)',
tension: 0.1,
data: []
};
}
}
*/
console.log(datasets);
const data = {
labels: time_labels,
datasets: datasets,
};
new Chart(document.getElementById("bar-chart"), {
type: 'line',
data: data
});
/*
{
options: {
legend: { display: false },
title: {
display: true,
text: report.Name
},
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
*/
</script>
</div>
<?php xhtmlFooter() ?>

View File

@ -0,0 +1,95 @@
<?php
//
// ZoneMinder web reports view file
// Copyright (C) 2022 Isaac Connor
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
#if (!canView('Reports')) {
#$view = 'error';
#return;
#} else if (!ZM_FEATURES_SNAPSHOTS) {
#$view = 'console';
#return;
#}
require_once('includes/Event.php');
require_once('includes/Filter.php');
require_once('includes/Report.php');
xhtmlHeaders(__FILE__, translate('Reports'));
getBodyTopHTML();
echo getNavBarHTML();
?>
<div id="page" class="container-fluid p-3">
<!-- Toolbar button placement and styling handled by bootstrap-tables -->
<div id="toolbar">
<button type="button" id="backBtn" class="btn btn-normal" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Back') ?>" disabled><i class="fa fa-arrow-left"></i></button>
<button type="button" id="refreshBtn" class="btn btn-normal" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Refresh') ?>" ><i class="fa fa-refresh"></i></button>
<button type="button" id="newBtn" class="btn btn-normal" value="AddNew" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Add New Report') ?>"><i class="fa fa-plus"></i></button>
<button type="button" id="deleteBtn" class="btn btn-danger" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Delete') ?>" disabled><i class="fa fa-trash"></i></button>
</div>
<!-- Table styling handled by bootstrap-tables -->
<div class="row justify-content-center table-responsive-sm">
<table
id="reportsTable"
data-locale="<?php echo i18n() ?>"
data-side-pagination="server"
data-ajax="ajaxRequest"
data-pagination="true"
data-show-pagination-switch="true"
data-page-list="[10, 25, 50, 100, 200, All]"
data-search="true"
data-cookie="true"
data-cookie-id-table="zmReportsTable"
data-cookie-expire="2y"
data-click-to-select="true"
data-remember-order="true"
data-show-columns="true"
data-show-export="true"
data-uncheckAll="true"
data-toolbar="#toolbar"
data-show-fullscreen="true"
data-click-to-select="true"
data-maintain-meta-data="true"
data-buttons-class="btn btn-normal"
data-show-jump-to="true"
data-show-refresh="true"
class="table-sm table-borderless"
style="display:none;"
>
<thead>
<!-- Row styling is handled by bootstrap-tables -->
<tr>
<th data-sortable="false" data-field="toggleCheck" data-checkbox="true"></th>
<th data-sortable="true" data-field="Id"><?php echo translate('Id') ?></th>
<th data-sortable="true" data-field="Name"><?php echo translate('Name') ?></th>
<th data-sortable="false" data-field="Description"><?php echo translate('Description') ?></th>
<th data-sortable="true" data-field="StartDateTime"><?php echo translate('Starting') ?></th>
<th data-sortable="true" data-field="EndDateTime"><?php echo translate('Ending') ?></th>
<th data-sortable="true" data-field="Interval"><?php echo translate('Interval') ?></th>
</tr>
</thead>
<tbody>
<!-- Row data populated via Ajax -->
</tbody>
</table>
</div>
</div>
<?php xhtmlFooter() ?>