mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-08-03 08:46:44 -04:00
A capture crash can leave events with no frames, no Length and no EndDateTime. The API's EndTimeSecs virtual field reported these with an end of NOW(), the fallback meant for a genuinely in-progress recording, so every orphan appeared to span from its start until now. On a monitor with hundreds of these, the events all overlapped: at a typical timestamp 313 events covered the same instant. findEventByTime is a binary search that assumes non-overlapping ordered events, so it returned an essentially arbitrary one, and zms then streamed a different event than the JS believed was playing. That is the erratic playback and event/stream mismatch seen in montage review. Two changes: - Event model EndTimeSecs: for an event with no EndDateTime and Length 0, report end = StartDateTime (zero duration) instead of NOW(). An event with no length has no span to occupy, so it no longer overlaps others. - montagereview receive_events: skip events with no frames. There is nothing to review in an empty event, and dropping them keeps the timeline and event selection to real recordings. The same NOW() fallback exists in web/ajax/events.php and web/skins/classic/views/montagereview.php; those paths are not used by montage review's event load and are left unchanged. Verified on a monitor with 521 orphaned events: overlapping events at a given instant dropped from 313 to 2, and the streamed event now matches the selected event with sub-second drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
192 lines
5.4 KiB
PHP
192 lines
5.4 KiB
PHP
<?php
|
|
require_once __DIR__ .'/../../../includes/Event.php';
|
|
|
|
App::uses('AppModel', 'Model');
|
|
/**
|
|
* Event Model
|
|
*
|
|
* @property Monitor $Monitor
|
|
* @property Frame $Frame
|
|
*/
|
|
class Event extends AppModel {
|
|
|
|
/**
|
|
* Use table
|
|
*
|
|
* @var mixed False or table name
|
|
*/
|
|
public $useTable = 'Events';
|
|
|
|
/**
|
|
* Primary key field
|
|
*
|
|
* @var string
|
|
*/
|
|
public $primaryKey = 'Id';
|
|
|
|
/**
|
|
* Display field
|
|
*
|
|
* @var string
|
|
*/
|
|
public $displayField = 'Name';
|
|
|
|
// For events that never wrote EndDateTime (zmc killed/crashed mid-event),
|
|
// fall back to StartDateTime + Length (Length is flushed to the DB every few
|
|
// seconds during recording, so it reflects the actual recorded duration).
|
|
// Only fall back to NOW() if Length is also 0 (event has no recorded data
|
|
// yet, e.g. just started). This prevents montagereview and other consumers
|
|
// from painting an event bar across hours/days of no real recording.
|
|
public $virtualFields = array(
|
|
'StartTimeSecs' => 'UNIX_TIMESTAMP(StartDateTime)',
|
|
'EndTimeSecs' => '(CASE WHEN Event.EndDateTime IS NOT NULL THEN UNIX_TIMESTAMP(Event.EndDateTime) WHEN Event.Length > 0 THEN UNIX_TIMESTAMP(Event.StartDateTime) + Event.Length ELSE UNIX_TIMESTAMP(Event.StartDateTime) END)',
|
|
'StartTime' => 'StartDateTime',
|
|
'EndTime' => '(CASE WHEN Event.EndDateTime IS NOT NULL THEN Event.EndDateTime WHEN Event.Length > 0 THEN DATE_ADD(Event.StartDateTime, INTERVAL FLOOR(Event.Length) SECOND) ELSE NOW() END)'
|
|
);
|
|
|
|
//The Associations below have been created with all possible keys, those that are not needed can be removed
|
|
|
|
/**
|
|
* belongsTo associations
|
|
*
|
|
* @var array
|
|
*/
|
|
public $belongsTo = array(
|
|
'Monitor' => array(
|
|
'className' => 'Monitor',
|
|
'foreignKey' => 'MonitorId',
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => ''
|
|
),
|
|
'Storage' => array(
|
|
'className' => 'Storage',
|
|
'joinTable' => 'Storage',
|
|
'foreignKey' => 'StorageId',
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => ''
|
|
)
|
|
);
|
|
|
|
/**
|
|
* hasMany associations
|
|
*
|
|
* @var array
|
|
*/
|
|
public $hasMany = array(
|
|
'Frame' => array(
|
|
'className' => 'Frame',
|
|
'foreignKey' => 'EventId',
|
|
'dependent' => true,
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => '',
|
|
'limit' => '',
|
|
'offset' => '',
|
|
'exclusive' => 'true',
|
|
'finderQuery' => '',
|
|
'counterQuery' => ''
|
|
)
|
|
);
|
|
|
|
/**
|
|
* * * hasMany associations
|
|
* * *
|
|
* * * @var array
|
|
* * */
|
|
public $hasAndBelongsToMany = array(
|
|
'Group' => array(
|
|
'className' => 'Group',
|
|
'joinTable' => 'Groups_Monitors',
|
|
'foreignKey' => 'MonitorId',
|
|
'associationForeignKey' => 'MonitorId',
|
|
'unique' => true,
|
|
'dependent' => false,
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => '',
|
|
'limit' => '',
|
|
'offset' => '',
|
|
'exclusive' => '',
|
|
'finderQuery' => '',
|
|
'counterQuery' => ''
|
|
),
|
|
'Tag' => array(
|
|
'className' => 'Tag',
|
|
'joinTable' => 'Events_Tags',
|
|
'foreignKey' => 'EventId',
|
|
'associationForeignKey' => 'TagId',
|
|
'unique' => true,
|
|
'dependent' => false,
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => '',
|
|
'limit' => '',
|
|
'offset' => '',
|
|
'exclusive' => '',
|
|
'finderQuery' => '',
|
|
'counterQuery' => ''
|
|
),
|
|
);
|
|
|
|
public $actsAs = array(
|
|
'Containable',
|
|
'CakePHP-Enum-Behavior.Enum' => array(
|
|
'Orientation' => array('ROTATE_0','ROTATE_90','ROTATE_180','ROTATE_270','FLIP_HORI','FLIP_VERT'),
|
|
'Scheme' => array('Deep','Medium','Shallow')
|
|
)
|
|
);
|
|
|
|
public function Relative_Path() {
|
|
$Event = ZM\Event::find_one(['Id'=>$this->id]);
|
|
return $Event ? $Event->Relative_Path() : '';
|
|
} // end function Relative_Path()
|
|
|
|
public function Path() {
|
|
$Event = ZM\Event::find_one(['Id'=>$this->id]);
|
|
return $Event ? $Event->Path() : '';
|
|
}
|
|
|
|
public function Link_Path() {
|
|
$Event = ZM\Event::find_one(['Id'=>$this->id]);
|
|
return $Event ? $Event->Link_Path() : '';
|
|
}
|
|
|
|
public function fileExists($event) {
|
|
if ($event['DefaultVideo']) {
|
|
if (file_exists($this->Path().'/'.$event['DefaultVideo'])) {
|
|
return 1;
|
|
}
|
|
|
|
// While an event is recording its DefaultVideo is incomplete.mp4. When
|
|
// the event closes the file is renamed to <Id>-video.* and the DB row is
|
|
// updated. If we still see incomplete.mp4 the model is likely stale, so
|
|
// reload the event from the database and re-check the new DefaultVideo.
|
|
if (preg_match('/^incomplete\./', basename($event['DefaultVideo']))) {
|
|
ZM\Event::clear_cache();
|
|
$Event = ZM\Event::find_one(array('Id'=>$this->id));
|
|
if ($Event and $Event->DefaultVideo() and $Event->DefaultVideo() != $event['DefaultVideo']
|
|
and file_exists($Event->Path().'/'.$Event->DefaultVideo())) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
ZM\Warning('File does not exist at ' . $this->Path().'/'.$event['DefaultVideo'] );
|
|
} else {
|
|
return 0;
|
|
}
|
|
} // end function fileExists($event)
|
|
|
|
public function fileSize($event) {
|
|
return filesize($this->Path().'/'.$event['DefaultVideo']);
|
|
}
|
|
|
|
public function beforeDelete($cascade=true) {
|
|
$Event = ZM\Event::find_one(['Id'=>$this->id]);
|
|
if ($Event) $Event->delete();
|
|
// Event->delete() will do it all, so cake doesn't have to do anything.
|
|
return false;
|
|
} // end function afterDelete
|
|
}
|