mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 06:42:23 +00:00
Merge branch 'main' into sameh/cctv-globe-plane-clamp
This commit is contained in:
5 files changed
+79
-3
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
- Show a new CCTV camera's frame the moment it is decoded, with no fade, so
|
||||||
|
switching cameras no longer lingers on an empty preview (samehkhamis, #1693).
|
||||||
@@ -2224,7 +2224,9 @@ Frame loading, calibration editing and status display have separate modules;
|
|||||||
providers, camera placement and navigation policy retain their existing owners.
|
providers, camera placement and navigation policy retain their existing owners.
|
||||||
Changing cameras invalidates old image callbacks and cancels an unfinished
|
Changing cameras invalidates old image callbacks and cancels an unfinished
|
||||||
calibration edit. Failed refreshes preserve settled pixels for the same camera,
|
calibration edit. Failed refreshes preserve settled pixels for the same camera,
|
||||||
while a newly selected camera never shows the prior camera's image. Disposal
|
while a newly selected camera never shows the prior camera's image. A frame is
|
||||||
|
shown without a fade once it is decoded (or after one second if its decode never
|
||||||
|
settles), so a new camera's preview is blank only while its frame loads. Disposal
|
||||||
releases listeners, subscriptions, image handlers, summary timers and chip-hide timers.
|
releases listeners, subscriptions, image handlers, summary timers and chip-hide timers.
|
||||||
|
|
||||||
## UI disposal
|
## UI disposal
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import { CctvControls } from './cctvControls.js';
|
import { CctvControls } from './cctvControls.js';
|
||||||
|
import { CCTV_FRAME_DECODE_WAIT_MS } from './cctvFrames.js';
|
||||||
|
|
||||||
function element() {
|
function element() {
|
||||||
const classes = new Set();
|
const classes = new Set();
|
||||||
@@ -59,6 +60,50 @@ test('a late image completion cannot replace a newer camera preview', (t) => {
|
|||||||
assert.equal(controls._frame.cameraId, 'b');
|
assert.equal(controls._frame.cameraId, 'b');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a camera frame settles once decoded, and a stale decode cannot land', async (t) => {
|
||||||
|
const { controls, requests } = fixture(t);
|
||||||
|
const decodes = [];
|
||||||
|
const queue = (src, id) => {
|
||||||
|
controls._queueCctvFrame(src, id, true);
|
||||||
|
const request = requests.at(-1);
|
||||||
|
request.decode = () =>
|
||||||
|
new Promise((resolve) => decodes.push({ src, resolve }));
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
const first = queue('first.jpg', 'a');
|
||||||
|
first.onload();
|
||||||
|
assert.equal(controls._frame.hasFrame, false);
|
||||||
|
const second = queue('second.jpg', 'b');
|
||||||
|
second.onload();
|
||||||
|
decodes[1].resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
decodes[0].resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(controls._frame.src, 'second.jpg');
|
||||||
|
assert.equal(controls._frame.cameraId, 'b');
|
||||||
|
assert.equal(controls._frame.hasFrame, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a loaded frame shows when its decode rejects or never settles', async (t) => {
|
||||||
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
|
const { controls, requests } = fixture(t);
|
||||||
|
controls._queueCctvFrame('broken.jpg', 'a', true);
|
||||||
|
requests[0].decode = () => Promise.reject(new Error('EncodingError'));
|
||||||
|
requests[0].onload();
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(controls._frame.src, 'broken.jpg');
|
||||||
|
assert.equal(controls._frame.hasFrame, true);
|
||||||
|
|
||||||
|
controls._queueCctvFrame('hidden.jpg', 'b', true);
|
||||||
|
requests[1].decode = () => new Promise(() => {});
|
||||||
|
requests[1].onload();
|
||||||
|
assert.equal(controls._frame.hasFrame, false);
|
||||||
|
t.mock.timers.tick(CCTV_FRAME_DECODE_WAIT_MS);
|
||||||
|
assert.equal(controls._frame.src, 'hidden.jpg');
|
||||||
|
assert.equal(controls._frame.hasFrame, true);
|
||||||
|
});
|
||||||
|
|
||||||
test('failed refresh preserves settled pixels, but changing cameras clears them', (t) => {
|
test('failed refresh preserves settled pixels, but changing cameras clears them', (t) => {
|
||||||
const { controls, requests } = fixture(t);
|
const { controls, requests } = fixture(t);
|
||||||
controls._queueCctvFrame('first.jpg', 'a', true);
|
controls._queueCctvFrame('first.jpg', 'a', true);
|
||||||
|
|||||||
+25
-1
@@ -77,6 +77,9 @@ export function cctvSourceLabelText(label) {
|
|||||||
return SOURCE_LABELS[text] ? t(SOURCE_LABELS[text]) : text;
|
return SOURCE_LABELS[text] ? t(SOURCE_LABELS[text]) : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Longest wait for a loaded frame's decode before it is shown anyway. */
|
||||||
|
export const CCTV_FRAME_DECODE_WAIT_MS = 1000;
|
||||||
|
|
||||||
/** The preview before any frame: nothing requested, nothing shown. */
|
/** The preview before any frame: nothing requested, nothing shown. */
|
||||||
export const EMPTY_CCTV_FRAME = Object.freeze({
|
export const EMPTY_CCTV_FRAME = Object.freeze({
|
||||||
cameraId: '',
|
cameraId: '',
|
||||||
@@ -127,7 +130,28 @@ export function _queueCctvFrame(src, cameraId, cameraChanged) {
|
|||||||
|
|
||||||
const preloader = new Image();
|
const preloader = new Image();
|
||||||
this._cctvFramePreloader = preloader;
|
this._cctvFramePreloader = preloader;
|
||||||
preloader.onload = () => this._settleCctvFrame(token, src, true);
|
// Settle once the image is decoded, not merely loaded: the panel's <img>
|
||||||
|
// then paints the frame the moment it takes the src, instead of showing
|
||||||
|
// its empty box for the decode.
|
||||||
|
// A decode that rejects after a load still has drawable data, as it was
|
||||||
|
// before decoding was awaited. A decode that never settles (a hidden tab may
|
||||||
|
// hold it until the page draws) shows the loaded frame after
|
||||||
|
// CCTV_FRAME_DECODE_WAIT_MS rather than leaving the preview loading.
|
||||||
|
preloader.onload = () => {
|
||||||
|
if (typeof preloader.decode !== 'function') {
|
||||||
|
this._settleCctvFrame(token, src, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let settled = false;
|
||||||
|
const settle = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
this._settleCctvFrame(token, src, true);
|
||||||
|
};
|
||||||
|
const timer = setTimeout(settle, CCTV_FRAME_DECODE_WAIT_MS);
|
||||||
|
preloader.decode().then(settle, settle);
|
||||||
|
};
|
||||||
preloader.onerror = () => this._settleCctvFrame(token, src, false);
|
preloader.onerror = () => this._settleCctvFrame(token, src, false);
|
||||||
preloader.src = src;
|
preloader.src = src;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,8 +250,11 @@ html:not([lang='en']) .cctv-controls .scene-btn {
|
|||||||
height: auto;
|
height: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
|
/* No fade: a camera change hides the old frame at once (never its pixels
|
||||||
|
under the new camera) and shows the new one as soon as it is decoded. A
|
||||||
|
180 ms fade-in after every arriving frame made each camera click linger
|
||||||
|
on black. */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 180ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#cctv-video {
|
#cctv-video {
|
||||||
|
|||||||
Reference in new issue
Block a user