From d65402eae382cdec80349a3d6a9c5ba3eb39a5ca Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 12:11:00 -0700 Subject: [PATCH] feat: read current camera frames through the CCTV source Add getFrame(camera) to the CCTV source. It reads one frame through the registered frame endpoint the layer already uses and returns its content type and bytes, so code outside the layer can show a camera's view. --- src/layers/cctv/source.js | 13 +++++++++++++ src/layers/cctv/source.test.mjs | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/layers/cctv/source.js b/src/layers/cctv/source.js index bf4363128..0dddce1b4 100644 --- a/src/layers/cctv/source.js +++ b/src/layers/cctv/source.js @@ -51,5 +51,18 @@ export function createCctvSource({ }, getFrameUrl: frameUrlFor, getMediaUrl: mediaUrlFor, + /** Read one current frame through the registered frame endpoint. */ + async getFrame(camera, { signal } = {}) { + signal?.throwIfAborted(); + const response = await fetchImpl(frameUrlFor(camera), { + cache: 'no-store', + signal, + }); + if (!response.ok) throw new Error('Camera frame HTTP ' + response.status); + const bytes = new Uint8Array(await response.arrayBuffer()); + signal?.throwIfAborted(); + const type = response.headers.get('content-type') || ''; + return { contentType: type.split(';')[0].trim().toLowerCase(), bytes }; + }, }; } diff --git a/src/layers/cctv/source.test.mjs b/src/layers/cctv/source.test.mjs index f0e3b0cc3..0103c03de 100644 --- a/src/layers/cctv/source.test.mjs +++ b/src/layers/cctv/source.test.mjs @@ -148,3 +148,28 @@ test('camera construction is inert and destruction cancels a pending catalog and assert.equal(a.getStats().count, 0); assert.equal(b.getStats().count, 0); }); + +test('frames are read through the registered frame endpoint', async () => { + const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]); + const requested = []; + const source = createCctvSource({ + fetchImpl: async (url, init) => { + requested.push({ url, cache: init.cache }); + return new Response(png, { + headers: { 'Content-Type': 'Image/PNG; charset=binary' }, + }); + }, + }); + const frame = await source.getFrame(camera); + assert.equal(frame.contentType, 'image/png'); + assert.deepEqual([...frame.bytes], [...png]); + assert.equal( + requested[0].url.split('?')[0], + '/api/cctv/frame/pack%2Fcamera%20%3Fx', + ); + assert.equal(requested[0].cache, 'no-store'); + const failing = createCctvSource({ + fetchImpl: async () => new Response(null, { status: 502 }), + }); + await assert.rejects(failing.getFrame(camera), /Camera frame HTTP 502/); +});