mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
cctv: live-video panel card — canvas painted from the active camera's video element, 16:9 wrap, gated against the still-image path
This commit is contained in:
1 parent
e02492cfc2
commit
71aa889133
6 files changed
+64
-1
No files matched your search
@@ -83,6 +83,7 @@ export function createPresentation({
|
||||
mountHeightM: camera.mountHeightM,
|
||||
active: isActive,
|
||||
feedType: camera.feedType,
|
||||
isVideo: parts.model.isVideoFeedType(camera.feedType),
|
||||
sourceKind:
|
||||
health?.sourceKind ||
|
||||
camera.sourceKind ||
|
||||
|
||||
@@ -2618,6 +2618,7 @@ export class StyleManager {
|
||||
_cctvFocusBtn: this._cctvFocusBtn,
|
||||
_cctvFrame: this._cctvFrame,
|
||||
_cctvFrameWrap: this._cctvFrameWrap,
|
||||
_cctvVideo: this._cctvVideo,
|
||||
_cctvMeta: this._cctvMeta,
|
||||
_cctvNearestBtn: this._cctvNearestBtn,
|
||||
_cctvNextBtn: this._cctvNextBtn,
|
||||
|
||||
@@ -164,7 +164,46 @@ export function _renderCctvState(state) {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._cctvFrame) {
|
||||
// Live video: for HLS cameras, paint the active camera's decoded <video>
|
||||
// element onto a canvas each frame (one decoder, two surfaces — the plane
|
||||
// reads the same element). The loop rides through hls.js pipeline resets
|
||||
// (a null element or unchanged currentTime is a skipped frame, not an exit)
|
||||
// and self-stops when the card hides or the feed goes away. The still-image
|
||||
// path below is suppressed while a live feed is showing.
|
||||
const liveIntent = enabled && !!activeCamera?.isVideo;
|
||||
if (this._cctvVideo) {
|
||||
this._cctvVideo.hidden = !liveIntent;
|
||||
if (this._cctvFrame) this._cctvFrame.hidden = liveIntent;
|
||||
this._cctvVideoActive = liveIntent;
|
||||
if (liveIntent && !this._cctvVideoRaf) {
|
||||
const canvas = this._cctvVideo;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
let lastTime = -1;
|
||||
const paint = () => {
|
||||
if (this.destroyed || !this._cctvVideoActive) {
|
||||
this._cctvVideoRaf = 0;
|
||||
return;
|
||||
}
|
||||
const v = this.cctv.getActiveVideoElement();
|
||||
if (v && v.currentTime !== lastTime) {
|
||||
lastTime = v.currentTime;
|
||||
if (
|
||||
canvas.width !== v.videoWidth ||
|
||||
canvas.height !== v.videoHeight
|
||||
) {
|
||||
canvas.width = v.videoWidth;
|
||||
canvas.height = v.videoHeight;
|
||||
}
|
||||
ctx.drawImage(v, 0, 0);
|
||||
}
|
||||
this._cctvVideoRaf = requestAnimationFrame(paint);
|
||||
};
|
||||
this._cctvVideoRaf = requestAnimationFrame(paint);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._cctvFrame && !liveIntent) {
|
||||
const nextSrc = enabled ? activeCamera?.frameUrl : null;
|
||||
const nextCameraId = enabled ? activeCamera?.id || '' : '';
|
||||
const cameraChanged = this._cctvFrame.dataset.cameraId !== nextCameraId;
|
||||
@@ -183,6 +222,8 @@ export function _renderCctvState(state) {
|
||||
if (!nextSrc) {
|
||||
this._clearCctvFrame();
|
||||
}
|
||||
} else if (liveIntent) {
|
||||
this._clearCctvFrame();
|
||||
}
|
||||
|
||||
this._syncCctvSourceBadge(activeCamera, enabled);
|
||||
|
||||
@@ -142,6 +142,7 @@ export function readShellElements(document = globalThis.document) {
|
||||
_cctvCalibResetBtn: document.getElementById('cctv-calib-reset-btn'),
|
||||
_cctvFrame: document.getElementById('cctv-frame'),
|
||||
_cctvFrameWrap: document.getElementById('cctv-frame-wrap'),
|
||||
_cctvVideo: document.getElementById('cctv-video'),
|
||||
_cctvSourceBadge: document.getElementById('cctv-source-badge'),
|
||||
_cctvMeta: document.getElementById('cctv-meta'),
|
||||
_cctvSummary: document.getElementById('cctv-summary'),
|
||||
|
||||
@@ -234,6 +234,8 @@
|
||||
image inside is taller — and the wrap is overflow:hidden, which clipped
|
||||
the bottom of the frame right back off. Never shrink the preview. */
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 16/9;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Lightweight loading shimmer while the active frame is fetching */
|
||||
@@ -298,10 +300,26 @@
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
#cctv-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#cctv-frame.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#cctv-video:not([hidden]) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#cctv-frame[hidden],
|
||||
#cctv-video[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#cctv-meta {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
</div>
|
||||
<div id="cctv-frame-wrap" class="cctv-frame-wrap">
|
||||
<img id="cctv-frame" alt="CCTV feed frame" />
|
||||
<canvas id="cctv-video" class="cctv-frame" hidden></canvas>
|
||||
</div>
|
||||
<div id="cctv-source-badge" role="status" aria-live="polite">SOURCE · UNKNOWN</div>
|
||||
<div id="cctv-meta">Enable CCTV to load camera intersections</div>
|
||||
|
||||
Reference in new issue
Block a user