diff --git a/web/ajax/console.php b/web/ajax/console.php
index ffaddccf2..9322a7979 100644
--- a/web/ajax/console.php
+++ b/web/ajax/console.php
@@ -460,7 +460,7 @@ function queryRequest() {
'replay' => 'single', 'rate' => '400'), '&');
if ($event->DefaultVideo()) {
$videoSrc = $event->getStreamSrc(array('mode' => 'mp4'), '&');
- $videoAttr = ' video_src="'.$videoSrc.'"';
+ $videoAttr = ' video_src="'.$videoSrc.'" data-event-start="'.htmlspecialchars($event->StartDateTime()).'"';
}
}
} else {
diff --git a/web/ajax/events.php b/web/ajax/events.php
index 89a6520de..f8f684c83 100644
--- a/web/ajax/events.php
+++ b/web/ajax/events.php
@@ -328,7 +328,7 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$videoAttr = '';
if ($event->DefaultVideo()) {
$videoSrc = $event->getStreamSrc(array('mode'=>'mp4'), '&');
- $videoAttr = ' video_src="' .$videoSrc. '"';
+ $videoAttr = ' video_src="' .$videoSrc. '" data-event-start="'.htmlspecialchars($event->StartDateTime()).'"';
}
// Modify the row data as needed
diff --git a/web/skins/classic/css/base/skin.css b/web/skins/classic/css/base/skin.css
index 3485e3d69..f7867532b 100644
--- a/web/skins/classic/css/base/skin.css
+++ b/web/skins/classic/css/base/skin.css
@@ -919,6 +919,67 @@ li.search-choice {
object-fit: cover;
}
+.thumb-overlay-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.thumb-overlay-status {
+ background: rgba(0, 0, 0, 0.8);
+ color: #fff;
+ padding: 8px 16px;
+ margin-top: 8px;
+ border-radius: 4px;
+ font-family: monospace;
+ font-size: 14px;
+ min-height: 20px;
+}
+
+.live-indicator {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: bold;
+ color: #ff4444;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.live-dot {
+ width: 10px;
+ height: 10px;
+ background: #ff4444;
+ border-radius: 50%;
+ animation: live-pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes live-pulse {
+ 0%, 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0.5;
+ transform: scale(1.2);
+ }
+}
+
+.time-indicator {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #ccc;
+}
+
+.time-indicator .fa {
+ color: #888;
+}
+
+.time-display {
+ font-variant-numeric: tabular-nums;
+}
+
a.flip {
float: right;
padding-right: 5px;
diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js
index 5a5cee5ea..d8d1310d2 100644
--- a/web/skins/classic/js/skin.js
+++ b/web/skins/classic/js/skin.js
@@ -1297,6 +1297,10 @@ function createThumbnailOverlay(img, overlaySrc, dimensions, streamType, monitor
const overlay = document.createElement('div');
overlay.id = 'thumb-overlay';
+ // Wrapper contains video container and status bar
+ const wrapper = document.createElement('div');
+ wrapper.className = 'thumb-overlay-wrapper';
+
// Container uses cached still image as background while stream loads
const container = document.createElement('div');
container.id = 'monitor-thumb-overlay'; // video-stream.js expects parent with id starting with "monitor"
@@ -1314,25 +1318,62 @@ function createThumbnailOverlay(img, overlaySrc, dimensions, streamType, monitor
}
};
- if (overlaySrc === 'live' && useGo2rtc) {
+ // Determine if this is a live stream or recorded video
+ const isLive = (overlaySrc === 'live');
+ const eventStart = img.dataset.eventStart;
+
+ // Create status bar (only if there's content to show)
+ let statusBar = null;
+ if (isLive || eventStart) {
+ statusBar = document.createElement('div');
+ statusBar.className = 'thumb-overlay-status';
+
+ if (isLive) {
+ // Live indicator with pulsing dot
+ statusBar.innerHTML = 'LIVE';
+ } else if (eventStart) {
+ // Wall clock time for recorded video with clock icon
+ statusBar.innerHTML = '' +
+ formatDateTime(new Date(eventStart)) + '';
+ }
+ }
+
+ if (isLive && useGo2rtc) {
createGo2rtcStream(container, go2rtcSrc, monitorId || go2rtcMid, fallbackToMjpeg);
} else if (streamType === 'rtsp2web') {
createRtsp2webStream(container, img, monitorId, fallbackToMjpeg);
} else if (streamType === 'janus') {
// Janus requires complex initialization; fall back to MJPEG
fallbackToMjpeg();
- } else if (overlaySrc !== 'live' && img.getAttribute('video_src') && currentView !== 'frames') {
- createVideoElement(container, overlaySrc);
+ } else if (!isLive && img.getAttribute('video_src') && currentView !== 'frames') {
+ createVideoElement(container, overlaySrc, eventStart, statusBar);
} else {
const overlayImg = document.createElement('img');
overlayImg.src = overlaySrc;
container.appendChild(overlayImg);
}
- overlay.appendChild(container);
+ wrapper.appendChild(container);
+ if (statusBar) wrapper.appendChild(statusBar);
+ overlay.appendChild(wrapper);
document.body.appendChild(overlay);
}
+// Format date/time for display in status bar
+function formatDateTime(date) {
+ if (!(date instanceof Date) || isNaN(date)) return '';
+ const options = {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false
+ };
+ return date.toLocaleString(undefined, options);
+}
+
function createGo2rtcStream(container, src, mid, fallbackToMjpeg) {
ensureVideoStreamLoaded().then(function() {
if (!document.getElementById('thumb-overlay')) return;
@@ -1410,7 +1451,7 @@ function createRtsp2webStream(container, img, monitorId, fallbackToMjpeg) {
}).catch(fallbackToMjpeg);
}
-function createVideoElement(container, src) {
+function createVideoElement(container, src, eventStart, statusBar) {
const video = document.createElement('video');
const previewRate = getPreviewRate();
video.src = src;
@@ -1421,6 +1462,19 @@ function createVideoElement(container, src) {
video.addEventListener('loadedmetadata', function() {
this.playbackRate = previewRate; // Some browsers reset playbackRate on metadata load
});
+
+ // Update wall clock time as video plays
+ if (eventStart && statusBar) {
+ const startTime = new Date(eventStart).getTime();
+ const timeDisplay = statusBar.querySelector('.time-display');
+ if (timeDisplay && !isNaN(startTime)) {
+ video.addEventListener('timeupdate', function() {
+ const currentTime = startTime + (video.currentTime * 1000);
+ timeDisplay.textContent = formatDateTime(new Date(currentTime));
+ });
+ }
+ }
+
container.appendChild(video);
}