Merge branch 'main' into i18n-spanish

This commit is contained in:
Meir Michanie authored and GitHub committed 2026-10-14 22:01:40 +02:00
commit 29f2885cda
17 files changed
+257 -16

No files matched your search

+27 -1
View File
@@ -1,3 +1,28 @@
/**
* A v4 UUID for the client lease. `crypto.randomUUID` only exists in secure
* contexts, so a page opened over plain HTTP from another machine (the
* HOST=0.0.0.0 LAN opt-in) has none; `getRandomValues` exists in every context
* and yields the same shape the server's lease check accepts.
*/
export function createLeaseId(cryptoImpl = globalThis.crypto) {
if (typeof cryptoImpl.randomUUID === 'function') {
return cryptoImpl.randomUUID();
}
const bytes = cryptoImpl.getRandomValues(new Uint8Array(16));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0'));
return [
hex.slice(0, 4),
hex.slice(4, 6),
hex.slice(6, 8),
hex.slice(8, 10),
hex.slice(10),
]
.map((group) => group.join(''))
.join('-');
}
/** One decoder per active camera; both surfaces consume this video element. */
export function attachCctvVideo(
video,
@@ -7,13 +32,14 @@ export function attachCctvVideo(
loadHls = () => import('hls.js'),
onFailure = () => {},
fetchImpl = globalThis.fetch,
cryptoImpl = globalThis.crypto,
} = {},
) {
// Preserve replay for existing finite video feeds; live HLS must not loop.
video.loop = feedType !== 'hls';
let disposed = false;
let hls = null;
const leaseId = feedType === 'hls' ? globalThis.crypto.randomUUID() : null;
const leaseId = feedType === 'hls' ? createLeaseId(cryptoImpl) : null;
const mediaUrl =
feedType === 'hls'
? `${url}${url.includes('?') ? '&' : '?'}lease=${leaseId}`
+38 -1
View File
@@ -1,6 +1,15 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { attachCctvVideo } from './videoPlayback.js';
import { webcrypto } from 'node:crypto';
import { attachCctvVideo, createLeaseId } from './videoPlayback.js';
const LEASE_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
/** What an insecure context exposes: getRandomValues but no randomUUID. */
const insecureCrypto = {
getRandomValues: (array) => webcrypto.getRandomValues(array),
};
function video() {
const v = new EventTarget();
Object.assign(v, {
@@ -69,6 +78,34 @@ test('native HLS releases its client lease without response-header access', asyn
assert.equal(releases[0].init.method, 'DELETE');
});
test('lease ids are v4 UUIDs with or without crypto.randomUUID', () => {
assert.match(createLeaseId(), LEASE_V4);
const ids = new Set();
for (let i = 0; i < 64; i++) ids.add(createLeaseId(insecureCrypto));
assert.equal(ids.size, 64);
for (const id of ids) assert.match(id, LEASE_V4);
});
test('HLS starts on a plain-HTTP LAN page without crypto.randomUUID', async () => {
const source = video();
source.canPlayType = () => 'probably';
const playback = attachCctvVideo(source, '/api/cctv/media/a', 'hls', {
loadHls: async () => ({ default: { isSupported: () => false } }),
fetchImpl: async () => {},
cryptoImpl: insecureCrypto,
});
try {
await playback.ready;
const lease = new URL(
source.src,
'http://192.168.1.10:4173/',
).searchParams.get('lease');
assert.match(lease, LEASE_V4);
} finally {
playback.dispose();
}
});
test('finite video feeds retain looping while live HLS does not loop', async () => {
for (const feedType of ['mp4', 'webm', 'hls']) {
const source = video();