mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(cctv): start live HLS on plain-HTTP LAN pages
The client lease id came from crypto.randomUUID, which browsers only expose in secure contexts. Opened over http://<lan-ip>:4173 (the HOST=0.0.0.0 opt-in), starting a live camera feed threw before playback. Fall back to a v4 UUID from crypto.getRandomValues, which the server's lease check accepts.
This commit is contained in:
1 parent
9542a5e58b
commit
f93755af48
4 files changed
+74
-3
No files matched your search
@@ -13,6 +13,12 @@
|
|||||||
largest cities) and `CCTV_VEGVESEN_VIDEO=0` keeps stills only. Attributed
|
largest cities) and `CCTV_VEGVESEN_VIDEO=0` keeps stills only. Attributed
|
||||||
under NLOD 2.0.
|
under NLOD 2.0.
|
||||||
|
|
||||||
|
- Live HLS cameras play on a page opened over plain HTTP from another machine
|
||||||
|
(`HOST=0.0.0.0`). The client lease id came from `crypto.randomUUID`, which
|
||||||
|
browsers only expose in secure contexts, so starting a live feed threw
|
||||||
|
before playback. The id now falls back to a v4 UUID built from
|
||||||
|
`crypto.getRandomValues`, which the server's lease check accepts.
|
||||||
|
|
||||||
## [0.2.1] — 2026-10-02 — God's Eye View through Your Agent (with security fixes and other features)
|
## [0.2.1] — 2026-10-02 — God's Eye View through Your Agent (with security fixes and other features)
|
||||||
|
|
||||||
- Ship each bundled data pack once. The region, marine, admin-boundary,
|
- Ship each bundled data pack once. The region, marine, admin-boundary,
|
||||||
|
|||||||
@@ -4814,7 +4814,9 @@ its paint loop. Camera deactivation destroys the decoder, and fatal playback
|
|||||||
errors revert to the labeled frame fallback. Camera changes clear the old panel
|
errors revert to the labeled frame fallback. Camera changes clear the old panel
|
||||||
frame. hls.js is imported only when an HLS camera starts. Two live sessions may
|
frame. hls.js is imported only when an HLS camera starts. Two live sessions may
|
||||||
coexist with at most eight independent consumer leases each; no-access lease
|
coexist with at most eight independent consumer leases each; no-access lease
|
||||||
expiry is 15 seconds. Closing one consumer leaves other consumers running. Encrypted/fMP4/byte-range playlists fail
|
expiry is 15 seconds. Closing one consumer leaves other consumers running. Lease
|
||||||
|
ids use `crypto.randomUUID` where the page is a secure context and an equivalent
|
||||||
|
v4 id from `crypto.getRandomValues` on plain-HTTP LAN pages. Encrypted/fMP4/byte-range playlists fail
|
||||||
closed. Performance and live agency reliability require recorded validation.
|
closed. Performance and live agency reliability require recorded validation.
|
||||||
|
|
||||||
## Wind forecast models
|
## Wind forecast models
|
||||||
|
|||||||
@@ -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. */
|
/** One decoder per active camera; both surfaces consume this video element. */
|
||||||
export function attachCctvVideo(
|
export function attachCctvVideo(
|
||||||
video,
|
video,
|
||||||
@@ -7,13 +32,14 @@ export function attachCctvVideo(
|
|||||||
loadHls = () => import('hls.js'),
|
loadHls = () => import('hls.js'),
|
||||||
onFailure = () => {},
|
onFailure = () => {},
|
||||||
fetchImpl = globalThis.fetch,
|
fetchImpl = globalThis.fetch,
|
||||||
|
cryptoImpl = globalThis.crypto,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
// Preserve replay for existing finite video feeds; live HLS must not loop.
|
// Preserve replay for existing finite video feeds; live HLS must not loop.
|
||||||
video.loop = feedType !== 'hls';
|
video.loop = feedType !== 'hls';
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let hls = null;
|
let hls = null;
|
||||||
const leaseId = feedType === 'hls' ? globalThis.crypto.randomUUID() : null;
|
const leaseId = feedType === 'hls' ? createLeaseId(cryptoImpl) : null;
|
||||||
const mediaUrl =
|
const mediaUrl =
|
||||||
feedType === 'hls'
|
feedType === 'hls'
|
||||||
? `${url}${url.includes('?') ? '&' : '?'}lease=${leaseId}`
|
? `${url}${url.includes('?') ? '&' : '?'}lease=${leaseId}`
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
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() {
|
function video() {
|
||||||
const v = new EventTarget();
|
const v = new EventTarget();
|
||||||
Object.assign(v, {
|
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');
|
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 () => {
|
test('finite video feeds retain looping while live HLS does not loop', async () => {
|
||||||
for (const feedType of ['mp4', 'webm', 'hls']) {
|
for (const feedType of ['mp4', 'webm', 'hls']) {
|
||||||
const source = video();
|
const source = video();
|
||||||
|
|||||||
Reference in new issue
Block a user