fix(onvif): import discovered cameras as type=onvif with credentials

importDiscoveredCamera was hardcoded to type="rtsp", losing ONVIF
identity. Camera edit showed RTSP fields, ONVIF event subscription
skipped (checks cam_type=="onvif"), re-discovery impossible.

Now creates type="onvif" with onvif_host/port/username/password stored
on the camera row. Streams still go into camera_streams (unchanged).
Bundle ships onvif fields → kiosk subscribes to PullPoint events.

Also passes host + port as hidden form fields from discover results
page so the add handler has them available. Basic manual camera
creation via UI stays rtsp-only (simpler); discovery flow produces
onvif type.
This commit is contained in:
Mitchell R 2026-05-22 18:30:41 +02:00
parent 2e40e78413
commit 05ca368f29
No known key found for this signature in database
2 changed files with 16 additions and 4 deletions

View file

@ -174,6 +174,8 @@ function parseDiscoveredStreams(raw: string): DiscoverAddStream[] {
function importDiscoveredCamera(
deps: AdminDeps,
rawName: string,
onvifHost: string,
onvifPort: number,
username: string,
password: string,
streams: DiscoverAddStream[],
@ -185,9 +187,13 @@ function importDiscoveredCamera(
const cam = deps.repo.createCamera({
name,
type: "rtsp",
type: "onvif",
rtsp_url: mainRtspUrl,
});
onvif_host: onvifHost,
onvif_port: onvifPort,
onvif_username: username,
onvif_password: password,
} as any);
for (const stream of streams) {
const width = stream.width == null ? null : Number(stream.width);
const height = stream.height == null ? null : Number(stream.height);
@ -587,6 +593,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
return htmlPage(CameraDiscoverResultsPage({
user: user.username,
host,
port,
username,
password,
cameras,
@ -603,6 +610,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
app.post("/admin/cameras/discover/add", async (event) => {
const body = await readBody<Record<string, string | string[]>>(event);
const onvifHost = formValue(body?.["host"]).trim();
const onvifPort = parseInt(formValue(body?.["port"]) || "80", 10) || 80;
const username = formValue(body?.["username"]).trim();
const password = formValue(body?.["password"]);
let imported = 0;
@ -613,7 +622,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
const rawName = formValue(body?.[`camera_${idx}_name`]).trim() || "ONVIF camera";
const streams = parseDiscoveredStreams(formValue(body?.[`camera_${idx}_streams_json`]));
if (streams.length === 0) continue;
const camId = importDiscoveredCamera(deps, rawName, username, password, streams);
const camId = importDiscoveredCamera(deps, rawName, onvifHost, onvifPort, username, password, streams);
if (camId != null) {
deps.nodered.forward("camera.changed", { camera_id: camId, event: "created", source: "server" });
}
@ -623,7 +632,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
const rawName = formValue(body?.["name"]).trim() || "ONVIF camera";
const streams = parseDiscoveredStreams(formValue(body?.["streams_json"]));
if (streams.length > 0) {
const camId = importDiscoveredCamera(deps, rawName, username, password, streams);
const camId = importDiscoveredCamera(deps, rawName, onvifHost, onvifPort, username, password, streams);
if (camId != null) {
deps.nodered.forward("camera.changed", { camera_id: camId, event: "created", source: "server" });
}

View file

@ -313,6 +313,7 @@ interface DiscoveredCameraRow {
interface CameraDiscoverResultsProps {
user: string;
host: string;
port?: number;
username: string;
password: string;
cameras: DiscoveredCameraRow[];
@ -438,6 +439,8 @@ export function CameraDiscoverResultsPage(props: CameraDiscoverResultsProps) {
<div class="card" style="text-align:center; color:#999; padding:2rem">No profiles returned</div>
) : (
<form method="post" action="/admin/cameras/discover/add">
<input type="hidden" name="host" value={props.host} />
<input type="hidden" name="port" value={String(props.port ?? 80)} />
<input type="hidden" name="username" value={props.username} />
<input type="hidden" name="password" value={props.password} />
<div style="display:flex; gap:0.5rem; align-items:center; margin-bottom:1rem; flex-wrap:wrap">