mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-03-13 03:18:25 -04:00
* Allows users to set an specific "Onvif alarm text" for each monitor With this personalization the cameras that use strings different to "MotionAlarm" such as Dahua, will be able to process their Onvif alarms. To make it easier, the change sets a default "Alarm Text" value for it to work out of the box and then allows to modify it according to the particular necessities of each brand. It consists of a new column on table Monitors, changes on Monitors.h, Monitors.cpp and a change on UI to manage the value of Alarm Text per camera. Updated es_la.php language file. Co-authored-by: ovargasp <ovargasp@hoitmail.com>
969 lines
35 KiB
PHP
969 lines
35 KiB
PHP
<?php
|
|
namespace ZM;
|
|
require_once('database.php');
|
|
require_once('Object.php');
|
|
require_once('Control.php');
|
|
require_once('Group.php');
|
|
require_once('Manufacturer.php');
|
|
require_once('Model.php');
|
|
require_once('Server.php');
|
|
require_once('Storage.php');
|
|
require_once('Zone.php');
|
|
|
|
class Monitor extends ZM_Object {
|
|
protected static $FunctionTypes = null;
|
|
|
|
public static function getFunctionTypes() {
|
|
if (!isset($FunctionTypes)) {
|
|
$FunctionTypes = array(
|
|
'None' => translate('FnNone'),
|
|
'Monitor' => translate('FnMonitor'),
|
|
'Modect' => translate('FnModect'),
|
|
'Record' => translate('FnRecord'),
|
|
'Mocord' => translate('FnMocord'),
|
|
'Nodect' => translate('FnNodect')
|
|
);
|
|
}
|
|
return $FunctionTypes;
|
|
}
|
|
|
|
protected static $CapturingOptions = null;
|
|
public static function getCapturingOptions() {
|
|
if (!isset($CapturingOptions)) {
|
|
$CapturingOptions = array(
|
|
'None'=>translate('None'),
|
|
'Ondemand' => translate('On Demand'),
|
|
'Always' => translate('Always'),
|
|
);
|
|
}
|
|
return $CapturingOptions;
|
|
}
|
|
|
|
protected static $AnalysingOptions = null;
|
|
public static function getAnalysingOptions() {
|
|
if (!isset($AnalysingOptions)) {
|
|
$AnalysingOptions = array(
|
|
'None' => translate('None'),
|
|
'Always' => translate('Always'),
|
|
);
|
|
}
|
|
return $AnalysingOptions;
|
|
}
|
|
|
|
protected static $AnalysisSourceOptions = null;
|
|
public static function getAnalysisSourceOptions() {
|
|
if (!isset($AnalysisSourceOptions)) {
|
|
$AnalysisSourceOptions = array(
|
|
'Primary' => translate('Primary'),
|
|
'Secondary' => translate('Secondary'),
|
|
);
|
|
}
|
|
return $AnalysisSourceOptions;
|
|
}
|
|
|
|
protected static $AnalysisImageOptions = null;
|
|
public static function getAnalysisImageOptions() {
|
|
if (!isset($AnalysisImageOptions)) {
|
|
$AnalysisImageOptions = array(
|
|
'FullColour' => translate('Full Colour'),
|
|
'YChannel' => translate('Y-Channel (Greyscale)'),
|
|
);
|
|
}
|
|
return $AnalysisImageOptions;
|
|
}
|
|
|
|
protected static $RecordingOptions = null;
|
|
public static function getRecordingOptions() {
|
|
if (!isset($RecordingOptions)) {
|
|
$RecordingOptions = array(
|
|
'None' => translate('None'),
|
|
'OnMotion' => translate('On Motion / Trigger / etc'),
|
|
'Always' => translate('Always'),
|
|
);
|
|
}
|
|
return $RecordingOptions;
|
|
}
|
|
|
|
protected static $RecordingSourceOptions = null;
|
|
public static function getRecordingSourceOptions() {
|
|
if (!isset($RecordingSourceOptions)) {
|
|
$RecordingSourceOptions = array(
|
|
'Primary' => translate('Primary'),
|
|
'Secondary' => translate('Secondary'),
|
|
'Both' => translate('Both'),
|
|
);
|
|
}
|
|
return $RecordingSourceOptions;
|
|
}
|
|
|
|
protected static $DecodingOptions = null;
|
|
public static function getDecodingOptions() {
|
|
if (!isset($DecodingOptions)) {
|
|
$DecodingOptions = array(
|
|
'None' => translate('None'),
|
|
'Ondemand' => translate('On Demand'),
|
|
'KeyFrames' => translate('KeyFrames Only'),
|
|
'KeyFrames+Ondemand' => translate('Keyframes + Ondemand'),
|
|
'Always' => translate('Always'),
|
|
);
|
|
}
|
|
return $DecodingOptions;
|
|
}
|
|
|
|
protected static $Statuses = null;
|
|
public static function getStatuses() {
|
|
if (!isset($Statuses)) {
|
|
$Statuses = array(
|
|
-1 => 'Unknown',
|
|
0 => 'Idle',
|
|
1 => 'PreAlarm',
|
|
2 => 'Alarm',
|
|
3 => 'Alert',
|
|
4 => 'Tape'
|
|
);
|
|
}
|
|
return $Statuses;
|
|
}
|
|
|
|
protected static $table = 'Monitors';
|
|
|
|
protected $defaults = array(
|
|
'Id' => null,
|
|
'Name' => array('type'=>'text','filter_regexp'=>'/[^\w\-\.\(\)\:\/ ]/', 'default'=>'Monitor'),
|
|
'Notes' => '',
|
|
'ServerId' => 0,
|
|
'StorageId' => 0,
|
|
'ManufacturerId' => null,
|
|
'ModelId' => null,
|
|
'Type' => 'Ffmpeg',
|
|
'Capturing' => 'Always',
|
|
'Analysing' => 'Always',
|
|
'Recording' => 'Always',
|
|
'RecordingSource' => 'Primary',
|
|
'AnalysisSource' => 'Primary',
|
|
'AnalysisImage' => 'FullColour',
|
|
'Enabled' => array('type'=>'boolean','default'=>1),
|
|
'Decoding' => 'Always',
|
|
'JanusEnabled' => array('type'=>'boolean','default'=>0),
|
|
'JanusAudioEnabled' => array('type'=>'boolean','default'=>0),
|
|
'LinkedMonitors' => array('type'=>'set', 'default'=>null),
|
|
'Triggers' => array('type'=>'set','default'=>''),
|
|
'EventStartCommand' => '',
|
|
'EventEndCommand' => '',
|
|
'ONVIF_URL' => '',
|
|
'ONVIF_Username' => '',
|
|
'ONVIF_Password' => '',
|
|
'ONVIF_Options' => '',
|
|
'Onvif_Alarm_Txt' => '',
|
|
'ONVIF_Event_Listener' => '0',
|
|
'use_Amcrest_API' => '0',
|
|
'Device' => '',
|
|
'Channel' => 0,
|
|
'Format' => '0',
|
|
'V4LMultiBuffer' => null,
|
|
'V4LCapturesPerFrame' => 1,
|
|
'Protocol' => null,
|
|
'Method' => '',
|
|
'Host' => null,
|
|
'Port' => '',
|
|
'SubPath' => '',
|
|
'Path' => null,
|
|
'SecondPath' => null,
|
|
'Options' => null,
|
|
'User' => null,
|
|
'Pass' => null,
|
|
// These are NOT NULL default 0 in the db, but 0 is not a valid value. FIXME
|
|
'Width' => null,
|
|
'Height' => null,
|
|
'Colours' => 4,
|
|
'Palette' => '0',
|
|
'Orientation' => 'ROTATE_0',
|
|
'Deinterlacing' => 0,
|
|
'DecoderHWAccelName' => null,
|
|
'DecoderHWAccelDevice' => null,
|
|
'SaveJPEGs' => 3,
|
|
'VideoWriter' => '0',
|
|
'OutputCodec' => null,
|
|
'Encoder' => 'auto',
|
|
'OutputContainer' => null,
|
|
'EncoderParameters' => "# Lines beginning with # are a comment \n# For changing quality, use the crf option\n# 1 is best, 51 is worst quality\ncrf=23\n",
|
|
'RecordAudio' => array('type'=>'boolean', 'default'=>0),
|
|
#'OutputSourceStream' => 'Primary',
|
|
'RTSPDescribe' => array('type'=>'boolean','default'=>0),
|
|
'Brightness' => -1,
|
|
'Contrast' => -1,
|
|
'Hue' => -1,
|
|
'Colour' => -1,
|
|
'EventPrefix' => 'Event-',
|
|
'LabelFormat' => '%N - %d/%m/%y %H:%M:%S',
|
|
'LabelX' => 0,
|
|
'LabelY' => 0,
|
|
'LabelSize' => 1,
|
|
'ImageBufferCount' => 3,
|
|
'MaxImageBufferCount' => 0,
|
|
'WarmupCount' => 0,
|
|
'PreEventCount' => 5,
|
|
'PostEventCount' => 5,
|
|
'StreamReplayBuffer' => 0,
|
|
'AlarmFrameCount' => 1,
|
|
'SectionLength' => 600,
|
|
'MinSectionLength' => 10,
|
|
'FrameSkip' => 0,
|
|
'MotionFrameSkip' => 0,
|
|
'AnalysisFPSLimit' => null,
|
|
'AnalysisUpdateDelay' => 0,
|
|
'MaxFPS' => null,
|
|
'AlarmMaxFPS' => null,
|
|
'FPSReportInterval' => 100,
|
|
'RefBlendPerc' => 6,
|
|
'AlarmRefBlendPerc' => 6,
|
|
'Controllable' => array('type'=>'boolean','default'=>0),
|
|
'ControlId' => null,
|
|
'ControlDevice' => null,
|
|
'ControlAddress' => null,
|
|
'AutoStopTimeout' => null,
|
|
'TrackMotion' => array('type'=>'boolean','default'=>0),
|
|
'TrackDelay' => null,
|
|
'ReturnLocation' => -1,
|
|
'ReturnDelay' => null,
|
|
'ModectDuringPTZ' => array('type'=>'boolean','default'=>0),
|
|
'DefaultRate' => 100,
|
|
'DefaultScale' => 0,
|
|
'SignalCheckPoints' => 0,
|
|
'SignalCheckColour' => '#0000BE',
|
|
'WebColour' => '#ff0000',
|
|
'Exif' => array('type'=>'boolean', 'default'=>0),
|
|
'Sequence' => null,
|
|
'ZoneCount' => 0,
|
|
'Refresh' => null,
|
|
'DefaultCodec' => 'auto',
|
|
'GroupIds' => array('default'=>array(), 'do_not_update'=>1),
|
|
'Latitude' => null,
|
|
'Longitude' => null,
|
|
'RTSPServer' => array('type'=>'boolean', 'default'=>0),
|
|
'RTSPStreamName' => '',
|
|
'Importance' => 'Normal',
|
|
);
|
|
private $status_fields = array(
|
|
'Status' => null,
|
|
'AnalysisFPS' => null,
|
|
'CaptureFPS' => null,
|
|
'CaptureBandwidth' => null,
|
|
);
|
|
private $summary_fields = array(
|
|
'TotalEvents' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'TotalEventDiskSpace' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'HourEvents' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'HourEventDiskSpace' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'DayEvents' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'DayEventDiskSpace' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'WeekEvents' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'WeekEventDiskSpace' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'MonthEvents' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'MonthEventDiskSpace' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'ArchivedEvents' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
'ArchivedEventDiskSpace' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1),
|
|
);
|
|
|
|
public function Control() {
|
|
if (!property_exists($this, 'Control')) {
|
|
if ($this->ControlId())
|
|
$this->{'Control'} = Control::find_one(array('Id'=>$this->{'ControlId'}));
|
|
|
|
if (!(property_exists($this, 'Control') and $this->{'Control'}))
|
|
$this->{'Control'} = new Control();
|
|
}
|
|
return $this->{'Control'};
|
|
}
|
|
|
|
public function Server() {
|
|
if (!property_exists($this, 'Server')) {
|
|
if ($this->ServerId())
|
|
$this->{'Server'} = Server::find_one(array('Id'=>$this->{'ServerId'}));
|
|
if (!property_exists($this, 'Server')) {
|
|
$this->{'Server'} = new Server();
|
|
}
|
|
}
|
|
return $this->{'Server'};
|
|
}
|
|
|
|
public function __call($fn, array $args) {
|
|
if (count($args)) {
|
|
if (is_array($this->defaults[$fn]) and $this->defaults[$fn]['type'] == 'set') {
|
|
$this->{$fn} = is_array($args[0]) ? implode(',', $args[0]) : $args[0];
|
|
} else {
|
|
$this->{$fn} = $args[0];
|
|
}
|
|
}
|
|
if (property_exists($this, $fn)) {
|
|
return $this->{$fn};
|
|
} else if (array_key_exists($fn, $this->defaults)) {
|
|
if ( is_array($this->defaults[$fn]) ) {
|
|
return $this->defaults[$fn]['default'];
|
|
}
|
|
return $this->defaults[$fn];
|
|
} else if (array_key_exists($fn, $this->status_fields)) {
|
|
$sql = 'SELECT * FROM `Monitor_Status` WHERE `MonitorId`=?';
|
|
$row = dbFetchOne($sql, NULL, array($this->{'Id'}));
|
|
if (!$row) {
|
|
Warning('Unable to load Monitor status record for Id='.$this->{'Id'}.' using '.$sql);
|
|
return null;
|
|
} else {
|
|
foreach ($row as $k => $v) {
|
|
$this->{$k} = $v;
|
|
}
|
|
}
|
|
return $this->{$fn};
|
|
} else if (array_key_exists($fn, $this->summary_fields)) {
|
|
$sql = 'SELECT * FROM `Event_Summaries` WHERE `MonitorId`=?';
|
|
$row = dbFetchOne($sql, NULL, array($this->{'Id'}));
|
|
if (!$row) {
|
|
Warning('Unable to load Event Summary record for Id='.$this->{'Id'}.' using '.$sql);
|
|
return null;
|
|
} else {
|
|
foreach ($row as $k => $v) {
|
|
$this->{$k} = $v;
|
|
}
|
|
}
|
|
return $this->{$fn};
|
|
} else {
|
|
$backTrace = debug_backtrace();
|
|
$file = $backTrace[1]['file'];
|
|
$line = $backTrace[1]['line'];
|
|
Warning("Unknown function call Monitor->$fn from $file:$line");
|
|
}
|
|
}
|
|
|
|
public function getStreamSrc($args, $querySep='&') {
|
|
$streamSrc = $this->Server()->UrlToZMS(
|
|
ZM_MIN_STREAMING_PORT ?
|
|
ZM_MIN_STREAMING_PORT+$this->{'Id'} :
|
|
null);
|
|
|
|
$args['monitor'] = $this->{'Id'};
|
|
|
|
if (ZM_OPT_USE_AUTH) {
|
|
if (ZM_AUTH_RELAY == 'hashed') {
|
|
$args['auth'] = generateAuthHash(ZM_AUTH_HASH_IPS);
|
|
} elseif ( ZM_AUTH_RELAY == 'plain' ) {
|
|
$args['user'] = $_SESSION['username'];
|
|
$args['pass'] = $_SESSION['password'];
|
|
} elseif ( ZM_AUTH_RELAY == 'none' ) {
|
|
$args['user'] = $_SESSION['username'];
|
|
}
|
|
}
|
|
if (ZM_RAND_STREAM) {
|
|
$args['rand'] = time();
|
|
}
|
|
|
|
# zms doesn't support width & height, so if no scale is set, default it
|
|
if (!isset($args['scale'])) {
|
|
if (isset($args['width']) and intval($args['width'])) {
|
|
$args['scale'] = intval((100*intval($args['width']))/$this->ViewWidth());
|
|
} else if (isset($args['height']) and intval($args['height'])) {
|
|
$args['scale'] = intval((100*intval($args['height']))/$this->ViewHeight());
|
|
}
|
|
}
|
|
if ($args['scale'] <= 0) {
|
|
$args['scale'] = 100;
|
|
}
|
|
if (isset($args['width']))
|
|
unset($args['width']);
|
|
if (isset($args['height']))
|
|
unset($args['height']);
|
|
|
|
$streamSrc .= '?'.http_build_query($args, '', $querySep);
|
|
|
|
return $streamSrc;
|
|
} // end function getStreamSrc
|
|
|
|
public function isPortrait() {
|
|
return $this->ViewWidth() > $this->ViewHeight();
|
|
}
|
|
public function isLandscape() {
|
|
return $this->ViewWidth() < $this->ViewHeight();
|
|
}
|
|
|
|
public function ViewWidth($new = null) {
|
|
if ($new)
|
|
$this->{'Width'} = $new;
|
|
|
|
$field = ( $this->Orientation() == 'ROTATE_90' or $this->Orientation() == 'ROTATE_270' ) ? 'Height' : 'Width';
|
|
if (property_exists($this, $field))
|
|
return $this->{$field};
|
|
return $this->defaults[$field];
|
|
} // end function Width
|
|
|
|
public function ViewHeight($new=null) {
|
|
if ($new)
|
|
$this->{'Height'} = $new;
|
|
|
|
$field = ( $this->Orientation() == 'ROTATE_90' or $this->Orientation() == 'ROTATE_270' ) ? 'Width' : 'Height';
|
|
if (property_exists($this, $field))
|
|
return $this->{$field};
|
|
return $this->defaults[$field];
|
|
} // end function Height
|
|
|
|
public function SignalCheckColour($new=null) {
|
|
$field = 'SignalCheckColour';
|
|
if ($new) {
|
|
$this->{$field} = $new;
|
|
}
|
|
|
|
// Validate that it's a valid colour (we seem to allow color names, not just hex).
|
|
// This also helps prevent XSS.
|
|
if (property_exists($this, $field) && preg_match('/^[#0-9a-zA-Z]+$/', $this->{$field})) {
|
|
return $this->{$field};
|
|
}
|
|
return $this->defaults[$field];
|
|
} // end function SignalCheckColour
|
|
|
|
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);
|
|
}
|
|
|
|
function zmcControl($mode=false) {
|
|
if (!(property_exists($this,'Id') and $this->{'Id'})) {
|
|
Warning('Attempt to control a monitor with no Id');
|
|
return;
|
|
}
|
|
if ((!defined('ZM_SERVER_ID')) or ( property_exists($this, 'ServerId') and (ZM_SERVER_ID==$this->{'ServerId'}) )) {
|
|
if ($this->Type() == 'Local') {
|
|
$zmcArgs = '-d '.$this->{'Device'};
|
|
} else {
|
|
$zmcArgs = '-m '.$this->{'Id'};
|
|
}
|
|
|
|
if ($mode == 'stop') {
|
|
daemonControl('stop', 'zmc', $zmcArgs);
|
|
} else {
|
|
if ($mode == 'restart') {
|
|
daemonControl('stop', 'zmc', $zmcArgs);
|
|
}
|
|
if ($this->{'Capturing'} != 'None') {
|
|
daemonControl('start', 'zmc', $zmcArgs);
|
|
}
|
|
}
|
|
} else if ($this->ServerId()) {
|
|
$Server = $this->Server();
|
|
|
|
$url = $Server->UrlToApi().'/monitors/daemonControl/'.$this->{'Id'}.'/'.$mode.'/zmc.json';
|
|
if (ZM_OPT_USE_AUTH) {
|
|
if (ZM_AUTH_RELAY == 'hashed') {
|
|
$url .= '?auth='.generateAuthHash(ZM_AUTH_HASH_IPS);
|
|
} else if (ZM_AUTH_RELAY == 'plain') {
|
|
$url .= '?user='.$_SESSION['username'];
|
|
$url .= '?pass='.$_SESSION['password'];
|
|
} else {
|
|
Error('Multi-Server requires AUTH_RELAY be either HASH or PLAIN');
|
|
return;
|
|
}
|
|
}
|
|
Debug('sending command to '.$url);
|
|
|
|
$context = stream_context_create();
|
|
try {
|
|
$result = file_get_contents($url, false, $context);
|
|
if ($result === FALSE) { /* Handle error */
|
|
Error("Error restarting zmc using $url");
|
|
}
|
|
} catch (Exception $e) {
|
|
Error("Except $e thrown trying to restart zmc");
|
|
}
|
|
} else {
|
|
Error('Server not assigned to Monitor in a multi-server setup. Please assign a server to the Monitor.');
|
|
}
|
|
} // end function zmcControl
|
|
|
|
public function GroupIds($new='') {
|
|
if ($new != '') {
|
|
if (!is_array($new)) {
|
|
$this->{'GroupIds'} = array($new);
|
|
} else {
|
|
$this->{'GroupIds'} = $new;
|
|
}
|
|
}
|
|
|
|
if (!property_exists($this, 'GroupIds')) {
|
|
if (property_exists($this, 'Id') and $this->{'Id'}) {
|
|
$this->{'GroupIds'} = dbFetchAll('SELECT `GroupId` FROM `Groups_Monitors` WHERE `MonitorId`=?', 'GroupId', array($this->{'Id'}));
|
|
if (!$this->{'GroupIds'})
|
|
$this->{'GroupIds'} = array();
|
|
} else {
|
|
$this->{'GroupIds'} = array();
|
|
}
|
|
}
|
|
return $this->{'GroupIds'};
|
|
}
|
|
|
|
public function delete() {
|
|
if (!$this->{'Id'}) {
|
|
Warning('Attempt to delete a monitor without id.');
|
|
return;
|
|
}
|
|
$this->zmcControl('stop');
|
|
|
|
// If fast deletes are on, then zmaudit will clean everything else up later
|
|
// If fast deletes are off and there are lots of events then this step may
|
|
// well time out before completing, in which case zmaudit will still tidy up
|
|
if (!ZM_OPT_FAST_DELETE) {
|
|
$markEids = dbFetchAll('SELECT Id FROM Events WHERE MonitorId=?', 'Id', array($this->{'Id'}));
|
|
foreach ($markEids as $markEid)
|
|
deleteEvent($markEid);
|
|
|
|
if ($this->{'Name'})
|
|
deletePath(ZM_DIR_EVENTS.'/'.basename($this->{'Name'}));
|
|
deletePath(ZM_DIR_EVENTS.'/'.$this->{'Id'});
|
|
$Storage = $this->Storage();
|
|
if ($Storage->Path() != ZM_DIR_EVENTS) {
|
|
if ($this->{'Name'})
|
|
deletePath($Storage->Path().'/'.basename($this->{'Name'}));
|
|
deletePath($Storage->Path().'/'.$this->{'Id'});
|
|
}
|
|
} // end if !ZM_OPT_FAST_DELETE
|
|
|
|
// This is the important stuff
|
|
dbQuery('DELETE FROM Zones WHERE MonitorId = ?', array($this->{'Id'}));
|
|
if ( ZM_OPT_X10 )
|
|
dbQuery('DELETE FROM TriggersX10 WHERE MonitorId=?', array($this->{'Id'}));
|
|
dbQuery('DELETE FROM Monitor_Status WHERE MonitorId = ?', array($this->{'Id'}));
|
|
dbQuery('DELETE FROM Event_Summaries WHERE MonitorId = ?', array($this->{'Id'}));
|
|
dbQuery('DELETE FROM Monitors WHERE Id = ?', array($this->{'Id'}));
|
|
} // end function delete
|
|
|
|
public function Storage($new = null) {
|
|
if ($new) {
|
|
$this->{'Storage'} = $new;
|
|
}
|
|
if (!(property_exists($this, 'Storage') and $this->{'Storage'})) {
|
|
$this->{'Storage'} = isset($this->{'StorageId'}) ?
|
|
Storage::find_one(array('Id'=>$this->{'StorageId'})) :
|
|
new Storage(NULL);
|
|
if (!$this->{'Storage'})
|
|
$this->{'Storage'} = new Storage(NULL);
|
|
}
|
|
return $this->{'Storage'};
|
|
}
|
|
|
|
public function Source( ) {
|
|
$source = '';
|
|
if ($this->{'Type'} == 'Local') {
|
|
$source = $this->{'Device'}.' ('.$this->{'Channel'}.')';
|
|
} else if ($this->{'Type'} == 'Remote') {
|
|
$source = preg_replace('/^.*@/', '', $this->{'Host'});
|
|
if ($this->{'Port'} != '80' and $this->{'Port'} != '554') {
|
|
$source .= ':'.$this->{'Port'};
|
|
}
|
|
} else if ($this->{'Type'} == 'VNC') {
|
|
$source = preg_replace( '/^.*@/', '', $this->{'Host'} );
|
|
if ($this->{'Port'} != '5900') {
|
|
$source .= ':'.$this->{'Port'};
|
|
}
|
|
} else if ($this->{'Type'} == 'Ffmpeg' || $this->{'Type'} == 'Libvlc' || $this->{'Type'} == 'WebSite') {
|
|
$url_parts = parse_url($this->{'Path'});
|
|
if (ZM_WEB_FILTER_SOURCE == 'Hostname') {
|
|
# Filter out everything but the hostname
|
|
if (isset($url_parts['host'])) {
|
|
$source = $url_parts['host'];
|
|
} else {
|
|
$source = $this->{'Path'};
|
|
}
|
|
} else if (ZM_WEB_FILTER_SOURCE == 'NoCredentials') {
|
|
# Filter out sensitive and common items
|
|
unset($url_parts['user']);
|
|
unset($url_parts['pass']);
|
|
#unset($url_parts['scheme']);
|
|
unset($url_parts['query']);
|
|
#unset($url_parts['path']);
|
|
if (isset($url_parts['port']) and ($url_parts['port'] == '80' or $url_parts['port'] == '554'))
|
|
unset($url_parts['port']);
|
|
$source = unparse_url($url_parts);
|
|
} else { # Don't filter anything
|
|
$source = $this->{'Path'};
|
|
}
|
|
}
|
|
if ($source == '') {
|
|
$source = 'Monitor ' . $this->{'Id'};
|
|
}
|
|
return $source;
|
|
} // end function Source
|
|
|
|
public function UrlToIndex($port=null) {
|
|
return $this->Server()->UrlToIndex($port);
|
|
}
|
|
|
|
public function UrlToZMS($port=null) {
|
|
return $this->Server()->UrlToZMS($port).'?mid='.$this->Id();
|
|
}
|
|
|
|
public function sendControlCommand($command) {
|
|
// command is generally a command option list like --command=blah but might be just the word quit
|
|
|
|
$options = array();
|
|
# Convert from a command line params to an option array
|
|
foreach (explode(' ', $command) as $option) {
|
|
if (preg_match('/--([^=]+)(?:=(.+))?/', $option, $matches)) {
|
|
$options[$matches[1]] = $matches[2]?$matches[2]:1;
|
|
} else if ($option != '' and $option != 'quit' and $option != 'start' and $option != 'stop') {
|
|
Warning("Ignored command for zmcontrol $option in $command");
|
|
}
|
|
}
|
|
if (!count($options)) {
|
|
if ($command == 'quit' or $command == 'start' or $command == 'stop') {
|
|
# These are special as we now run zmcontrol as a daemon through zmdc.
|
|
$status = daemonStatus('zmcontrol.pl', array('--id', $this->{'Id'}));
|
|
Debug("Current status $status");
|
|
if ($status or ((!defined('ZM_SERVER_ID')) or (property_exists($this, 'ServerId') and (ZM_SERVER_ID==$this->{'ServerId'})))) {
|
|
daemonControl($command, 'zmcontrol.pl', '--id '.$this->{'Id'});
|
|
return;
|
|
}
|
|
$options['command'] = $command;
|
|
} else {
|
|
Warning('No commands to send to zmcontrol from '.$command);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ((!defined('ZM_SERVER_ID')) or (property_exists($this, 'ServerId') and (ZM_SERVER_ID==$this->{'ServerId'}))) {
|
|
# Local
|
|
Debug('Trying to send options ' . print_r($options, true));
|
|
|
|
$optionString = jsonEncode($options);
|
|
Debug('Trying to send options '.$optionString);
|
|
// Either connects to running zmcontrol.pl or runs zmcontrol.pl to send the command.
|
|
$socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
|
|
if ($socket < 0) {
|
|
Error('socket_create() failed: '.socket_strerror($socket));
|
|
return false;
|
|
}
|
|
$sockFile = ZM_PATH_SOCKS.'/zmcontrol-'.$this->{'Id'}.'.sock';
|
|
if (@socket_connect($socket, $sockFile)) {
|
|
if (!socket_write($socket, $optionString)) {
|
|
Error('Can\'t write to control socket: '.socket_strerror(socket_last_error($socket)));
|
|
return false;
|
|
}
|
|
} else if ($command != 'quit') {
|
|
$command = ZM_PATH_BIN.'/zmcontrol.pl '.$command.' --id '.$this->{'Id'};
|
|
|
|
// Can't connect so use script
|
|
$ctrlOutput = exec(escapeshellcmd($command));
|
|
}
|
|
socket_close($socket);
|
|
} else if ($this->ServerId()) {
|
|
$Server = $this->Server();
|
|
|
|
$url = $Server->UrlToApi().'/monitors/daemonControl/'.$this->{'Id'}.'/'.$command.'/zmcontrol.pl.json';
|
|
if (ZM_OPT_USE_AUTH) {
|
|
if (ZM_AUTH_RELAY == 'hashed') {
|
|
$url .= '?auth='.generateAuthHash(ZM_AUTH_HASH_IPS);
|
|
} else if (ZM_AUTH_RELAY == 'plain') {
|
|
$url .= '?user='.$_SESSION['username'];
|
|
$url .= '?pass='.$_SESSION['password'];
|
|
} else if (ZM_AUTH_RELAY == 'none') {
|
|
$url .= '?user='.$_SESSION['username'];
|
|
}
|
|
}
|
|
Debug('sending command to '.$url);
|
|
|
|
$context = stream_context_create();
|
|
try {
|
|
$result = file_get_contents($url, false, $context);
|
|
if ($result === FALSE) { /* Handle error */
|
|
Error("Error sending command using $url");
|
|
return false;
|
|
}
|
|
} catch (Exception $e) {
|
|
Error("Exception $e thrown trying to send command to $url");
|
|
return false;
|
|
}
|
|
} else {
|
|
Error('Server not assigned to Monitor in a multi-server setup. Please assign a server to the Monitor.');
|
|
return false;
|
|
} // end if we are on the recording server
|
|
return true;
|
|
} // end function sendControlCommand($mid, $command)
|
|
|
|
function Groups($new='') {
|
|
if ($new != '')
|
|
$this->Groups = $new;
|
|
if (!property_exists($this, 'Groups')) {
|
|
$this->Groups = Group::find(array('Id'=>$this->GroupIds()));
|
|
}
|
|
return $this->Groups;
|
|
}
|
|
function connKey($new='') {
|
|
if ($new)
|
|
$this->connKey = $new;
|
|
if (!isset($this->connKey)) {
|
|
if (!empty($GLOBALS['connkey'])) {
|
|
$this->connKey = $GLOBALS['connkey'];
|
|
} else {
|
|
$this->connKey = generateConnKey();
|
|
}
|
|
}
|
|
return $this->connKey;
|
|
}
|
|
|
|
function canEdit() {
|
|
global $user;
|
|
return ( $user && ($user['Monitors'] == 'Edit') && ( !$this->{'Id'} || visibleMonitor($this->{'Id'}) ));
|
|
}
|
|
|
|
function canView() {
|
|
global $user;
|
|
if (!$user) {
|
|
# auth turned on and not logged in
|
|
return false;
|
|
}
|
|
if (!empty($user['MonitorIds']) ) {
|
|
# For the purposes of viewing, having specified monitors trumps the Monitor->canView setting.
|
|
if (in_array($this->{'Id'}, explode(',', $user['MonitorIds']))) {
|
|
return true;
|
|
}
|
|
}
|
|
return ($user['Monitors'] != 'None');
|
|
}
|
|
|
|
function AlarmCommand($cmd) {
|
|
if ((!defined('ZM_SERVER_ID')) or (property_exists($this, 'ServerId') and (ZM_SERVER_ID==$this->{'ServerId'}))) {
|
|
switch ($cmd) {
|
|
case 'on' : $cmd = ' -a'; break;
|
|
case 'off': $cmd = ' -c'; break;
|
|
case 'disable': $cmd = ' -n'; break;
|
|
case 'status': $cmd = ' -s'; break;
|
|
default:
|
|
Warning("Invalid command $cmd in AlarmCommand");
|
|
return false;
|
|
}
|
|
|
|
$cmd = getZmuCommand($cmd.' -m '.$this->{'Id'});
|
|
$output = shell_exec($cmd);
|
|
Debug("Running $cmd output: $output");
|
|
return $output;
|
|
}
|
|
|
|
if ($this->ServerId()) {
|
|
$Server = $this->Server();
|
|
|
|
$url = $Server->UrlToApi().'/monitors/alarm/id:'.$this->{'Id'}.'/command:'.$cmd.'.json';
|
|
$auth_relay = get_auth_relay();
|
|
if ($auth_relay) $url .= '?'.$auth_relay;
|
|
|
|
Debug('sending command to '.$url);
|
|
|
|
$context = stream_context_create();
|
|
try {
|
|
$result = file_get_contents($url, false, $context);
|
|
if ($result === FALSE) { /* Handle error */
|
|
Error('Error sending command using '.$url);
|
|
return false;
|
|
}
|
|
Debug('Result '.$result);
|
|
$json = json_decode($result, true);
|
|
return $json['status'];
|
|
|
|
} catch (Exception $e) {
|
|
Error("Exception $e thrown trying to send command to $url");
|
|
return false;
|
|
}
|
|
} // end if we are on the recording server
|
|
Error('Server not assigned to Monitor in a multi-server setup. Please assign a server to the Monitor.');
|
|
return false;
|
|
}
|
|
function TriggerOn() {
|
|
$output = $this->AlarmCommand('on');
|
|
if ($output and preg_match('/Alarmed event id: (\d+)$/', $output, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
Warning('No event returned from TriggerOn');
|
|
}
|
|
function TriggerOff() {
|
|
$output = $this->AlarmCommand('off');
|
|
}
|
|
function DisableAlarms() {
|
|
$output = $this->AlarmCommand('disable');
|
|
}
|
|
function Model() {
|
|
if (!property_exists($this, 'Model')) {
|
|
if (property_exists($this, 'ModelId') and $this->{'ModelId'}) {
|
|
$this->{'Model'} = Model::find_one(array('Id'=>$this->ModelId()));
|
|
if (!$this->{'Model'})
|
|
$this->{'Model'} = new Model();
|
|
} else {
|
|
$this->{'Model'} = new Model();
|
|
}
|
|
}
|
|
return $this->{'Model'};
|
|
}
|
|
function Manufacturer() {
|
|
if (!property_exists($this, 'Manufacturer')) {
|
|
if (property_exists($this, 'ManufacturerId') and $this->{'ManufacturerId'}) {
|
|
$this->{'Manufacturer'} = Manufacturer::find_one(array('Id'=>$this->ManufacturerId()));
|
|
if (!$this->{'Manufacturer'})
|
|
$this->{'Manufacturer'} = new Manufacturer();
|
|
} else {
|
|
$this->{'Manufacturer'} = new Manufacturer();
|
|
}
|
|
}
|
|
return $this->{'Manufacturer'};
|
|
}
|
|
function getMonitorStateHTML() {
|
|
$html = '
|
|
<div id="monitorStatus'.$this->Id().'" class="monitorStatus">
|
|
<div id="monitorState'.$this->Id().'" class="monitorState">
|
|
<span>'.translate('State').':<span id="stateValue'.$this->Id().'"></span></span>
|
|
<span id="viewingFPS'.$this->Id().'" title="'.translate('Viewing FPS').'"><span id="viewingFPSValue'.$this->Id().'"></span> fps</span>
|
|
<span id="captureFPS'.$this->Id().'" title="'.translate('Capturing FPS').'"><span id="captureFPSValue'.$this->Id().'"></span> fps</span>
|
|
';
|
|
if ($this->Analysing() != 'None') {
|
|
$html .= '<span id="analysisFPS'.$this->Id().'" title="'.translate('Analysis FPS').'"><span id="analysisFPSValue'.$this->Id().'"></span> fps</span>
|
|
';
|
|
}
|
|
$html .= '
|
|
<span id="rate'.$this->Id().'" class="rate hidden">'.translate('Rate').': <span id="rateValue'.$this->Id().'"></span>x</span>
|
|
<span id="delay'.$this->Id().'" class="delay hidden">'.translate('Delay').': <span id="delayValue'.$this->Id().'"></span>s</span>
|
|
<span id="level'.$this->Id().'" class="buffer hidden">'.translate('Buffer').': <span id="levelValue'.$this->Id().'"></span>%</span>
|
|
<span class="zoom hidden" id="zoom'.$this->Id().'">'. translate('Zoom').': <span id="zoomValue'.$this->Id().'"></span>x</span>
|
|
</div>
|
|
</div>
|
|
';
|
|
return $html;
|
|
}
|
|
|
|
/* options['width'] is the desired view width not necessarily the image width requested.
|
|
* It can be % in which case we us it to set the scale
|
|
* It can be px in which case we can use it to calculate the scale
|
|
* Same width height. If both are set we should calculate the smaller resulting scale
|
|
*/
|
|
function getStreamHTML($options) {
|
|
if (isset($options['scale']) and $options['scale'] != '' and $options['scale'] != 'fixed') {
|
|
Debug("Have scale:" . $options['scale']);
|
|
if ( $options['scale'] != 'auto' && $options['scale'] != '0' ) {
|
|
#ZM\Warning('Setting dimensions from scale:'.$options['scale']);
|
|
$options['width'] = reScale($this->ViewWidth(), $options['scale']).'px';
|
|
$options['height'] = reScale($this->ViewHeight(), $options['scale']).'px';
|
|
} else if ( ! ( isset($options['width']) or isset($options['height']) ) ) {
|
|
$options['width'] = '100%';
|
|
$options['height'] = 'auto';
|
|
}
|
|
} else {
|
|
$options['scale'] = 100;
|
|
# scale is empty or 100
|
|
# There may be a fixed width applied though, in which case we need to leave the height empty
|
|
if ( ! ( isset($options['width']) and $options['width'] ) ) {
|
|
# Havn't specified width. If we specified height, then we should
|
|
# use a width that keeps the aspect ratio, otherwise no scaling,
|
|
# no dimensions, so assume the dimensions of the Monitor
|
|
|
|
if ( ! (isset($options['height']) and $options['height']) ) {
|
|
# If we havn't specified any scale or dimensions, then we must be using CSS to scale it in a dynamic way. Can't make any assumptions.
|
|
#$options['width'] = $monitor->ViewWidth().'px';
|
|
#$options['height'] = $monitor->ViewHeight().'px';
|
|
}
|
|
} else {
|
|
#ZM\Warning("Have width ".$options['width']);
|
|
if ( preg_match('/^(\d+)px$/', $options['width'], $matches) ) {
|
|
$scale = intval(100*$matches[1]/$this->ViewWidth());
|
|
#ZM\Warning("Scale is $scale");
|
|
if ( $scale < $options['scale'] )
|
|
$options['scale'] = $scale;
|
|
} else if ( preg_match('/^(\d+)%$/', $options['width'], $matches) ) {
|
|
$scale = intval($matches[1]);
|
|
if ( $scale < $options['scale'] )
|
|
$options['scale'] = $scale;
|
|
} else {
|
|
Warning('Invalid value for width: '.$options['width']);
|
|
}
|
|
}
|
|
}
|
|
if ( ! isset($options['mode'] ) ) {
|
|
$options['mode'] = 'stream';
|
|
}
|
|
if ( ! isset($options['width'] ) )
|
|
$options['width'] = 0;
|
|
if ( ! isset($options['height'] ) )
|
|
$options['height'] = 0;
|
|
|
|
if (!isset($options['maxfps'])) {
|
|
$options['maxfps'] = ZM_WEB_VIDEO_MAXFPS;
|
|
}
|
|
if ($this->StreamReplayBuffer())
|
|
$options['buffer'] = $this->StreamReplayBuffer();
|
|
//Warning("width: " . $options['width'] . ' height: ' . $options['height']. ' scale: ' . $options['scale'] );
|
|
$html = '
|
|
<div id="monitor'. $this->Id() . '" class="monitor">
|
|
<div
|
|
id="imageFeed'. $this->Id() .'"
|
|
class="monitorStream imageFeed"
|
|
data-monitor-id="'. $this->Id() .'"
|
|
data-width="'. $this->ViewWidth() .'"
|
|
data-height="'.$this->ViewHeight() .'" style="'.
|
|
#(($options['width'] and ($options['width'] != '0px')) ? 'width: '.$options['width'].';' : '').
|
|
#(($options['height'] and ($options['height'] != '0px')) ? 'height: '.$options['height'].';' : '').
|
|
'">';
|
|
|
|
if ($this->Type() == 'WebSite') {
|
|
$html .= getWebSiteUrl(
|
|
'liveStream'.$this->Id(), $this->Path(),
|
|
( isset($options['width']) ? $options['width'] : NULL ),
|
|
( isset($options['height']) ? $options['height'] : NULL ),
|
|
$this->Name()
|
|
);
|
|
//FIXME, the width and height of the image need to be scaled.
|
|
} else if ((ZM_WEB_STREAM_METHOD == 'mpeg') && ZM_MPEG_LIVE_FORMAT) {
|
|
$streamSrc = $this->getStreamSrc( array(
|
|
'mode' => 'mpeg',
|
|
'scale' => (isset($options['scale'])?$options['scale']:100),
|
|
'bitrate'=> ZM_WEB_VIDEO_BITRATE,
|
|
'maxfps' => ZM_WEB_VIDEO_MAXFPS,
|
|
'format' => ZM_MPEG_LIVE_FORMAT
|
|
) );
|
|
$html .= getVideoStreamHTML( 'liveStream'.$this->Id(), $streamSrc, $options['width'], $options['height'], ZM_MPEG_LIVE_FORMAT, $this->Name() );
|
|
} else if ( $this->JanusEnabled() ) {
|
|
$html .= '<video id="liveStream'.$this->Id().'" '.
|
|
((isset($options['width']) and $options['width'] and $options['width'] != '0')?'width="'.$options['width'].'"':'').
|
|
' autoplay muted controls playsinline=""></video>';
|
|
} else if ( $options['mode'] == 'stream' and canStream() ) {
|
|
$options['mode'] = 'jpeg';
|
|
$streamSrc = $this->getStreamSrc($options);
|
|
$html .= getImageStreamHTML('liveStream'.$this->Id(), $streamSrc, $options['width'], $options['height'], $this->Name());
|
|
} else if ( $options['mode'] == 'single' and canStream() ) {
|
|
$streamSrc = $this->getStreamSrc($options);
|
|
$html .= getImageStreamHTML('liveStream'.$this->Id(), $streamSrc, $options['width'], $options['height'], $this->Name());
|
|
} else {
|
|
if ($options['mode'] == 'stream') {
|
|
Info('The system has fallen back to single jpeg mode for streaming. Consider enabling Cambozola or upgrading the client browser.');
|
|
}
|
|
$options['mode'] = 'single';
|
|
$streamSrc = $this->getStreamSrc($options);
|
|
$html .= getImageStill('liveStream'.$this->Id(), $streamSrc,
|
|
(isset($options['width']) ? $options['width'] : null),
|
|
(isset($options['height']) ? $options['height'] : null),
|
|
$this->Name());
|
|
}
|
|
|
|
if (isset($options['zones']) and $options['zones']) {
|
|
$html .= '<svg class="zones" id="zones'.$this->Id().'" viewBox="0 0 '.$this->ViewWidth().' '.$this->ViewHeight() .'" preserveAspectRatio="none">'.PHP_EOL;
|
|
foreach (Zone::find(array('MonitorId'=>$this->Id()), array('order'=>'Area DESC')) as $zone) {
|
|
$html .= $zone->svg_polygon();
|
|
} // end foreach zone
|
|
$html .= '
|
|
Sorry, your browser does not support inline SVG
|
|
</svg>
|
|
';
|
|
} # end if showZones
|
|
$html .= PHP_EOL.'</div><!--monitorStream-->'.PHP_EOL;
|
|
if (isset($options['state']) and $options['state']) {
|
|
//if ((!ZM_WEB_COMPACT_MONTAGE) && ($this->Type() != 'WebSite')) {
|
|
$html .= $this->getMonitorStateHTML();
|
|
}
|
|
$html .= PHP_EOL.'</div>'.PHP_EOL;
|
|
return $html;
|
|
} // end getStreamHTML
|
|
} // end class Monitor
|
|
?>
|