Files
zoneminder/web/ajax/status.php
T
Isaac ConnorandClaude Opus 5 33661907d1 feat: persist the peak audio level on each frame row
zm_update-1.39.31.sql gave monitors audio detection, but the level only ever
existed in shared memory, so it was gone the moment the frame passed and there
was nothing for the event view to plot. Add Frames.AudioLevel next to Score, on
the same 0-100 dBFS-derived scale the threshold uses.

What is stored is the peak since the previous row, not the level at the instant
the row was written. Frames rows are written well below the capture rate --
only alarm, bulk and score-increasing frames get one -- so sampling at write
time would drop exactly the short loud noises worth seeing on a timeline.
AudioDetector accumulates the peak as it decodes and Event::AddFrame takes it
where the row is built, which clears it so each row covers its own interval.
The Event constructor takes and discards it once, otherwise an event's first
row reports the loudest moment since the previous event ended.

This needs no shared memory change: zma is now an offline re-analysis tool and
the live analysis runs in a thread of zmc, alongside the capture thread that
runs the decoder, so the peak can stay in the AudioDetector. SharedData keeps
its documented 888-byte layout and its fixed offsets.

The frames ajax returns the column, and Score with it. elements in
web/ajax/status.php is a whitelist that never listed Score, which is why the
event view's cue strip has been reading an undefined Score off every frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpiSWBmtQkR5bcgpHWY4ME
2026-09-20 18:17:43 -05:00

618 lines
22 KiB
PHP

<?php
if (!isset($_REQUEST['entity'])) {
Error("No entity pass to status request.");
http_response_code(404);
return;
} else {
}
if ($_REQUEST['entity'] == 'navBar') {
global $bandwidth_options, $user;
$data = array();
if ( ZM_OPT_USE_AUTH && (ZM_AUTH_RELAY == 'hashed') ) {
$auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS);
$data['auth'] = $auth_hash;
$data['auth_relay'] = get_auth_relay();
}
// Each widget on the navbar has its own function
// Call the functions we want to dynamically update
$data['getBandwidthHTML'] = getBandwidthHTML($bandwidth_options, $user);
$data['getSysLoadHTML'] = getSysLoadHTML();
$data['getCpuUsageHTML'] = getCpuUsageHTML();
$data['getDbConHTML'] = getDbConHTML();
$data['getStorageHTML'] = getStorageHTML();
//$data['getShmHTML'] = getShmHTML();
$data['getRamHTML'] = getRamHTML();
$data['getLogStatusHTML'] = logState();
ajaxResponse($data);
return;
}
$statusData = array(
'system' => array(
'permission' => 'System',
'table' => 'Monitors',
'limit' => 1,
'elements' => array(
'MonitorCount' => array( 'sql' => 'count(*)' ),
'ActiveMonitorCount' => array( 'sql' => 'count(if(`Capturing` != \'None\',1,NULL))' ),
'State' => array( 'func' => function() { return daemonCheck() ? translate('Running') : translate('Stopped'); } ),
'Load' => array( 'func' => function() { return getLoad(); } ),
'Disk' => array( 'func' => function() { return getDiskPercent(); } ),
),
),
'monitor' => array(
#'permission' => 'Monitors',
'object' => 'Monitor',
'table' => 'Monitors',
'limit' => 1,
'selector' => 'Monitors.Id',
'elements' => array(
'Id' => array( 'sql' => 'Monitors.Id' ),
'Name' => array( 'sql' => 'Monitors.Name' ),
'Type' => true,
'Capturing' => true,
'Analysing' => true,
'Recording' => true,
'Enabled' => true,
'LinkedMonitors' => true,
'Triggers' => true,
'Device' => true,
'Channel' => true,
'Format' => true,
'Host' => true,
'Port' => true,
'Path' => true,
'Width' => array( 'sql' => 'Monitors.Width' ),
'Height' => array( 'sql' => 'Monitors.Height' ),
'Palette' => true,
'Orientation' => true,
'Brightness' => true,
'Contrast' => true,
'Hue' => true,
'Colour' => true,
'EventPrefix' => true,
'LabelFormat' => true,
'LabelX' => true,
'LabelY' => true,
'LabelSize' => true,
'ImageBufferCount' => true,
'WarmupCount' => true,
'PreEventCount' => true,
'PostEventCount' => true,
'AlarmFrameCount' => true,
'SectionLength' => true,
'MotionFrameSkip' => true,
'MaxFPS' => true,
'AlarmMaxFPS' => true,
'FPSReportInterval' => true,
'RefBlendPerc' => true,
'Controllable' => true,
'ControlId' => true,
'ControlDevice' => true,
'ControlAddress' => true,
'AutoStopTimeout' => true,
'TrackMotion' => true,
'TrackDelay' => true,
'ReturnLocation' => true,
'ReturnDelay' => true,
'DefaultView' => true,
'DefaultRate' => true,
'DefaultScale' => true,
'WebColour' => true,
'Sequence' => true,
'MinEventId' => array( 'sql' => '(SELECT min(Events.Id) FROM Events WHERE Events.MonitorId = Monitors.Id)' ),
'MaxEventId' => array( 'sql' => '(SELECT max(Events.Id) FROM Events WHERE Events.MonitorId = Monitors.Id)' ),
'TotalEvents' => array( 'sql' => '(SELECT count(Events.Id) FROM Events WHERE Events.MonitorId = Monitors.Id)' ),
'Status' => (isset($_REQUEST['id'])?array( 'zmu' => '-m '.escapeshellarg($_REQUEST['id']).' -s' ):null),
'FrameRate' => (isset($_REQUEST['id'])?array( 'zmu' => '-m '.escapeshellarg($_REQUEST['id']).' -f' ):null),
'CaptureFPS' => [ 'sql'=>'(SELECT `CaptureFPS` FROM Monitor_Status WHERE MonitorId=Monitors.Id)' ],
'AnalysisFPS' => [ 'sql'=>'(SELECT `AnalysisFPS` FROM Monitor_Status WHERE MonitorId=Monitors.Id)' ],
'CaptureBandwidth' => [ 'sql'=>'(SELECT `CaptureBandwidth` FROM Monitor_Status WHERE MonitorId=Monitors.Id)' ],
),
),
'events' => array(
'permission' => 'Events',
'table' => 'Events',
'selector' => 'Events.MonitorId',
'elements' => array(
'Id' => true,
'MonitorId' => true,
'Name' => true,
'Cause' => true,
'Notes' => true,
'StartDateTime' => true,
'StartDateTimeFormatted' => array('postFunction'=>function($row){
global $dateTimeFormatter;
return $dateTimeFormatter->format(strtotime($row['StartDateTime']));
}),
# Left for backwards compatibility. Remove in 1.37
'EndDateTime' => true,
'EndDateTimeFormatted' => array('postFunction'=>function($row){
global $dateTimeFormatter;
return $dateTimeFormatter->format(strtotime($row['EndDateTime']));
}),
'Width' => true,
'Height' => true,
'Length' => true,
'Frames' => true,
'AlarmFrames' => true,
'TotScore' => true,
'AvgScore' => true,
'MaxScore' => true,
),
),
'event' => array(
'permission' => 'Events',
'table' => 'Events',
'limit' => 1,
'selector' => 'Events.Id',
'elements' => array(
'Id' => array( 'sql' => 'Events.Id' ),
'MonitorId' => true,
'MonitorName' => array('sql' => '(SELECT Monitors.Name FROM Monitors WHERE Monitors.Id = Events.MonitorId)'),
'Name' => true,
'Cause' => true,
'DiskSpace' => true,
'Storage' => array('sql' => '(SELECT Storage.Name FROM Storage WHERE Storage.Id=Events.StorageId)'),
'StartDateTime' => true,
'StartDateTimeFormatted' => array('postFunction'=>function($row){
global $dateTimeFormatter;
return $dateTimeFormatter->format(strtotime($row['StartDateTime']));
}),
# Left for backwards compatibility. Remove in 1.37
'EndDateTime' => true,
'EndDateTimeFormatted' => array('postFunction'=>function($row){
global $dateTimeFormatter;
return $dateTimeFormatter->format(strtotime($row['EndDateTime']));
}),
'Width' => true,
'Height' => true,
'Length' => true,
'Frames' => true,
'DefaultVideo' => true,
'AlarmFrames' => true,
'TotScore' => true,
'AvgScore' => true,
'MaxScore' => true,
'Archived' => true,
'Videoed' => true,
'Uploaded' => true,
'Emailed' => true,
'Messaged' => true,
'Executed' => true,
'Notes' => true,
'MinFrameId' => array( 'sql' => '(SELECT min(Frames.FrameId) FROM Frames WHERE EventId=Events.Id)' ),
'MaxFrameId' => array( 'sql' => '(SELECT max(Frames.FrameId) FROM Frames WHERE Events.Id = Frames.EventId)' ),
'MinFrameDelta' => array( 'sql' => '(SELECT min(Frames.Delta) FROM Frames WHERE Events.Id = Frames.EventId)' ),
'MaxFrameDelta' => array( 'sql' => '(SELECT max(Frames.Delta) FROM Frames WHERE Events.Id = Frames.EventId)' ),
),
),
'frames' => array(
'permission' => 'Events',
'table' => 'Frames',
'selector' => 'EventId',
'elements' => array(
'EventId' => true,
'FrameId' => true,
'Type' => true,
'Delta' => true,
// elements is a whitelist, so the event view's cue graph was reading an
// undefined Score off every frame until these were added.
'Score' => true,
'AudioLevel' => true,
),
),
'frame' => array(
'permission' => 'Events',
'table' => 'Frames',
'limit' => 1,
'selector' => array( array( 'table' => 'Events', 'join' => 'Events.Id = Frames.EventId', 'selector'=>'Events.Id' ), 'Frames.FrameId' ),
'elements' => array(
//'Id' => array( 'sql' => 'Frames.FrameId' ),
'FrameId' => true,
'EventId' => true,
'Type' => true,
'TimeStamp' => true,
'TimeStampShort' => array( 'sql' => 'date_format( StartDateTime, \''.MYSQL_FMT_DATETIME_SHORT.'\' )' ),
'Delta' => true,
'Score' => true,
//'Image' => array( 'postFunc' => 'getFrameImage' ),
),
),
'frameimage' => array(
'permission' => 'Events',
'func' => 'getFrameImage'
),
'nearframe' => array(
'permission' => 'Events',
'func' => 'getNearFrame'
),
'nearevents' => array(
'permission' => 'Events',
'func' => 'getNearEvents'
)
);
function collectData() {
global $statusData;
$entity = strtolower(validJsStr($_REQUEST['entity']));
$entitySpec = &$statusData[$entity];
#print_r( $entitySpec );
if (isset($entitySpec['permission'])) {
if (!canView($entitySpec['permission'])) {
ajaxError('Unrecognised action or insufficient permissions for '.$entity.' permission: '.$entitySpec['permission']);
return;
}
}
if ( !empty($entitySpec['func']) ) {
return call_user_func($entitySpec['func']);
}
$data = array();
$postFuncs = array();
$postFunctions = array();
$fieldSql = array();
$joinSql = array();
$groupSql = array();
$values = array();
$elements = &$entitySpec['elements'];
$lc_elements = array_change_key_case($elements);
$id = false;
if ( isset($_REQUEST['id']) )
if ( !is_array($_REQUEST['id']) )
$id = array( validJsStr($_REQUEST['id']) );
else
$id = array_values($_REQUEST['id']);
# Early per-object permission check before processing elements.
# This must happen before the element loop because func/zmu elements
# execute immediately and would bypass the post-SQL canView() check.
if (isset($entitySpec['object']) && $id) {
$object_name = 'ZM\\'.$entitySpec['object'];
$object = new $object_name($id[0]);
if (!$object->Id()) {
ajaxError('Not found: '.$entity.' id '.$id[0]);
return;
}
if (!$object->canView()) {
ajaxError('Insufficient permissions for '.$entity.' id '.$id[0]);
return;
}
}
if ( !isset($_REQUEST['element']) )
$_REQUEST['element'] = array_keys($elements);
else if ( !is_array($_REQUEST['element']) )
$_REQUEST['element'] = array( validJsStr($_REQUEST['element']) );
if ( isset($entitySpec['selector']) ) {
if ( !is_array($entitySpec['selector']) )
$entitySpec['selector'] = array( $entitySpec['selector'] );
foreach( $entitySpec['selector'] as $selector )
if ( is_array( $selector ) && isset($selector['table']) && isset($selector['join']) )
$joinSql[] = 'left join '.$selector['table'].' on '.$selector['join'];
}
foreach ( $_REQUEST['element'] as $element ) {
if ( !($elementData = $lc_elements[strtolower($element)]) ) {
ajaxError('Bad '.validJsStr($_REQUEST['entity']).' element '.$element);
continue;
}
if (isset($elementData['func'])) {
$data[$element] = call_user_func($elementData['func']);
} else if ( isset($elementData['postFunc']) ) {
$postFuncs[$element] = $elementData['postFunc'];
} else if ( isset($elementData['postFunction']) ) {
$postFunctions[$element] = $elementData['postFunction'];
} else if ( isset($elementData['zmu']) ) {
$command = escapeshellcmd(getZmuCommand(' '.$elementData['zmu']));
$data[$element] = exec($command);
} else {
if ( isset($elementData['sql']) )
$fieldSql[] = $elementData['sql'].' as '.$element;
else
$fieldSql[] = '`'.$element.'`';
if ( isset($elementData['table']) && isset($elementData['join']) ) {
$joinSql[] = 'left join '.$elementData['table'].' on '.$elementData['join'];
}
if ( isset($elementData['group']) ) {
$groupSql[] = $elementData['group'];
}
}
} # end foreach element
if (isset($entitySpec['object'])) {
$fieldSql[] = 'Id';
}
if ( count($fieldSql) ) {
$sql = 'SELECT '.join(', ', $fieldSql).' FROM '.$entitySpec['table'];
if ( $joinSql )
$sql .= ' '.join(' ', array_unique($joinSql));
if ( $id && !empty($entitySpec['selector']) ) {
$index = 0;
$where = array();
foreach ( $entitySpec['selector'] as $selIndex => $selector ) {
$selectorParamName = ':selector' . $selIndex;
if ( is_array($selector) ) {
$where[] = $selector['selector'].' = '.$selectorParamName;
$values[$selectorParamName] = validInt($id[$index]);
} else {
$where[] = $selector.' = '.$selectorParamName;
$values[$selectorParamName] = validInt($id[$index]);
}
$index++;
}
$sql .= ' WHERE '.join(' AND ', $where);
}
if ( $groupSql )
$sql .= ' GROUP BY '.join(',', array_unique($groupSql));
if ( !empty($_REQUEST['sort']) ) {
$sort_fields = explode(',', $_REQUEST['sort']);
$order_clauses = array();
foreach ( $sort_fields as $sort_field ) {
if (!preg_match('/^`?(\w+)`?\s*(ASC|DESC)?( NULLS FIRST)?$/i', $sort_field, $matches)) {
ZM\Error('Sort field didn\'t match regexp '.$sort_field);
continue;
}
// Check that the field name is one we are actually selecting.
// $fieldSql entries may be bare (`Name`), backtick-wrapped (`Name`),
// or aliased (expression as Name), so match against all forms.
$field = $matches[1];
$found = false;
foreach ($fieldSql as $f) {
if ($f === $field || $f === '`'.$field.'`' || preg_match('/\bas\s+`?'.$field.'`?$/i', $f)) {
$found = true;
break;
}
}
if (!$found) {
ZM\Error('Sort field '.$field.' from '.$sort_field.' not in SQL Fields: '.join(',', $fieldSql));
continue;
}
$clause = '`'.$field.'`';
if (!empty($matches[2])) {
$clause .= ' '.strtoupper($matches[2]);
}
if (!empty($matches[3])) {
$clause .= ' '.strtoupper(trim($matches[3]));
}
$order_clauses[] = $clause;
} # end foreach sort field
if ($order_clauses) {
$sql .= ' ORDER BY '.join(', ', $order_clauses);
}
} # end if has sort
if ( !empty($entitySpec['limit']) )
$limit = $entitySpec['limit'];
elseif ( !empty($_REQUEST['count']) )
$limit = validInt($_REQUEST['count']);
$limit_offset = '';
if ( !empty($_REQUEST['offset']) )
$limit_offset = validInt($_REQUEST['offset']) . ', ';
if ( !empty($limit) )
$sql .= ' limit '.$limit_offset.$limit;
if ( isset($limit) && ($limit == 1) ) {
if ( $sqlData = dbFetchOne($sql, NULL, $values) ) {
if (isset($entitySpec['object'])) {
ZM\Debug("Have object".$entitySpec['object']);
$object_name = 'ZM\\'.$entitySpec['object'];
$object = new $object_name($sqlData);
ZM\Debug("Canview:".$object->canView());
if (!$object->canView()) {
ajaxError('Insufficient permissions for '.$entity.' id '.$sqlData['Id']);
return;
}
}
foreach ( $postFuncs as $element=>$func )
$sqlData[$element] = call_user_func($func, $sqlData);
foreach ( $postFunctions as $element=>$function )
$sqlData[$element] = $function($sqlData);
$data = array_merge($data, $sqlData);
}
} else {
$count = 0;
foreach ( dbFetchAll($sql, NULL, $values) as $sqlData ) {
if (isset($entitySpec['object'])) {
ZM\Debug("Have object".$entitySpec['object']);
$object_name = 'ZM\\'.$entitySpec['object'];
$object = new $object_name($sqlData);
ZM\Debug("Canview:".$object->canView());
if (!$object->canView()) continue;
}
foreach ( $postFuncs as $element=>$func )
$sqlData[$element] = call_user_func($func, $sqlData);
foreach ( $postFunctions as $element=>$function )
$sqlData[$element] = $function($sqlData);
$data[] = $sqlData;
if ( isset($limit) && ++$count >= $limit )
break;
} # end foreach
} # end if have limit == 1
} else {
ZM\Debug("No fieldSQL");
}
//ZM\Debug(print_r($data, true));
return $data;
}
function formatDateTime($dt) {
return $dateTimeFormatter->format(strtotime($dt));
}
$data = collectData();
if ( !isset($_REQUEST['layout']) ) {
$_REQUEST['layout'] = 'json';
}
switch ( $_REQUEST['layout'] ) {
case 'xml NOT CURRENTLY SUPPORTED' :
header('Content-type: application/xml');
echo('<?xml version="1.0" encoding="iso-8859-1"?>
');
$entity = strtolower($_REQUEST['entity']);
$entity = preg_replace('/[^A-Za-z0-9]/', '', $entity);
echo '<'.$entity.'>
';
foreach ( $data as $key=>$value ) {
$key = strtolower($key);
echo "<$key>".htmlentities($value)."</$key>\n";
}
echo '</'.$entity.">\n";
break;
case 'json' :
{
$response = array( strtolower(validJsStr($_REQUEST['entity'])) => $data );
if ( ZM_OPT_USE_AUTH && (ZM_AUTH_RELAY == 'hashed') ) {
$auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS);
$response['auth'] = $auth_hash;
$response['auth_relay'] = get_auth_relay();
}
if ( isset($_REQUEST['loopback']) )
$response['loopback'] = validJsStr($_REQUEST['loopback']);
#ZM\Warning(print_r($response, true));
ajaxResponse($response);
break;
}
case 'text' :
header('Content-type: text/plain');
echo join(' ', array_values($data));
break;
default:
ZM\Error('Unsupported layout: '.$_REQUEST['layout']);
}
function getFrameImage() {
$eventId = validCardinal($_REQUEST['eid']);
$frameId = validCardinal($_REQUEST['fid']);
$sql = 'SELECT * FROM Frames WHERE EventId = ? AND FrameId = ?';
if ( !($frame = dbFetchOne($sql, NULL, array($eventId, $frameId))) ) {
ZM\Debug("Frame not found for event $eventId frame $frameId");
$frame = array();
$frame['EventId'] = $eventId;
$frame['FrameId'] = $frameId;
$frame['Type'] = 'Virtual';
}
$event = new ZM\Event($frame['EventId']);
$frame['Image'] = $event->getImageSrc($frame, SCALE_BASE);
return $frame;
}
function getNearFrame() {
$eventId = validCardinal($_REQUEST['id'][0]);
$frameId = validCardinal($_REQUEST['id'][1]);
$sql = 'SELECT FrameId FROM Frames WHERE EventId = ? AND FrameId <= ? ORDER BY FrameId DESC LIMIT 1';
if ( !$nearFrameId = dbFetchOne($sql, 'FrameId', array($eventId, $frameId)) ) {
$sql = 'SELECT * FROM Frames WHERE EventId = ? AND FrameId > ? ORDER BY FrameId ASC LIMIT 1';
if ( !$nearFrameId = dbFetchOne($sql, 'FrameId', array($eventId, $frameId)) ) {
return( array() );
}
}
$_REQUEST['entity'] = 'frame';
$_REQUEST['id'][1] = $nearFrameId;
return collectData();
}
function getNearEvents() {
global $user, $sortColumn, $sortOrder;
$eventId = validCardinal($_REQUEST['id']);
$NearEvents = array('EventId'=>$eventId);
$event = dbFetchOne('SELECT * FROM Events WHERE Id=?', NULL, array($eventId));
if ( !$event ) return $NearEvents;
$filter = ZM\Filter::parse($_REQUEST['filter']);
parseSort();
if ( count($user->unviewableMonitorIds()) ) {
$filter = $filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=>'IN', 'val'=>$user->viewableMonitorIds()));
}
$filter_sql = $filter->sql();
# When listing, it may make sense to list them in descending order.
# But when viewing Prev should be timewise earlier and Next should be after.
if ( $sortColumn == 'E.Id' or $sortColumn == 'E.StartDateTime' ) {
$sortOrder = 'ASC';
}
$sql = '
SELECT E.Id AS Id, E.StartDateTime AS StartDateTime
FROM Events AS E
INNER JOIN Monitors AS M ON E.MonitorId = M.Id
LEFT JOIN Events_Tags AS ET ON E.Id = ET.EventId
LEFT JOIN Tags AS T ON T.Id = ET.TagId
WHERE E.Id != ? AND '.$sortColumn.'
'.($sortOrder=='ASC'?'<=':'>=').' ?';
if ($filter->sql()) {
$sql .= ' AND ('.$filter->sql().')';
}
$sql .= ' AND E.StartDateTime <= ? ORDER BY '.$sortColumn.' '.($sortOrder=='ASC'?'DESC':'ASC');
if ( $sortColumn != 'E.Id' ) {
# When sorting by starttime, if we have two events with the same starttime (different monitors) then we should sort secondly by Id
$sql .= ', E.Id DESC';
}
$sql .= ' LIMIT 1';
$result = dbQuery($sql, [$eventId, $event[$_REQUEST['sort_field']], $event['StartDateTime']]);
if ( !$result ) {
ZM\Error('Failed to load previous event using '.$sql);
return $NearEvents;
}
$prevEvent = dbFetchNext($result);
$sql = '
SELECT E.Id AS Id, E.StartDateTime AS StartDateTime
FROM Events AS E
INNER JOIN Monitors AS M ON E.MonitorId = M.Id
LEFT JOIN Events_Tags AS ET ON E.Id = ET.EventId
LEFT JOIN Tags AS T ON T.Id = ET.TagId
WHERE E.Id != ? AND '.$sortColumn.'
'.($sortOrder=='ASC'?'>=':'<=').' ?';
if ($filter->sql()) {
$sql .= ' AND ('.$filter->sql().')';
}
$sql .= ' AND E.StartDateTime >= ? ORDER BY '.$sortColumn.' '.($sortOrder=='ASC'?'ASC':'DESC');
if ( $sortColumn != 'E.Id' ) {
# When sorting by starttime, if we have two events with the same starttime (different monitors) then we should sort secondly by Id
$sql .= ', E.Id ASC';
}
$sql .= ' LIMIT 1';
$result = dbQuery($sql, [$eventId, $event[$_REQUEST['sort_field']], $event['StartDateTime']]);
if ( !$result ) {
ZM\Error('Failed to load next event using '.$sql);
return $NearEvents;
}
$nextEvent = dbFetchNext($result);
if ( $prevEvent ) {
$NearEvents['PrevEventId'] = $prevEvent['Id'];
$NearEvents['PrevEventStartTime'] = $prevEvent['StartDateTime'];
$NearEvents['PrevEventDefVideoPath'] = getEventDefaultVideoPath($prevEvent['Id']);
} else {
$NearEvents['PrevEventId'] = $NearEvents['PrevEventStartTime'] = $NearEvents['PrevEventDefVideoPath'] = 0;
}
if ( $nextEvent ) {
$NearEvents['NextEventId'] = $nextEvent['Id'];
$NearEvents['NextEventStartTime'] = $nextEvent['StartDateTime'];
$NearEvents['NextEventDefVideoPath'] = getEventDefaultVideoPath($nextEvent['Id']);
} else {
$NearEvents['NextEventId'] = $NearEvents['NextEventStartTime'] = $NearEvents['NextEventDefVideoPath'] = 0;
}
return $NearEvents;
} # end function getNearEvents()
?>