Merge branch 'master' into add_go2rtc

This commit is contained in:
Isaac Connor
2025-06-26 13:31:56 -04:00
22 changed files with 560 additions and 308 deletions

View File

@@ -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
@@ -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

View File

@@ -1639,6 +1639,13 @@ our @options = (
},
category => 'web',
},
{
name => 'ZM_WEB_NAVBAR_LINKS',
default => '',
description => 'Additional Links to put in navbar. Should be a comma separated list of HTML a tags.',
type => $types{string},
category => 'web',
},
{
name => 'ZM_WEB_NAVBAR_STICKY',
default => '1',

View File

@@ -1041,7 +1041,18 @@ sub patchDB {
sub migratePasswords {
use Crypt::Eksblowfish::Bcrypt;
use Data::Entropy::Algorithms qw(rand_bits);
my $random;
eval {
require Bytes::Random::Secure;
$random = Bytes::Random::Secure->new( Bits => 16*8);
};
if ($@ or !$random) {
eval {
require Data::Entropy::Algorithms;
$random =Data::Entropy::Algorithms::rand_bits(16*8);
};
}
print("Migratings passwords, if any...\n");
my $sql = 'SELECT * FROM `Users`';
my $sth = $dbh->prepare_cached($sql) or die( "Can't prepare '$sql': ".$dbh->errstr() );
@@ -1050,7 +1061,7 @@ sub migratePasswords {
my $scheme = substr($user->{Password}, 0, 1);
if ($scheme eq '*') {
print('-->'.$user->{Username}." password will be migrated\n");
my $salt = Crypt::Eksblowfish::Bcrypt::en_base64(rand_bits(16*8));
my $salt = Crypt::Eksblowfish::Bcrypt::en_base64($random);
my $settings = '$2a$10$'.$salt;
my $pass_hash = Crypt::Eksblowfish::Bcrypt::bcrypt($user->{Password},$settings);
my $new_pass_hash = '-ZM-'.$pass_hash;

View File

@@ -231,14 +231,16 @@ int zmDbDoInsert(const std::string &query) {
return 0;
int rc;
while ((rc = mysql_query(&dbconn, query.c_str())) and !zm_terminate) {
std::string reason = mysql_error(&dbconn);
if (mysql_ping(&dbconn)) {
if (!zmDbReconnect()) sleep(1);
} else {
Error("Can't run query %s: %s", query.c_str(), mysql_error(&dbconn));
Error("Can't run query %s: %d %s", query.c_str(), rc, reason.c_str());
if ((mysql_errno(&dbconn) != ER_LOCK_WAIT_TIMEOUT))
return 0;
}
}
// Might not be an int... FIXME
int id = mysql_insert_id(&dbconn);
Debug(2, "Success running sql insert %s. Resulting id is %d", query.c_str(), id);
return id;

View File

@@ -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<Microseconds>((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(),

View File

@@ -555,6 +555,12 @@ int FfmpegCamera::OpenFfmpeg() {
if (!mOptions.empty()) {
ret = av_dict_parse_string(&opts, mOptions.c_str(), "=", ",", 0);
const AVDictionaryEntry *entry = av_dict_get(opts, "thread_count", nullptr, AV_DICT_MATCH_CASE);
if (entry) {
mVideoCodecContext->thread_count = std::stoul(entry->value);
Debug(1, "Setting codec thread_count to %d", mVideoCodecContext->thread_count);
av_dict_set(&opts, "thread_count", nullptr, AV_DICT_MATCH_CASE);
}
// reorder_queue is for avformat not codec
av_dict_set(&opts, "reorder_queue_size", nullptr, AV_DICT_MATCH_CASE);
av_dict_set(&opts, "probesize", nullptr, AV_DICT_MATCH_CASE);

View File

@@ -1432,13 +1432,13 @@ void Monitor::CancelForced() {
void Monitor::actionReload() { shared_data->action |= RELOAD; }
void Monitor::actionEnable() {
shared_data->action |= RELOAD;
shared_data->capturing = true;
Info("actionEnable, capturing enabled.");
}
void Monitor::actionDisable() {
shared_data->action |= RELOAD;
shared_data->capturing = false;
Info("actionDisable, capturing temporarily disabled.");
}
void Monitor::actionSuspend() { shared_data->action |= SUSPEND; }
@@ -2745,6 +2745,10 @@ std::vector<std::shared_ptr<Monitor>> Monitor::LoadFfmpegMonitors(
* Returns -1 on failure.
*/
int Monitor::Capture() {
if (!shared_data->capturing) {
Debug(1, "Not capturing");
return 0;
}
unsigned int index = shared_data->image_count % image_buffer_count;
if (image_buffer.empty() or (index >= image_buffer.size())) {
Error("Image Buffer is invalid. Check ImageBufferCount. size is %zu",
@@ -3487,7 +3491,7 @@ bool Monitor::DumpSettings(char *output, bool verbose) {
alarm_ref_blend_perc);
sprintf(output + strlen(output), "Track Motion : %d\n", track_motion);
sprintf(output + strlen(output), "Capturing %d - %s\n", capturing,
Capturing_Strings[capturing].c_str());
Capturing_Strings[shared_data->capturing].c_str());
sprintf(output + strlen(output), "Analysing %d - %s\n", analysing,
Analysing_Strings[analysing].c_str());
sprintf(output + strlen(output), "Recording %d - %s\n", recording,

View File

@@ -105,7 +105,14 @@ void Monitor::ONVIF::start() {
if (rc != SOAP_OK) {
const char *detail = soap_fault_detail(soap);
Error("ONVIF Couldn't create subscription! %d, fault:%s, detail:%s", rc, soap_fault_string(soap), detail ? detail : "null");
if (rc > 8) {
Error("ONVIF Couldn't create subscription at %s! %d, fault:%s, detail:%s", full_url.c_str(),
rc, soap_fault_string(soap), detail ? detail : "null");
} else {
Error("ONVIF Couldn't create subscription at %s! %d %s, fault:%s, detail:%s", full_url.c_str(),
rc, SOAP_STRINGS[rc].c_str(),
soap_fault_string(soap), detail ? detail : "null");
}
std::stringstream ss;
std::ostream *old_stream = soap->os;

View File

@@ -20,6 +20,7 @@
#include "zm_time.h"
#include <cinttypes>
#include <ctime>
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<int64_t>(now_frac.count()));
return timeString;
}
SystemTimePoint StringToSystemTimePoint(const std::string &timestamp) {
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;
}

View File

@@ -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

View File

@@ -1171,20 +1171,8 @@ int VideoStore::writeVideoFramePacket(const std::shared_ptr<ZMPacket> zm_packet)
video_out_ctx->height
);
} else if (!zm_packet->in_frame) {
Debug(4, "Have no in_frame");
if (zm_packet->packet->size and !zm_packet->decoded) {
Debug(4, "Decoding");
if (!zm_packet->decode(video_in_ctx)) {
Debug(2, "unable to decode yet.");
return 0;
}
// Go straight to out frame
swscale.Convert(zm_packet->in_frame.get(), out_frame);
} else {
Error("Have neither in_frame or image in packet %d!",
zm_packet->image_index);
return 0;
} // end if has packet or image
Error("Have neither in_frame or image in packet %d!", zm_packet->image_index);
return 0;
} else {
// Have in_frame.... may need to convert it to out_frame
swscale.Convert(zm_packet->in_frame.get(), zm_packet->out_frame.get());
@@ -1278,9 +1266,11 @@ int VideoStore::writeVideoFramePacket(const std::shared_ptr<ZMPacket> zm_packet)
video_out_stream->time_base.den);
if (video_last_pts != AV_NOPTS_VALUE) {
opkt->duration = opkt->pts - video_last_pts;
opkt->duration = opkt->pts - video_last_pts;
Debug(1, "Duration %" PRId64 " from pts %" PRId64 " - last %" PRId64, opkt->duration, opkt->pts, video_last_pts);
if (opkt->duration < 0) opkt->duration = 0;
}
video_last_pts = zm_packet->in_frame->pts;
video_last_pts = opkt->pts;
write_packet(opkt.get(), video_out_stream);
} // end while receive_packet
} else { // Passthrough

View File

@@ -76,7 +76,7 @@ if (isset($_REQUEST['sort'])) {
// Offset specifies the starting row to return, used for pagination
$offset = 0;
if (isset($_REQUEST['offset'])) {
if (isset($_REQUEST['offset']) and ($_REQUEST['offset'] != 'NaN')) {
if ((!is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']))) {
ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']);
} else {
@@ -87,7 +87,7 @@ if (isset($_REQUEST['offset'])) {
// Limit specifies the number of rows to return
// Set the default to 0 for events view, to prevent an issue with ALL pagination
$limit = 0;
if (isset($_REQUEST['limit'])) {
if (isset($_REQUEST['limit']) and ($_REQUEST['limit'] != 'NaN')) {
if ((!is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']))) {
ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']);
} else {

View File

@@ -82,9 +82,15 @@ class EventsController extends AppController {
// TODO: Implement request based limits.
# 'limit' => '100',
'order' => array('StartDateTime'),
'paramType' => 'querystring',
);
if ($this->request->query('limit')) {
$settings['limit'] = $this->request->query('limit');
}
if ( isset($conditions['GroupId']) ) {
$settings['joins'] = array(
array(
@@ -99,17 +105,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']);

View File

@@ -27,7 +27,7 @@ define( 'ZM_CONFIG_SUBDIR', '@ZM_CONFIG_SUBDIR@' ); // Path to config subfolder
define( 'ZM_VERSION', '@VERSION@' ); // Version
define( 'ZM_DIR_TEMP', '@ZM_TMPDIR@' );
define( 'ZM_DIR_CACHE', '@ZM_CACHEDIR@' );
global $configvals;
global $zm_configvals;
$configFile = ZM_CONFIG;
$localConfigFile = basename($configFile);
@@ -40,8 +40,8 @@ if ( file_exists($localConfigFile) && filesize($localConfigFile) > 0 ) {
}
# Process name, value pairs from the main config file first
$configvals = process_configfile($configFile);
if (!$configvals) $configvals = [];
$zm_configvals = process_configfile($configFile);
if (!$zm_configvals) $zm_configvals = [];
# Search for user created config files. If one or more are found then
# update our config value array with those values
@@ -51,7 +51,7 @@ if ( is_dir($configSubFolder) ) {
foreach ( glob($configSubFolder.'/*.conf') as $filename ) {
//error_log("processing $filename");
$newconfigvals = process_configfile($filename);
if ($newconfigvals) $configvals = array_replace($configvals, $newconfigvals);
if ($newconfigvals) $zm_configvals = array_replace($zm_configvals, $newconfigvals);
}
} else {
error_log('WARNING: ZoneMinder configuration subfolder found but is not readable. Check folder permissions on '.$configSubFolder);
@@ -62,7 +62,7 @@ if ( is_dir($configSubFolder) ) {
# Now that our array our finalized, define each key => value
# pair in the array as a constant
foreach ( $configvals as $key => $value ) {
foreach ( $zm_configvals as $key => $value ) {
define($key, $value);
}
@@ -229,7 +229,7 @@ $GLOBALS['defaultUser'] = array(
);
function loadConfig( $defineConsts=true ) {
global $config;
global $zm_config;
global $dbConn;
$config = array();
@@ -238,18 +238,18 @@ function loadConfig( $defineConsts=true ) {
if ( !$result )
echo mysql_error();
while( $row = dbFetchNext($result) ) {
$config[$row['Name']] = $row;
$zm_config[$row['Name']] = $row;
if ( $defineConsts ) {
# Values in conf.d files override db so check if already defined and update value
if ( ! defined($row['Name']) ) {
define($row['Name'], $row['Value']);
} else {
$config[$row['Name']]['Value'] = constant($row['Name']);
$zm_config[$row['Name']]['Value'] = constant($row['Name']);
}
}
}
return $config;
return $zm_config;
} # end function loadConfig
require_once('Server.php');

View File

@@ -160,3 +160,7 @@ body.sticky #monitorList thead {
font-size: 16px;
}
}
.colThumbnail img:hover {
position: absolute;
}

View File

@@ -296,6 +296,7 @@ function getNormalNavBarHTML($running, $user, $bandwidth_options, $view, $skin)
echo getReportsHTML($view);
echo getRprtEvntAuditHTML($view);
echo getMapHTML($view);
echo getAdditionalLinksHTML($view);
echo getHeaderFlipHTML();
echo '</ul></div><div id="accountstatus">
';
@@ -426,6 +427,7 @@ function getCollapsedNavBarHTML($running, $user, $bandwidth_options, $view, $ski
echo getReportsHTML($view);
echo getRprtEvntAuditHTML($view);
echo getMapHTML($view);
echo getAdditionalLinksHTML($view);
echo '</ul>';
}
?>
@@ -885,6 +887,23 @@ function getMapHTML($view) {
return $result;
}
// Returns the html representing the content of the ZM_WEB_NAVBAR_LINKS content
function getAdditionalLinksHTML($view) {
$result = '';
if (defined('ZM_WEB_NAVBAR_LINKS')) {
if (ZM_WEB_NAVBAR_LINKS) {
foreach (explode(',', ZM_WEB_NAVBAR_LINKS) as $link) {
$result .= '<li class="nav-item">'.$link.'</li>'.PHP_EOL;
}
}
}
return $result;
}
// Returns the html representing the header collapse toggle menu item
function getHeaderFlipHTML() {
$result = '';

View File

@@ -1076,6 +1076,15 @@ function thumbnail_onmouseover(event) {
const imgClass = ( currentView == 'console' ) ? 'zoom-console' : 'zoom';
const imgAttr = ( currentView == 'frames' ) ? 'full_img_src' : 'stream_src';
img.src = img.getAttribute(imgAttr);
if ( currentView == 'console' ) {
const rect = img.getBoundingClientRect();
const zoomHeight = rect.height * 5; // scale factor defined in css
if ( rect.bottom + (zoomHeight - rect.height) > window.innerHeight ) {
img.style.transformOrigin = '0% 100%';
} else {
img.style.transformOrigin = '0% 0%';
}
}
thumbnail_timeout = setTimeout(function() {
img.classList.add(imgClass);
}, 250);
@@ -1088,6 +1097,9 @@ function thumbnail_onmouseout(event) {
var imgAttr = ( currentView == 'frames' ) ? 'img_src' : 'still_src';
img.src = img.getAttribute(imgAttr);
img.classList.remove(imgClass);
if ( currentView == 'console' ) {
img.style.transformOrigin = '';
}
}
function initThumbAnimation() {

View File

@@ -145,8 +145,8 @@ stateStrings[STATE_ALERT] = "<?php echo translate('Alert') ?>";
global $user;
if ($user) {
// Only include config if logged in or auth turned off. The login view doesn't require any config.
global $config;
foreach ($config as $name=>$c) {
global $zm_config;
foreach ($zm_config as $name=>$c) {
if (!$c['Private']) {
$value = preg_replace('/(\n\r?)/', '\\\\$1', $c['Value']);
$value = preg_replace('/\'/', '\\\\\'', $value);

View File

@@ -88,7 +88,7 @@ foreach ($dnsmasq_config as $name=>$value) {
# Handled below
} else {
echo '<div class="row"><label class="form-label">'.$name.'</label><span class="value">'.PHP_EOL;
echo '<input type="text" name="config['.validHtmlStr($name).']" value="'.validHtmlStr($value).'"/></span></div>'.PHP_EOL;
echo '<input type="text" name="dnsmasq_config['.validHtmlStr($name).']" value="'.validHtmlStr($value).'"/></span></div>'.PHP_EOL;
}
}
?>
@@ -97,24 +97,25 @@ foreach ($dnsmasq_config as $name=>$value) {
<div class="leases"><h2>Leases</h2>
<?php
function process_dnsmasq_configfile($configFile) {
$configvals = array();
$our_configvals = array();
if (is_readable($configFile)) {
$cfg = fopen($configFile, 'r') or ZM\Error('Could not open config file: '.$configFile);
while ( !feof($cfg) ) {
$str = fgets($cfg, 256);
if ( preg_match('/^\s*(#.*)?$/', $str) ) {
continue;
} else if ( preg_match('/^\s*([^=\s]+)\s*(=\s*[\'"]*(.*?)[\'"]*\s*)?$/', $str, $matches) ) {
$configvals[$matches[1]] = isset($matches[3]) ? $matches[3] : 'yes';
} else {
ZM\Error("Malformed line in config $configFile\n$str");
}
} else if ( preg_match('/^\s*([^=\s]+)\s*(=\s*option:[^=\s]+)?(=\s*[\'"]*(.*?)[\'"]*\s*)?$/', $str, $matches) ) {
//ZM\Debug(print_r($matches, true));
$our_configvals[$matches[1].(isset($matches[2])?$matches[2]:'')] = isset($matches[4]) ? $matches[4] : 'yes';
} else {
ZM\Error("Malformed line in config $configFile\n$str");
}
}
fclose($cfg);
} else {
ZM\Error('WARNING: dnsmasq configuration file found but is not readable. Check file permissions on '.$configFile);
}
return $configvals;
return $our_configvals;
}
function read_leasefile($file) {

View File

@@ -41,7 +41,7 @@ echo getNavBarHTML();
</div>
<!-- Table styling handled by bootstrap-tables -->
<div class="row justify-content-center table-responsive-sm">
<div id="content" class="row justify-content-center table-responsive-sm">
<table
id="framesTable"
data-locale="<?php echo i18n() ?>"

View File

@@ -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.
@@ -41,30 +50,37 @@ function evaluateLoadTimes() {
// limit this from about 40fps to .1 fps
currentDisplayInterval = Math.min(Math.max(currentDisplayInterval, 40), 10000);
imageLoadTimesEvaluated=0;
setSpeed(speedIndex);
//setSpeed(speedIndex);
$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<len; i++) {
const event_id = events_for_monitor[monId][i].Id;
const e = events[event_id];
@@ -207,19 +222,28 @@ function getFrame(monId, time, last_Frame) {
}
}
}
if (!Event) return;
if (!Event) {
console.log('No event found for ' + time + ' ' + secs2inputstr(time) + ' on monitor ' + monId);
return;
}
if (!Event.FramesById) {
// It is assumed at this time that every event has frames
console.log('No FramesById for event ', Event.Id);
load_Frames({event_id: Event}).then(function() {
var event_list = {};
event_list[Event.Id] = Event;
loadFrames(event_list).then(function() {
if (!Event.FramesById) {
console.log("No FramesById after load_Frames!", Event);
console.log("No FramesById after loadFrames!", Event);
}
return findFrameByTime(Event.FramesById, time);
}, function(Error) {
console.log(Error);
});
return;
} else if (!Event.FramesById.length) {
console.log("frames loading for event " + Event.Id);
return;
}
// Need to get frame by time, not some fun calc that assumes frames have the same length.
@@ -302,37 +326,32 @@ function getImageSource(monId, time) {
return;
}
let scale = parseInt(100*monitorCanvasObj[monId].width / monitorWidth[monId]);
let scale = parseInt(100 * monitorCanvasObj[monId].width / monitorWidth[monId]);
if (scale > 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 {
@@ -423,103 +425,91 @@ function loadImage2Monitor( monId, url ) {
function timerFire() {
// See if we need to reschedule
if ( ( currentDisplayInterval != timerInterval ) || ( currentSpeed == 0 ) ) {
console.log("Turn off interrupts timerInterfave", timerInterval, currentDisplayInterval, currentSpeed);
// zero just turn off interrupts
clearInterval(timerObj);
timerObj = null;
timerInterval = currentDisplayInterval;
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);
outputUpdate(currentTimeSecs);
} else {
//console.log("Current time " + currentTimeSecs + " + " + playSecsPerInterval);
} else if (playSecsPerInterval || (currentTimeSecs==minTimeSecs)) {
outputUpdate(playSecsPerInterval + currentTimeSecs);
}
return;
}
if ((currentSpeed > 0 || liveMode != 0) && !timerObj) {
timerObj = setInterval(timerFire, timerInterval); // don't fire out of live mode if speed is zero
} else {
console.log("timefire", "CurrentSpeed", currentSpeed, "liveMode", liveMode, timerObj);
}
} // 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;
const x = (sliderX > cWidth/2) ? sliderX - len - 10 : 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.innerHTML = secs2dbstr(minTimeSecs);
o.style.position = "absolute";
o.style.bottom = labbottom;
o.style.font = labfont;
o.style.left = "5px";
o = document.getElementById('scrubleft');
if ( numMonitors == 0 ) { // we need a len calculation if we skipped the slider
len = o.offsetWidth;
}
@@ -532,13 +522,7 @@ function drawSliderOnGraph(val) {
o.style.display = "inline-flex"; // safari won't take this but will just ignore
}
var o = document.getElementById('scrubright');
o.innerHTML = secs2dbstr(maxTimeSecs);
o.style.position = "absolute";
o.style.bottom = labbottom;
o.style.font = labfont;
// If the slider will overlay part of this suppress (this is the right side)
o.style.left=(cWidth - len - 15).toString() + "px";
o = document.getElementById('scrubright');
if ( sliderX > cWidth - len - 20 || cWidth < len * 4 ) {
o.style.display = "none";
} else {
@@ -547,68 +531,89 @@ function drawSliderOnGraph(val) {
}
}
function drawFrameOnGraph(frame) {
if (!frame.Score) return;
// Now put in scored frames (if any)
let x1 = parseInt( (frame.TimeStampSecs - minTimeSecs) / rangeTimeSecs * cWidth); // round low end down
let 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;
underSlider = undefined; // flag we don't have a slider cached
const 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)
cHeight = parseInt(window.innerHeight * 0.10);
if ( cHeight < numMonitors * 20 ) {
cHeight = parseInt(window.innerHeight * 0.10); // 10%
if (cHeight < numMonitors * 20) { //Minimum 20px per monitor maybe it should be 10px per monitor?
cHeight = numMonitors * 20;
}
canvas.height = cHeight;
if ( events && ( Object.keys(events).length == 0 ) ) {
ctx.globalAlpha = 1;
if (events && ( Object.keys(events).length == 0 ) ) {
ctx.font = "40px Georgia";
ctx.globalAlpha = 1;
ctx.fillStyle = "white";
var t = "No data found in range - choose differently";
const t = LOADING ? "Loading events" : "No events found.";
var l = ctx.measureText(t).width;
ctx.fillText(t, (cWidth - l)/2, cHeight-10);
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
// Note that this may be a sparse array
// Should we clear the canvas?
// 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
// At first, no events loaded, that's ok, later, we will have some events, should only draw those in the time range.
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++ ) {
// Note that this may be a sparse array
for (let i=0; i < numMonitors; i++) {
// Apparently we have to set these each time before calling fillText
ctx.font = parseInt(rowHeight * timeLabelsFractOfRow).toString() + "px Georgia";
ctx.fillStyle = "white";
ctx.globalAlpha = 1;
ctx.fillStyle = "white";
// This should roughly center font in row
ctx.fillText(monitorName[monitorPtr[i]], 0, (i + 1 - (1 - timeLabelsFractOfRow)/2 ) * rowHeight);
}
underSlider = undefined; // flag we don't have a slider cached
drawSliderOnGraph(currentTimeSecs);
return;
} // end function drawGraph
function redrawScreen() {
@@ -624,7 +629,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 +651,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 +684,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 +733,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 +754,7 @@ function secs2inputstr(s) {
}
function secs2dbstr(s) {
if ( ! parseInt(s) ) {
if (!parseInt(s)) {
console.log("Invalid value for " + s + " seconds");
return '';
}
@@ -761,7 +767,7 @@ function secs2dbstr(s) {
}
function setFit(value) {
fitMode=value;
fitMode = value;
redrawScreen();
}
@@ -896,6 +902,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 +948,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
@@ -991,47 +998,201 @@ function clickMonitor(event) {
return;
}
function changeDateTime(e) {
/*
var minTime_element = $j('#minTime');
var maxTime_element = $j('#maxTime');
function changeFilters(e) {
console.log(e, this);
// Need to update minTimeSecs and maxTimeSecs
var minTime = moment(minTime_element.val());
var maxTime = moment(maxTime_element.val());
if ( minTime.isAfter(maxTime) ) {
maxTime_element.parent().addClass('has-error');
return; // Don't reload because we have invalid datetime filter.
let minMoment, maxMoment;
const matches = this.name.match(/^filter\[Query\]\[terms\]\[(\d+)\]\[val\]$/);
console.log(matches);
if (matches && matches.length) {
const name = 'filter[Query][terms]['+matches[1]+'][attr]';
const attr = this.form.elements[name];
if (attr && (attr.value == 'StartDateTime')) {
const val = this;
const op = this.form.elements['filter[Query][terms]['+matches[1]+'][op]'];
if (op.value == '>=') {
minMoment = moment(val.value, 'YYYY-MM-DD HH:mm:ss');
if (!minMoment.isValid()) {
alert("Date start is not valid." + val.value);
return;
}
} else if (op == '<=') {
maxMoment = moment(val.value, 'YYYY-MM-DD HH:mm:ss');
if (!maxMoment.isValid()) maxMoment = moment();
}
} else {
console.log("No attr", attr);
}
} else {
maxTime_element.parent().removeClass('has-error');
const regexp = /^filter\[Query\]\[terms\]\[(\d+)\]\[attr\]$/;
$j('#fieldsTable input[value="StartDateTime"]').each(function(index) {
const matches = this.name.match(regexp);
console.log('looking at', this, matches);
if (matches && matches.length) {
const val = this.form.elements['filter[Query][terms]['+matches[1]+'][val]'];
if (val) {
const op = this.form.elements['filter[Query][terms]['+matches[1]+'][op]'];
if (op.value == '>=') {
minMoment = moment(val.value, 'YYYY-MM-DD HH:mm:ss');
if (!minMoment.isValid()) {
alert("Date start is not valid." + val.value);
return;
}
} else if (op == '<=') {
maxMoment = moment(val.value, 'YYYY-MM-DD HH:mm:ss');
if (!maxMoment.isValid()) maxMoment = moment();
}
} else {
console.log("no val ", matches);
}
} else {
console.log("No matches for ", this.name);
}
});
} // end if a datetime or something else
if (minMoment) {
minTimeSecs = minMoment.unix();
console.log("Set minMoment to ", minTimeSecs);
if (currentTimeSecs < minTimeSecs) {
console.log("Adjusting currentTimeSecs", currentTimeSecs, minTimeSecs);
currentTimeSecs = minTimeSecs;
}
} else {
console.log("No minMoment");
}
var minStr = "&minTime="+($j('#minTime')[0].value);
var maxStr = "&maxTime="+($j('#maxTime')[0].value);
*/
var zoomStr="";
for ( var i=0; i < numMonitors; i++ ) {
if ( monitorZoomScale[monitorPtr[i]] < 0.99 || monitorZoomScale[monitorPtr[i]] > 1.01 ) { // allow for some up/down changes and just treat as 1 of almost 1
zoomStr += "&z" + monitorPtr[i].toString() + "=" + monitorZoomScale[monitorPtr[i]].toFixed(2);
}
if (maxMoment) {
maxTimeSecs = maxMoment.unix();
console.log(currentTimeSecs, minTimeSecs, maxTimeSecs);
if (currentTimeSecs > maxTimeSecs) currentTimeSecs = maxTimeSecs;
} else {
console.log("No maxMoment");
}
// 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];
//var uri = "?view=" + currentView + fitStr + minStr + maxStr + liveStr + zoomStr + "&scale=" + $j("#scaleslider")[0].value + "&speed=" + speeds[$j("#speedslider")[0].value];
window.location = uri;
drawGraph(); // Will use new values
loadEventData();
wait_for_events();
}
// >>>>>>>>> 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; i<len; i++) {
const ev = data.events[i].Event;
events[parseInt(ev.Id)] = ev;
if (!events_by_monitor_id[ev.MonitorId]) {
events_by_monitor_id[ev.MonitorId] = []; // just event ids
events_for_monitor[ev.MonitorId] = []; // id=>event
}
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 +1202,39 @@ 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();
wait_for_events();
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);
});
@@ -1119,15 +1260,26 @@ function initPage() {
$j('#fieldsTable input, #fieldsTable select').each(function(index) {
const el = $j(this);
if (el.hasClass('datetimepicker')) {
el.datetimepicker({timeFormat: "HH:mm:ss", dateFormat: "yy-mm-dd", maxDate: 0, constrainInput: false, onClose: changeDateTime});
el.datetimepicker({timeFormat: "HH:mm:ss", dateFormat: "yy-mm-dd", maxDate: 0, constrainInput: false, onClose: changeFilters});
} else if (el.hasClass('datepicker')) {
el.datepicker({dateFormat: "yy-mm-dd", maxDate: 0, constrainInput: false, onClose: changeDateTime});
el.datepicker({dateFormat: "yy-mm-dd", maxDate: 0, constrainInput: false, onClose: changeFilters});
} else {
el.on('change', changeDateTime);
el.on('change', changeFilters);
}
});
}
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() {
monitor_ids = [];
for (const key in monitorIndex) {
@@ -1156,8 +1308,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) {
console.log("Loading frames", zm_events);
function loadFrames(zm_events) {
return new Promise(function(resolve, reject) {
const url = Servers[serverId].urlToApi()+'/frames/index';
@@ -1219,4 +1370,4 @@ function load_Frames(zm_events) {
} // end while zm_events.legtnh
} // end Promise
);
} // end function load_Frames(Event)
} // end function loadFrames(Event)

View File

@@ -31,6 +31,7 @@ var fitMode=<?php echo $fitMode?>;
// slider scale, which is only for replay and relative to real time
var currentSpeed=<?php echo $speeds[$speedIndex]?>;
var speedIndex=<?php echo $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': <?php echo $monitor->Id() ?>,
'Name': '<?php echo $monitor->Name() ?>',
'connKey': '<?php echo $monitor->connKey() ?>',
'Width': <?php echo $monitor->ViewWidth() ?>,
'Height':<?php echo $monitor->ViewHeight() ?>,
'JanusEnabled':<?php echo $monitor->JanusEnabled() ?>,
'Url': '<?php echo $monitor->UrlToIndex( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>',
'UrlToZms': '<?php echo $monitor->UrlToZMS( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>',
'onclick': function(){window.location.assign( '?view=watch&mid=<?php echo $monitor->Id() ?>' );},
'Type': '<?php echo $monitor->Type() ?>',
'Refresh': '<?php echo $monitor->Refresh() ?>',
'Janus_Pin': '<?php echo $monitor->Janus_Pin() ?>',
'WebColour': '<?php echo $monitor->WebColour() ?>'
};
<?php
} // end foreach monitor
echo '
var monitorName = [];
@@ -183,16 +210,17 @@ foreach ( $monitors as $m ) {
}
echo "
var numMonitors = $numMonitors;
var minTimeSecs=parseInt($minTimeSecs);
var maxTimeSecs=parseInt($maxTimeSecs);
var minTimeSecs =parseInt($minTimeSecs);
var maxTimeSecs =parseInt($maxTimeSecs);
var minTime='$minTime';
var maxTime='$maxTime';
";
echo 'var rangeTimeSecs='.($maxTimeSecs - $minTimeSecs + 1).";\n";
if ( isset($defaultCurrentTimeSecs) )
if ( isset($defaultCurrentTimeSecs) ) {
echo 'var currentTimeSecs=parseInt('.$defaultCurrentTimeSecs.");\n";
else
} else {
echo 'var currentTimeSecs=parseInt('.$minTimeSecs.");\n";
}
echo 'var speeds=[';
for ( $i=0; $i < count($speeds); $i++ )
@@ -202,6 +230,7 @@ echo "];\n";
var cWidth; // save canvas width
var cHeight; // save canvas height
var rowHeight = 0;
var canvas; // global canvas definition so we don't have to keep looking it up
var ctx = null;
var underSlider; // use this to hold what is hidden by the slider