mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 06:42:23 +00:00
fix(cctv): a new camera's frame shows the moment it is decoded, without a fade
The preview settled when the image had loaded, so the panel's <img> took the src while it was still decoding and then faded in over 180 ms: every camera click lingered on an empty box. The preview now settles once the frame is decoded (a rejected decode still shows the loaded frame, and a decode that never settles shows it after one second), and the fade is gone.
This commit is contained in:
1 parent
c02ede3bb4
commit
6e4df4c287
4 files changed
+77
-3
No files matched your search
@@ -2224,7 +2224,9 @@ Frame loading, calibration editing and status display have separate modules;
|
||||
providers, camera placement and navigation policy retain their existing owners.
|
||||
Changing cameras invalidates old image callbacks and cancels an unfinished
|
||||
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.
|
||||
|
||||
## UI disposal
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { CctvControls } from './cctvControls.js';
|
||||
import { CCTV_FRAME_DECODE_WAIT_MS } from './cctvFrames.js';
|
||||
|
||||
function element() {
|
||||
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');
|
||||
});
|
||||
|
||||
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) => {
|
||||
const { controls, requests } = fixture(t);
|
||||
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;
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
export const EMPTY_CCTV_FRAME = Object.freeze({
|
||||
cameraId: '',
|
||||
@@ -127,7 +130,28 @@ export function _queueCctvFrame(src, cameraId, cameraChanged) {
|
||||
|
||||
const preloader = new Image();
|
||||
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.src = src;
|
||||
}
|
||||
|
||||
@@ -250,8 +250,11 @@ html:not([lang='en']) .cctv-controls .scene-btn {
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
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;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
#cctv-video {
|
||||
|
||||
Reference in new issue
Block a user