mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 22:26:33 +00:00
Cloud cameras are now a distinct type ('cloud') managed entirely by
sync. Bidirectional: cameras added in vendor cloud appear automatically,
removed cameras get deleted. Cloud cameras and their entities are
read-only in admin UI — no manual editing.
- Camera type CHECK widened to include 'cloud'
- New columns: cloud_account_id, cloud_vendor_camera_id,
cloud_stream_url, cloud_stream_type
- Repo: upsertCloudCamera, deleteCloudCamerasNotIn,
listCloudCamerasByAccount
- Sync replaces import: full reconciliation per account
- Hik-Connect: fetch HLS preview URLs via previewURLs endpoint
- Tuya: fetch stream URLs during sync (not just on demand)
- Kiosk API: GET /api/kiosk/cameras/:id/stream returns fresh
relay URL from vendor (session-based URLs expire)
- Cloud cameras show read-only detail page with cloud badge
- Coolify compose: postgres 18 as default, BF_DB=postgres,
server depends_on postgres healthy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
57 lines
2.2 KiB
TypeScript
57 lines
2.2 KiB
TypeScript
/**
|
|
* TP-Link (Tapo/VIGI) camera integration.
|
|
*
|
|
* TP-Link has NO public cloud API. Tapo cameras support local RTSP +
|
|
* ONVIF. VIGI is enterprise-only with direct NVR access. For
|
|
* BetterFrame: connect to cameras directly via RTSP on the LAN.
|
|
*
|
|
* RTSP format: rtsp://user:pass@camera_ip/stream1 (main)
|
|
* rtsp://user:pass@camera_ip/stream2 (sub)
|
|
*
|
|
* This provider is essentially a direct-RTSP helper — no cloud relay.
|
|
* All auth on server — kiosk only gets RTSP URLs.
|
|
*/
|
|
import type { CloudCameraProvider, CloudCamera, CloudVendor } from "./types.js";
|
|
|
|
export class TpLinkProvider implements CloudCameraProvider {
|
|
vendor: CloudVendor = "tplink";
|
|
|
|
credentialFields() {
|
|
return [
|
|
{ name: "host", label: "Camera IP/Host", type: "text" as const, required: true },
|
|
{ name: "username", label: "Camera Username", type: "text" as const, required: true },
|
|
{ name: "password", label: "Camera Password", type: "password" as const, required: true },
|
|
];
|
|
}
|
|
|
|
async testCredentials(creds: Record<string, string>): Promise<{ ok: boolean; error?: string }> {
|
|
// No HTTP API — test by probing RTSP port (554).
|
|
const { createConnection } = await import("node:net");
|
|
const host = creds["host"] ?? "localhost";
|
|
return new Promise((resolve) => {
|
|
const sock = createConnection(
|
|
{ host, port: 554, timeout: 3000 },
|
|
() => { sock.destroy(); resolve({ ok: true }); },
|
|
);
|
|
sock.on("error", () => resolve({ ok: false, error: "RTSP port unreachable" }));
|
|
sock.on("timeout", () => { sock.destroy(); resolve({ ok: false, error: "Timeout" }); });
|
|
});
|
|
}
|
|
|
|
async listCameras(creds: Record<string, string>): Promise<CloudCamera[]> {
|
|
return [{
|
|
vendor_id: creds["host"] ?? "unknown",
|
|
name: `TP-Link Camera (${creds["host"]})`,
|
|
model: null,
|
|
rtsp_url: `rtsp://${creds["username"]}:${creds["password"]}@${creds["host"]}/stream1`,
|
|
relay_url: null,
|
|
online: true,
|
|
stream_type: "rtsp",
|
|
extra: {},
|
|
}];
|
|
}
|
|
|
|
async getStreamUrl(creds: Record<string, string>, _vendorCameraId: string): Promise<string | null> {
|
|
return `rtsp://${creds["username"]}:${creds["password"]}@${creds["host"]}/stream1`;
|
|
}
|
|
}
|